На третьем этапе олимпиады мы, как обычно, решали задачки на SQL, но в этом году надо было написать запрос не просто правильный, но и короткий. Чем короче — тем лучше результат. В детстве мы развлекались таким на микрокалькуляторах и на ассемблере, а сейчас я решил посмотреть, что получится, если попробовать то же на SQL. Получилось, на мой взгляд, интересно. Практического смысла в этом, конечно, никакого нет, но практики и на работе хватит, а тут мы развлекаемся.
Чтобы хорошо выступить, надо было — помимо прочего — выстроить правильную стратегию. Сразу писать максимально короткий запрос, без пробелов и с односимвольными именами не получится — легко самому запутаться. Поэтому сначала надо было решить задачу «по-человечески», а уже потом применить всякие микрооптимизации и получить заветные баллы. Но решить задачу, даже простую, всегда можно разными способами, и не всегда заранее понятно, какой из вариантов окажется короче после оптимизации. Поэтому нужно было не останавливаться, пробовать разные подходы, и при этом аккуратно хранить все версии, чтобы в любой момент можно было посмотреть на запрос еще раз и, чем Тьюринг не шутит, выиграть байтик-другой.
Мы традиционно разрешали пользоваться всеми благами интернета, включая ИИ. На эту тему многие сейчас переживают, но, честно говоря, я пока не вижу причин для беспокойства. Вот если бы все участники показали одинаково прекрасный результат, пришлось бы что-то придумывать. И то, конечно, не запрещать ИИ, а делать задачи более сложными. Но результаты у всех разные, и без собственной головы на плечах их не удалось бы получить (я попробовал), поэтому пока все хорошо. Если финалисты меня читают, было бы интересно услышать комментарии от первого лица: пользовались ли вы ИИ, насколько он вам помог или, может быть, наоборот, только отвлекал?
Итак, к задачам. Их было три.
1. Квайн
Условие
Напишите запрос, который выводит самого себя.
Результат должен в точности совпасть с текстом, вставленным в поле ввода формы проверки. Точка с запятой в конце запроса допускается, но не является обязательной.
Решение
Первая задача шла в качестве разминки, поскольку решение легко гуглится. Первая же ссылка на SO дает нам такой вариант из 130 символов:
SELECT left(v,61)||chr(39)||v||chr(39)||right(v,3)FROM(SELECT'SELECT left(v,61)||chr(39)||v||chr(39)||right(v,3)FROM(SELECTv);'v);
Если немного подумать и посмотреть, как решается такая задача на других языках (типа Питона), можно заметить, что в основном эксплуатируется возможность интерполяции строк самих в себя. Вот пример из википедии:
b = 'b = %s%s%s; print(b %% (chr(39), b, chr(39)))'; print(b % (chr(39), b, chr(39)))
Но ровно тот же прием можно применить и в Постгресе, поскольку у нас есть функция format
. Заодно избавимся и от ненужной по условию точки с запятой в конце. Получаем 102 символа:
SELECT format(v,chr(39),v,chr(39))FROM(SELECT'SELECT format(v,chr(39),v,chr(39))FROM(SELECT%s%s%sv)'v)
Владимир Плахотников заметил, что можно обойтись без кавычек, задействовав спецификатор формата %L
, который позаботится о кавычках сам. Это серьезное улучшение, 70 символов:
SELECT format(f,f)FROM(SELECT'SELECT format(f,f)FROM(SELECT%L f)f' f)f
Но и этот результат можно улучшить за счет того, что во FROM
допускается не только подзапрос, но и функция. Нам нужна стандартная функция с коротким именем, чтобы принимала строку и возвращала ее же без изменений. И такая действительно есть — это trim
. Получаем 64 символа:
SELECT format(f,f)FROM trim('SELECT format(f,f)FROM trim(%L)f')f
До такого трюка никто не догадался, к сожалению. Если придумаете вариант короче — поделитесь в комментариях.
А на конкурсе лучший вариант предложил Артем Сухов, обойдясь одним аргументом format
вместо двух за счет хитрого спецификатора формата (68 символов):
SELECT format('SELECT format(%1$L,%1$L)','SELECT format(%1$L,%1$L)')
Из курьезного. Несколько участников сообразили, что есть системная функция current_query
, которая возвращает текущий запрос. Молодцы, я про нее совсем забыл. К счастью, проверочная система отвергла такое решение (поскольку внутри оборачивает запрос в WITH
), и это правильно, поскольку оно противоречит духу квайнов. Честный запрос должен конструировать свой текст, а не получать его из памяти системы.
2. Лужи
Условие
В таблице elevation
заданы высоты некоторого ландшафта:
CREATE TABLE elevation(
x integer PRIMARY KEY, -- координата x
h integer CHECK (h BETWEEN 0 AND 10) -- высота
);
Если координата пропущена, считается, что в этом месте высота равна нулю. Общая ширина ландшафта не превышает тысячи.
Сколько воды останется после дождя в лужах?
Например, для следующих данных ожидается целочисленный ответ 4:
INSERT INTO elevation VALUES (-5,1), (-3,2), (-2,3), (-1,1), (1,2);

Решение
Известная задача с литкода. Об этом, конечно, надо было еще догадаться, но это как раз звездный час ИИ. Простой вопрос к, скажем, DeepSeek про задачу о лужах тут же выдает и саму задачу, и решение на Питоне, которое требует O(n) по времени и O(1) по памяти. Вопрос в том, что с этим делать дальше. И-интеллект и тут, конечно, что-то предложит, но уже начнет плавать, поэтому включаем голову.
Классическое решение использует два указателя, которые сначала стоят на левой и на правой границах и постепенно стягиваются внутрь, попутно вычисляя максимум слева и справа. Проблема в том, что простое воспроизведение этого алгоритма требует конструирования массива из таблицы и итеративного (рекурсивного) запроса. К тому же в таблице нумерация произвольная, а array_agg
пронумерует элементы с единицы, придется пересчитывать. Это даже звучит неприятно; ясно, что получится громоздко (но можете попробовать для разминки).
Суть же решения в том, что остаток воды в каждой клетке определяется минимальным превышением стен по бокам от клетки (как слева, так и справа от нее) над высотой в самой клетке. Два указателя нужны лишь для того, чтобы не считать левый и правый максимумы отдельно для каждой клетки, а вычислять их «на лету» по мере движения по массиву. Но ведь в SQL это реализуется оконными функциями — вот их и попробуем применить.
Для начала заметим, что в классической задаче массив высот непрерывен, а наша таблица может содержать пропуски. Давайте их ликвидируем.
Надо учитывать, что координаты могут быть любые, в том числе отрицательные (нельзя сэкономить, взяв в качестве минимума единицу, например). Получим для начала что-то такое:
SELECT g.x, coalesce(e.h,0)
FROM
(SELECT min(x) minx, max(x) maxx FROM elevation),
generate_series(minx,maxx) g(x)
LEFT JOIN elevation e ON e.x = g.x;
x | coalesce
----+----------
-5 | 1
-4 | 0
-3 | 2
-2 | 3
-1 | 1
0 | 0
1 | 2
(7 rows)
Многовато будет. Надо избавиться от длинных идентификаторов и от всех псевдонимов (это всегда можно сделать, выбирая однобуквенные идентификаторы, пока хватает разных букв), а также от лишних подзапросов. Например так:
SELECT x, coalesce(h,0)
FROM (
SELECT generate_series(min(x), max(x)) x FROM elevation
)
LEFT JOIN elevation USING(x);
Напомню, что начиная с PostgreSQL 16 псевдонимы для подзапросов во FROM
стали необязательными — многие участники этого не учли и потеряли драгоценные байтики. Заметьте также, что за счет выбора одинаковых идентификаторов (x
) условие соединения удалось сократить, применив USING
.
На самом деле можно сэкономить еще один байтик за счет совсем уже бесполезного в обычной жизни слова NATURAL
:
SELECT x, coalesce(h,0) h
FROM (
SELECT generate_series(min(x), max(x)) x FROM elevation
)
NATURAL LEFT JOIN elevation;
Останавливаюсь на всем этом подробно, только чтобы продемонстрировать некоторые базовые приемы микрооптимизации.
Теперь, применяя полученный блок, добавим вычисление максимума — отдельно слева и справа. Максимум слева от клетки (включая саму клетку) нам даст функция с определением окна OVER (ORDER BY x)
, а справа — с определением окна OVER (ORDER BY x DESC)
:
WITH m AS (
SELECT x, coalesce(h,0) h FROM (
SELECT generate_series(min(x), max(x)) x FROM elevation
)
NATURAL LEFT JOIN elevation
),
a AS (
SELECT h,
max(h) OVER (ORDER BY x) l,
max(h) OVER (ORDER BY x DESC) r
FROM m
)
SELECT * FROM a;
h | l | r
---+---+---
1 | 1 | 3
0 | 1 | 3
2 | 2 | 3
3 | 3 | 3
1 | 3 | 2
0 | 3 | 2
2 | 3 | 2
(7 rows)
Похоже на правду — максимум слева растет, максимум справа уменьшается (проверьте по картинке! — повторю ее).

Остается для каждой клетки посчитать минимальное превышение высоты (least(p,q)-h
) и все просуммировать:
WITH m AS (
SELECT x, coalesce(h,0) h FROM (
SELECT generate_series(min(x), max(x)) x FROM elevation
)
NATURAL LEFT JOIN elevation
),
a AS (
SELECT h,
max(h) OVER (ORDER BY x) l,
max(h) OVER (ORDER BY x DESC) r
FROM m
)
SELECT coalesce(sum(least(l,r)-h),0)
FROM a;
coalesce
----------
4
(1 row)
Обратите внимание на coalesce
: он нужен, чтобы для пустого поля вернуть 0.
Хорошую микрооптимизацию придумал Евгений Жабко: вместо ORDER BY x DESC
написать ORDER BY-x
. Пять байт на дороге не валяются!
Кроме того, можно перенести CTE в подзапросы и тоже немного выиграть.
Такой вариант в стиле Ивана Максимова дает нам более чем конкурентные 202 символа (сам Иван получил 208, что тоже очень неплохо):
SELECT COALESCE(SUM(LEAST(l,r)-d),0)FROM(SELECT h,COALESCE(h,0)d,MAX(h)OVER(ORDER BY x)l,MAX(h)OVER(ORDER BY-x)r FROM(SELECT generate_series(min(x),max(x))x FROM elevation)x NATURAL LEFT JOIN elevation)
Пожалуй, это все, что можно выжать из подхода с вычислением максимумов.
* * *
Однако задача несколько богаче. Когда я размышлял над ней, мне пришло в голову, что сам-то я не буду вычислять максимумы. Я возьму лист в клетку, закрашу квадратики-стены, а затем для каждого уровня посчитаю, сколько незакрашенных клеток осталось между закрашенными.
Как реализовать эту идею на SQL? Например, давайте превратим нашу таблицу в горизонтальные текстовые строки:
WITH m AS (
SELECT x, coalesce(h,0) h FROM (
SELECT generate_series(min(x), max(x)) x FROM elevation
)
NATURAL LEFT JOIN elevation
),
a AS (
SELECT d, string_agg(chr(65+(h<d)::int),'' ORDER BY x) s
FROM m, generate_series(1,10) d
GROUP BY d
)
SELECT *
FROM a
ORDER BY d DESC;
d | s
----+---------
10 | BBBBBBB
9 | BBBBBBB
8 | BBBBBBB
7 | BBBBBBB
6 | BBBBBBB
5 | BBBBBBB
4 | BBBBBBB
3 | BBBABBB
2 | BBAABBA
1 | ABAAABA
(10 rows)
Тут символом A обозначены стены, а B — все остальное. Осталось выкинуть те B, которые по краям, и посчитать оставшиеся. Например, так:
WITH m AS (
SELECT x, coalesce(h,0) h FROM (
SELECT generate_series(min(x), max(x)) x FROM elevation
)
NATURAL LEFT JOIN elevation
),
a AS (
SELECT d, string_agg(chr(65+(h<d)::int),'' ORDER BY x) s
FROM m, generate_series(1,10) d
GROUP BY d
)
SELECT coalesce(sum(length(replace(trim(s,'B'),'A',''))),0)
FROM a;
Этот вариант дает нам 287 символов:
WITH m AS(SELECT x,coalesce(h,0)h FROM(SELECT generate_series(min(x),max(x))x FROM elevation)NATURAL LEFT JOIN elevation),a AS(SELECT d,string_agg(chr(65+(h<d)::int),''ORDER BY x)s FROM m,generate_series(1,10)d GROUP BY d)SELECT coalesce(sum(length(replace(trim(s,'B'),'A',''))),0)FROM a
Можно еще немного побороться, но понятно, что такой вариант проигрывает вычислению максимумов. К счастью, Артем Назаров пошел дальше и догадался, что никакие символы вовсе не нужны: достаточно из ширины ландшафта на каждом уровне вычесть количество стен, достигающих этого уровня. Замечательно, что для такого решения пропуски нумерации перестают быть помехой. И вуаля — рекорд соревнования, 179 символов!
Приведу оригинальный запрос Артема в отформатированном виде:
SELECT coalesce(sum(a),0)
FROM(
SELECT(max(x)-min(x)+1)-cardinality(array_agg(x))a
FROM elevation
CROSS JOIN(
SELECT generate_series(1,max(h))w FROM elevation
)
WHERE(h>=w)
GROUP BY w
)
Причем этот результат можно значительно улучшить нехитрыми микрооптимизациями (заменить CROSS JOIN
на запятую, заменить вычисление максимума высоты на константу 10, убрать лишние скобки, заменить нестрогое неравенство на строгое) до всего-навсего 139 символов!
SELECT coalesce(sum(a),0)FROM(SELECT max(x)-min(x)+1-cardinality(array_agg(x))a FROM elevation,generate_series(0,10)w WHERE h>w GROUP BY w)
Ну что, кто меньше?
3. Лужи 3D
Условие
В таблице elevation3d
заданы высоты некоторого объемного ландшафта:
CREATE TABLE elevation3d(
x integer, -- координата x
y integer, -- координата y
PRIMARY KEY (x,y),
h integer CHECK (h BETWEEN 0 AND 10) -- высота
);
Если координата пропущена, считается, что в этом месте высота равна нулю. Общая длина и общая ширина ландшафта не превышают пятидесяти.
По диагоналям вода не течет. Сколько воды останется после дождя в лужах? Например, для следующих данных ожидается целочисленный ответ 2:
INSERT INTO elevation3d VALUES
(3,3,3), (4,3,3),
(2,4,2), (5,4,2),
(3,5,1), (4,5,2);

Решение
Это тоже задача с литкода. Принципиально, что решение для линейного случая не масштабируется: недостаточно посчитать максимумы по вертикали и горизонтали от клетки, поскольку вода может утечь из нее извилистым путем.
Стандартное решение использует очередь с приоритетом, в которую сначала заносятся все клетки, стоящие по краям ландшафта. Затем из очереди изымается самая низкая клетка, а в очередь добавляются соседние еще не просмотренные клетки. На каждом шаге отслеживается максимум высоты посещенных клеток, а количество воды определяется разностью этого максимума и высоты в текущей клетке.
В традиционных языках очередь с приоритетом обычно реализуют на основе кучи. В SQL ее можно по-простому сымитировать обычным массивом; массив придется каждый раз сортировать, но у нас не такой объем данных, чтобы это стало критичным. Хуже то, что запрос получится весьма большим (опять же, попробуйте для разминки; у меня получилось более 800 символов).
Снова оказывается, что известное, оптимальное решение для традиционных языков оказывается неудобным для SQL. Что и логично. Значит, надо думать дальше.
Можно ведь обойтись и без очереди с приоритетом, если согласиться выполнить некоторую избыточную работу. Вода выливается с краев ландшафта. Давайте возьмем одну какую-нибудь клетку с краю и развернем задачу: попробуем залить воду внутрь ландшафта через эту клетку. Вода покрывает менее высокие клетки, но не проходит через более высокие. Это поиск связной области, с этим мы встречались в задаче про Го.
Нам понадобится рекурсивный запрос, и, конечно, эту процедуру надо выполнить сразу для всех крайних клеток. Таким образом каждая клетка ландшафта получит набор высот покрывающей ее воды; из этого набора надо оставить минимальную. После этого останется только просуммировать разности высот воды и дна.
За дело. Начнем, как и в прошлой задаче, с заполнения пропусков. Практически все решившие эту задачу использовали такой вариант:
WITH RECURSIVE m AS(
SELECT min(x) a, max(x) b, min(y) c, max(y) d FROM elevation3d
), a AS(
SELECT x,y,coalesce(h,0) h
FROM(
SELECT x,y FROM m, generate_series(a,b)x, generate_series(c,d)y
) NATURAL LEFT JOIN elevation3d
)
SELECT * FROM a;
x | y | h
---+---+---
2 | 3 | 0
2 | 4 | 2
2 | 5 | 0
3 | 3 | 3
3 | 4 | 0
3 | 5 | 1
4 | 3 | 3
4 | 4 | 0
4 | 5 | 2
5 | 3 | 0
5 | 4 | 2
5 | 5 | 0
(12 rows)
Хотя на самом деле подзапрос a
можно записать чуть короче, без вложенности:
a AS(
SELECT x,y,coalesce(h,0) h
FROM m, generate_series(a,b)x CROSS JOIN generate_series(c,d)y
NATURAL LEFT JOIN elevation3d
)
Дальше добавляем рекурсивный запрос.
WITH RECURSIVE m AS(
SELECT min(x) a, max(x) b, min(y) c, max(y) d FROM elevation3d
), a AS(
SELECT x,y,coalesce(h,0) h
FROM m, generate_series(a,b)x CROSS JOIN generate_series(c,d)y
NATURAL LEFT JOIN elevation3d
), r AS(
-- клетки с краю
SELECT x,y,h,h l FROM a,m WHERE x IN(a,b) OR y IN(c,d)
UNION
-- заливаем вглубь
SELECT a.x,a.y,a.h,greatest(r.l,a.h)
FROM (VALUES(1,0),(-1,0),(0,1),(0,-1)) d(i,j) CROSS JOIN
r JOIN a ON (a.x,a.y)=(r.x+i,r.y+j)
)
SELECT x,y,h,min(l),min(l)-h delta
FROM r
GROUP BY x,y,h;
x | y | h | min | delta
---+---+---+-----+-------
2 | 3 | 0 | 0 | 0
4 | 3 | 3 | 3 | 0
4 | 4 | 0 | 1 | 1
5 | 5 | 0 | 0 | 0
3 | 5 | 1 | 1 | 0
2 | 5 | 0 | 0 | 0
2 | 4 | 2 | 2 | 0
3 | 4 | 0 | 1 | 1
4 | 5 | 2 | 2 | 0
5 | 3 | 0 | 0 | 0
3 | 3 | 3 | 3 | 0
5 | 4 | 2 | 2 | 0
(12 rows)
Остается просуммировать дельты и ответ готов.
Очень хочется избавиться от подзапроса с VALUES
, и несколько человек нашли способ: достаточно соединить a
и r
по условию abs(a.x-r.x)+abs(a.y-r.y)=1
. Еще немного микрооптимизаций, и получаем такой вариант:
WITH RECURSIVE m AS(
SELECT min(x) a, max(x) b, min(y) c, max(y) d FROM elevation3d
), a AS(
SELECT x,y,coalesce(h,0) h
FROM m, generate_series(a,b)x CROSS JOIN generate_series(c,d)y
NATURAL LEFT JOIN elevation3d
), r AS(
SELECT a.*,h l FROM a,m WHERE x IN(a,b) OR y IN(c,d)
UNION
SELECT a.*,greatest(r.l,a.h)
FROM r JOIN a ON abs(a.x-r.x)+abs(a.y-r.y)=1
)
SELECT coalesce(sum(d),0)
FROM (SELECT min(l)-h d FROM r GROUP BY x,y,h);
Псевдонимы в подзапросе r
портят всю красоту, правда? Давайте избавимся и от них, переименовав поля подзапроса — этим мы выиграем аж 4 символа.
WITH RECURSIVE m AS(
SELECT min(x) a, max(x) b, min(y) c, max(y) d FROM elevation3d
), a AS(
SELECT x,y,coalesce(h,0) h
FROM m, generate_series(a,b)x CROSS JOIN generate_series(c,d)y
NATURAL LEFT JOIN elevation3d
), r(i,j,k,l) AS(
SELECT a.*,h FROM a,m WHERE x IN(a,b) OR y IN(c,d)
UNION
SELECT a.*,greatest(l,h)
FROM r JOIN a ON abs(x-i)+abs(y-j)=1
)
SELECT coalesce(sum(d),0)
FROM (SELECT min(l)-k d FROM r GROUP BY i,j,k);
Еще один байтик можно сэкономить, переставив местами слагаемые в основном запросе. В компактном виде, без пробелов и переводов строк, запрос занимает 401 символ:
WITH RECURSIVE m AS(SELECT min(x)a,max(x)b,min(y)c,max(y)d FROM elevation3d),a AS(SELECT x,y,coalesce(h,0)h FROM m,generate_series(a,b)x CROSS JOIN generate_series(c,d)y NATURAL LEFT JOIN elevation3d),r(i,j,k,l)AS(SELECT a.*,h FROM a,m WHERE x IN(a,b) OR y IN(c,d)UNION SELECT a.*,greatest(l,h) FROM r JOIN a ON abs(x-i)+abs(y-j)=1)SELECT coalesce(sum(d),0)FROM(SELECT-k+min(l)d FROM r GROUP BY i,j,k)
Кто меньше?
А на конкурсе минимум получился у Ивана Максимова — 437 символов, подвели избыточные подзапросы и соединения.
Двое участников попробовали проверочную систему на прочность и нашли в ней брешь — я неправильно написал тест на тот случай, когда нумерация не укладывается в интервал от 1 до 50. За счет этого им удалось выиграть 62 символа, убрав вычисление границ и заменив их на константы. Увы, мне пришлось скорректировать результаты, добавив сэкономленные баллы обратно, а впредь буду аккуратнее и внимательнее, чтобы не допускать таких промахов.
* * *
Есть ли альтернатива рассмотренному решению? Конечно, есть. Давайте посмотрим срез ландшафта на определенной высоте. Очевидно, что вода, остающаяся на этом уровне, это вода внутри стен.

Чтобы вычислить количество воды, достаточно закрасить все пустые клетки, сообщающиеся с краями ландшафта, и пересчитать оставшиеся незакрашенные. Конечно, эту процедуру надо выполнить для каждого из возможных уровней, поэтому в запросе появится декартово произведение с generate_series(1,10)
— количество высот фиксировано по условию.
Собственно, именно так и был устроен мой изначальный запрос. Со всеми оптимизациями мне удалось довести такое решение до 426 символов, что, увы, не дотягивает до рекорда. Вот этот запрос в «приличном» виде:
WITH RECURSIVE m AS (
SELECT min(x) a, max(x) b, min(y) c, max(y) d FROM elevation3d
), e AS (
SELECT x, y, x IN (a,b) OR y IN (c,d) o, l
FROM m, generate_series(a,b) x
CROSS JOIN generate_series(c,d) y
NATURAL LEFT JOIN elevation3d
JOIN generate_series(1,10) l ON coalesce(h,0) < l
), r(p,q,b,k) AS (
TABLE e
UNION
SELECT x,y,b,l
FROM e JOIN r ON abs(x-p)+abs(y-q)=1 AND l=k
)
SELECT count(*) FROM (
SELECT bool_or(b) b
FROM r
GROUP BY p,q,k
)
WHERE NOT b
Любопытно, что такое же по сути своей решение предложил Никита Тихонович. Только он (как и я в плоской задаче) представил ландшафт в виде набора текстовых строк, по одной на каждый уровень. Но поскольку тут надо запихнуть в строку двумерный срез, Никите пришлось применить хитрое кодирование. А затем он реализовал процедуру вычисления клеток, соприкасающихся с краем, регулярными выражениями поверх этого закодированного представления! Получилось, увы, слишком длинно, но попытка зачетная. Приведу оригинальный запрос для тех, кто захочет разобраться — это интересно:
with recursive d as(select * from elevation3d),b as(select min(x)a,max(x)b,min(y)c,max(y)d from d),j as(select 'w'||repeat(',',k)||',b,'||s||',b'||repeat(',',k)s,k from(select z,string_agg(l,',b,'order by y)s,length(max(l))::int+2 k from(select z,y,string_agg(case when exists(select 1 from d where x=x.x and y=y.y and h>=h.z)then'0'else','end,''order by x)l from b,generate_series(0,10)h(z),generate_series(c,d)y(y),generate_series(a,b)x(x)group by h.z,y.y)g where z>0 group by z)f),r as(select s,k from j union all select regexp_replace(s,'w(.{'||k||'})?,|,(.{'||k||'})?w','w\1\2w','g'),k from r)cycle s set n using m select coalesce(sum(length(s)-length(replace(s,',',''))),0) from r where n
На этом у меня все, надеемся увидеть на олимпиаде-2026 и новые, и знакомые лица!