惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cyber Attacks, Cyber Crime and Cyber Security
A
Arctic Wolf
Help Net Security
Help Net Security
T
Threatpost
K
Kaspersky official blog
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
T
The Exploit Database - CXSecurity.com
Stack Overflow Blog
Stack Overflow Blog
大猫的无限游戏
大猫的无限游戏
J
Java Code Geeks
B
Blog
Latest news
Latest news
爱范儿
爱范儿
G
Google Developers Blog
P
Privacy International News Feed
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security
H
Help Net Security
aimingoo的专栏
aimingoo的专栏
T
Tenable Blog
S
Securelist
博客园 - 【当耐特】
MongoDB | Blog
MongoDB | Blog
Last Week in AI
Last Week in AI
美团技术团队
P
Proofpoint News Feed
Cisco Talos Blog
Cisco Talos Blog
Know Your Adversary
Know Your Adversary
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Cyberwarzone
Cyberwarzone
C
Cisco Blogs
F
Fortinet All Blogs
L
Lohrmann on Cybersecurity
AWS News Blog
AWS News Blog
P
Privacy & Cybersecurity Law Blog
M
MIT News - Artificial intelligence
G
GRAHAM CLULEY
Simon Willison's Weblog
Simon Willison's Weblog
The Cloudflare Blog
The Register - Security
The Register - Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
L
LINUX DO - 热门话题
V
Visual Studio Blog
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
«IT-Планета 2026»: задачи второго этапа по PostgreSQL
Евгений Давыдов · 2026-06-18 · via Все публикации подряд на Хабре

Продолжаем проводить конкурс SQL в рамках международной олимпиады «IT-Планета 2026». Как обычно, на втором этапе участникам было предложено решить 5 задач на чистом SQL. Давайте перейдём к рассмотрению задач. В этот раз я решил разобрать задачи на примере реальных вариантов, присланных конкурсантами. Может быть, где-то они покажутся избыточными, где-то — неоптимальными, но зато они рабочие и на их примере можно объяснить подход к решению. На момент проверки мы видим только обезличенные данные: я не знаю авторов решений, только их идентификаторы, поэтому оставим всё как есть, только идентификаторы участников.

Перейдём к задачам.

  1. Сломанная клавиатура

  2. Шлагбаум парковки

  3. Свободное место на парковке

  4. Турнирная таблица НХЛ

  5. Мобильная сеть

  6. Общие мысли

Задача 1: Сломанная клавиатура

Условие

Программист Василий любил пить кофе, когда работал за ноутбуком. Он нечаянно пролил напиток, и теперь у него на клавиатуре не работают клавиши b,c,k,l,q,v,x,z,|,*,<,&,$,%.

В информационной системе хранятся данные о работе агентов компании. Каждый месяц в таблицу M сохраняются начисления за месяц. Таблицы созданы следующими командами:

-- таблица «агенты»
create table a (id_a int primary key,             -- идентификатор агента
                n text not null,                  -- имя агента
                p numeric(3,1) default 0 not null -- комиссионные проценты
);

-- таблица «начисления за месяц»
create table m(id_m int primary key,                  -- идентификатор начисления
               id_a int not null references a (id_a), -- идентификатор агента
               s numeric(8,2) not null                -- начисления за месяц
);

Руководителю Василия Сергею Петровичу срочно понадобился отчёт по работе агентов за месяц с вычисленными итоговыми суммами с учётом комиссионных процентов за месяц. Отчет нужен очень срочно, другой клавиатуры у Василия нет. Помогите Василию написать запрос для получения данных, не используя клавиши, которые не работают.

В итоговый ответ должны попасть следующие поля:

  • Id_a — идентификатор агента;

  • Id_m — идентификатор начисления;

  • N — имя агента;

  • S — начисление за месяц;

  • P — комиссионные проценты;

  • T — итог начисления с учётом комиссионных процентов.

Расчёт итога:

T = S*(1+P/100)

В результат должны попасть все агенты, даже если им в этом месяце ничего не начислено.

Пример

Для следующих входных данных:

insert into a values (1,'Иванов',-5.5);
insert into a values (2,'Петрова',5);
insert into a values (3,'Кузнецов',0);
insert into a values (4,'Волкова',2);
insert into a values (5,'Зайцев',0);
insert into a values (6,'Воронова',3);
insert into a values (7,'Козлов',5);

insert into m values (1,1,10000);
insert into m values (2,2,10000);
insert into m values (3,3,6000);
insert into m values (4,5,5000);
insert into m values (5,6,2000);

Результат:

 id_a | id_m |    n     |    s     |  p   |    t     
------+------+----------+----------+------+----------
    1 |    1 | Иванов   | 10000.00 | -5.5 |  9450.00
    2 |    2 | Петрова  | 10000.00 |  5.0 | 10500.00
    3 |    3 | Кузнецов |  6000.00 |  0.0 |  6000.00
    5 |    4 | Зайцев   |  5000.00 |  0.0 |  5000.00
    6 |    5 | Воронова |  2000.00 |  3.0 |  2060.00
    7 |      | Козлов   |          |  5.0 |         
    4 |      | Волкова  |          |  2.0 |         
(7 rows)

Разбор решения

Формализуем правила:

  1. В запросе запрещено использовать часть символов.

  2. Запрещённые символы ограничены только списком из условия.

  3. Они не могут использоваться вне зависимости от регистра.

Решение

Эта задача задумывалась как простое задание для разминки, но половина участников с ней не справилась. Причина проста: раз запрос — значит оператор SELECT. Однако если посмотреть на список запрещённых символов, видно, что в нём присутствуют c и l. Значит, написать SELECT не получится, мы просто не сможем набрать это слово. Поэтому все решения с SELECT формально неправильны по критерию использования запрещённых символов.

Тут мы вспоминаем, что DML-операторы могут возвращать отношение через конструкцию RETURNING. Будем использовать UPDATE, обновляя данные на те же самые значения.

Ещё одна сложность была связана с вычисляемым полем: знак умножения * тоже запрещён. Пришлось вспомнить математику и придумать, чем заменить умножение, причём с учётом того, что проценты могут быть отрицательными.

Рассмотрим решение участника с идентификатором 16747. Поскольку оно невелико, приведём его сразу полностью:

UPDATE a SET p = a.p
FROM m RIGHT JOIN a AS a1 ON m.id_a = a1.id_a
WHERE a.id_a = a1.id_a
RETURNING 
	a.id_a, m.id_m, a.n, m.s, a.p, 
	ROUND((power(m.s + (1 + a.p/100.0), 2) - power(m.s, 2) - power(1 + a.p/100.0, 2)) / 2, 2) AS t;

Используется оператор UPDATE, в нём обновляется таблица a с агентами. Поскольку нужны данные по всем агентам, даже без начислений в таблице m, приходится применять внешнее соединение. Выбрано правое внешнее соединение, потому что левое (LEFT) и полное (FULL) запрещены из-за символа l. При этом таблицу a для соединения приходится использовать ещё раз.

Вычисление итогового результата возможно несколькими способами: заменой умножения делением или через разность квадратов. В данном примере применена разность квадратов, хотя большинство участников использовали замену умножения на деление. Однако это решение встречалось настолько часто, что возникало подозрение на использование ИИ.

Больше сказать по этой задаче нечего.

Задача 2: Шлагбаум парковки

Условие

В организации имеется парковка с ограниченным количеством мест. Для въезда и выезда используется один общий шлагбаум. Рядом с ним установлена камера с системой распознавания автомобильных номеров. Камера фиксирует как въезд, так и выезд автомобиля, сохраняя только распознанный номер и отметку времени. В журнале могут присутствовать повторяющиеся записи.

ИИ в установленной камере иногда ошибается, и тогда в номере, сохранённом в журнале, может присутствовать одна неправильная буква или цифра. Также в сохранённом номере может присутствовать одна нераспознанная цифра или буква, которая заменяется на символ «?».

Формат автомобильного номера: 000xxx, где:

  • 000 — последовательность из трёх цифр;

  • xxx — последовательность из трёх букв латинского алфавита.

Определения

Расстояние между двумя номерами — количество позиций, в которых они различаются. Корректный номер — номер, не содержащий символа «?». Ошибочный номер — номер, содержащий ровно один символ «?». Подозрительный номер — номер, который:

  • не содержит «?»;

  • встречается в журнале только один раз;

  • отличается от некоторого корректного номера не более чем в одной позиции.

Не каждый номер, встречающийся один раз, является подозрительным.

Необходимо:

  • Обработать журнал и заменить ошибочные и подозрительные номера на корректные в соответствии с приведёнными ниже правилами.

  • Порядок записей и их количество должны быть сохранены.

Правила замены

Обработка выполняется в порядке следования записей в соответствии с временнóй меткой.

Ошибочные номера

Пусть рассматривается запись с ошибочным номером.

  1. Рассматриваются корректные номера из предшествующих записей по временнóй метке.

  2. Из них выбираются номера, совпадающие с данным во всех позициях, кроме позиции с «?».

  3. Если такие номера существуют, выбирается номер:

    1. встречавшийся ранее наибольшее количество раз;

    2. если таких номеров несколько, то выбирается тот, который встречается раньше остальных.

  4. Если среди предыдущих записей подходящего номера нет, поиск выполняется по тем же критериям по всему журналу.

Подозрительные номера

Пусть рассматривается запись с подозрительным номером.

  1. Рассматриваются корректные номера из предшествующих записей по временнóй метке.

  2. Выбираются номера, расстояние до которых не превышает 1.

  3. Если такие номера существуют, выбирается номер:

    1. встречавшийся ранее наибольшее количество раз;

    2. если таких номеров несколько, то выбирается тот, который встречается раньше остальных.

  4. Если подходящего номера нет, запись остаётся без изменений.

В результате необходимо получить журнал, в котором все ошибочные и подозрительные номера заменены на соответствующие корректные номера. Повторяющиеся строки журнала должны сохраниться.

Для регистрации въездов и выездов в системе создана таблица:

create table gate (
   id  bigserial primary key,        -- Идентификатор записи журнала
   carnumber varchar(6) not null,    -- автомобильный номер
   ts  timestamp not null            -- временная метка
);

Пример

Для следующих входных данных:

insert into gate(carnumber,ts) values ('123abc','2026-03-01 08:00:00');
insert into gate(carnumber,ts) values ('123abc','2026-03-01 18:13:00');
insert into gate(carnumber,ts) values ('123abc','2026-03-02 08:05:00');
insert into gate(carnumber,ts) values ('123abb','2026-03-02 18:15:00');
insert into gate(carnumber,ts) values ('564kpa','2026-03-01 07:50:00');
insert into gate(carnumber,ts) values ('56?kpa','2026-03-01 13:05:00');
insert into gate(carnumber,ts) values ('565kpa','2026-03-01 13:50:00');
insert into gate(carnumber,ts) values ('565kpa','2026-03-01 17:50:00');
insert into gate(carnumber,ts) values ('845cko','2026-03-01 10:01:00');
insert into gate(carnumber,ts) values ('845cko','2026-03-01 17:16:00');
insert into gate(carnumber,ts) values ('745cko','2026-03-02 13:01:00');
insert into gate(carnumber,ts) values ('845c?o','2026-03-02 17:55:00');

Результат:

 id | carnumber |         ts          
----+-----------+---------------------
  1 | 123abc    | 2026-03-01 08:00:00
  2 | 123abc    | 2026-03-01 18:13:00
  3 | 123abc    | 2026-03-02 08:05:00
  4 | 123abc    | 2026-03-02 18:15:00
  5 | 564kpa    | 2026-03-01 07:50:00
  6 | 564kpa    | 2026-03-01 13:05:00
  7 | 565kpa    | 2026-03-01 13:50:00
  8 | 565kpa    | 2026-03-01 17:50:00
  9 | 845cko    | 2026-03-01 10:01:00
 10 | 845cko    | 2026-03-01 17:16:00
 11 | 845cko    | 2026-03-02 13:01:00
 12 | 845cko    | 2026-03-02 17:55:00
(12 rows)

Разбор решения

Рассмотрим некоторые замечания к формулировке, которые появились после обсуждений.

Все поиски — только по исходному журналу

Самое коварное место — фраза «из предшествующих записей». Возникает естественное желание вести накопленный журнал: обработали запись, заменили номер и для следующей записи ищем кандидатов уже среди исправленных. Но тестовый пример показывает, что это не так.

Кандидаты для замены всегда ищутся в исходной таблице, а не в промежуточном результате. Замены не влияют друг на друга. Статусы «ошибочный» и «подозрительный» тоже определяются один раз по исходным данным до начала обработки и не пересчитываются динамически.

Подозрительный номер определяется только по предыдущим записям

Ошибочный номер при отсутствии кандидатов среди предыдущих записей может быть исправлен по всему журналу, в правилах это сказано явно. Для подозрительного номера такой возможности нет: если среди предыдущих записей подходящего корректного номера не нашлось, запись остаётся без изменений, даже если позже в журнале такой номер присутствует.

Пример из тестовых данных: 564kpa (id=5) формально подозрительный: встречается один раз, расстояние до 565kpa равно единице. Но это самая первая запись по времени, предыдущих у неё нет. Поэтому замены не происходит, и номер остаётся 564kpa.

Корректный номер может быть одновременно и подозрительным

Определение корректного номера требует ровно одного — отсутствия в номере символа «?». Оно ничего не говорит о частоте встречаемости или расстоянии до других номеров. Поэтому номер может быть одновременно и корректным, и подозрительным — эти статусы не исключают друг друга.

В чём это проявляется? Кандидата для замены ошибочного номера следует искать среди корректных, а не среди «не подозрительных». Поэтому 564kpa (id=5), будучи корректным по формальному признаку, выступает кандидатом для ошибочного 56?kpa (id=6), и этот кандидат выигрывает, так как он единственный подходит под шаблон. То, что 564kpa сам является подозрительным, никак не ограничивает его право быть кандидатом.

Частота и порядок появления считаются по предшествующим записям

Фраза «встречавшийся ранее наибольшее количество раз» означает «среди записей, предшествующих текущей по временнóй метке». Считается не общее количество вхождений номера в журнал, а только те, что расположены до обрабатываемой строки. Аналогично и с критерием «встречается раньше остальных»: сравниваются первые появления среди предшествующих записей, а не по всему журналу.

Дубликаты сохраняются

В условии явно сказано: «в журнале могут присутствовать повторяющиеся записи» и «порядок записей и их количество должны быть сохранены». Если запись не подлежит замене, она переносится в результат как есть. Никакого удаления дубликатов или группировки.

Перейдём к рассмотрению решения, предложенного конкурсантом с идентификатором 24838.

  1. Запрос orig_unique — убираем дубли в журнале.

        SELECT carnumber, ts
          FROM gate
         GROUP BY carnumber, ts
    Результат выполнения
     carnumber |         ts          
    -----------+---------------------
     565kpa    | 2026-03-01 17:50:00
     745cko    | 2026-03-02 13:01:00
     56?kpa    | 2026-03-01 13:05:00
     123abb    | 2026-03-02 18:15:00
     845cko    | 2026-03-01 17:16:00
     123abc    | 2026-03-01 08:00:00
     565kpa    | 2026-03-01 13:50:00
     123abc    | 2026-03-02 08:05:00
     845cko    | 2026-03-01 10:01:00
     123abc    | 2026-03-01 18:13:00
     564kpa    | 2026-03-01 07:50:00
     845c?o    | 2026-03-02 17:55:00
    (12 rows)
  2. Оставляем в orig_correct только корректные номера: как отмечалось ранее, это номера где нет символа «?».

        SELECT carnumber, ts
          FROM orig_unique
         WHERE POSITION('?' IN carnumber) = 0
    Результат выполнения
     carnumber |         ts          
    -----------+---------------------
     123abb    | 2026-03-02 18:15:00
     123abc    | 2026-03-01 08:00:00
     123abc    | 2026-03-01 18:13:00
     123abc    | 2026-03-02 08:05:00
     564kpa    | 2026-03-01 07:50:00
     565kpa    | 2026-03-01 13:50:00
     565kpa    | 2026-03-01 17:50:00
     745cko    | 2026-03-02 13:01:00
     845cko    | 2026-03-01 10:01:00
     845cko    | 2026-03-01 17:16:00
    (10 rows)
  3. Запрос orig_stats статистика по исходному журналу. Вычислим частоту встречаемости номера и минимальное значение отметки времени.

    SELECT carnumber, 
           COUNT(*) AS freq, 
           MIN(ts) AS first_ts
      FROM orig_correct
     GROUP BY carnumber
    Результат выполнения
     carnumber | freq |      first_ts       
    -----------+------+---------------------
     123abb    |    1 | 2026-03-02 18:15:00
     123abc    |    3 | 2026-03-01 08:00:00
     564kpa    |    1 | 2026-03-01 07:50:00
     565kpa    |    2 | 2026-03-01 13:50:00
     745cko    |    1 | 2026-03-02 13:01:00
     845cko    |    2 | 2026-03-01 10:01:00
    (6 rows)
  4. Запрос suspicious — список подозрительных номеров. Оставляем только те номера, которые удовлетворяют двум условиям: встречаются в исходном журнале ровно один раз, и существует хотя бы один корректный номер на расстоянии не более 1.

    SELECT o1.carnumber
      FROM orig_stats o1
     WHERE o1.freq = 1
       AND EXISTS (
              SELECT 1 FROM orig_stats o2
               WHERE o2.carnumber != o1.carnumber
                 AND (
                  (CASE WHEN SUBSTR(o1.carnumber,1,1) = SUBSTR(o2.carnumber,1,1) THEN 0 ELSE 1 END) +
                  (CASE WHEN SUBSTR(o1.carnumber,2,1) = SUBSTR(o2.carnumber,2,1) THEN 0 ELSE 1 END) +
                  (CASE WHEN SUBSTR(o1.carnumber,3,1) = SUBSTR(o2.carnumber,3,1) THEN 0 ELSE 1 END) +
                  (CASE WHEN SUBSTR(o1.carnumber,4,1) = SUBSTR(o2.carnumber,4,1) THEN 0 ELSE 1 END) +
                  (CASE WHEN SUBSTR(o1.carnumber,5,1) = SUBSTR(o2.carnumber,5,1) THEN 0 ELSE 1 END) +
                  (CASE WHEN SUBSTR(o1.carnumber,6,1) = SUBSTR(o2.carnumber,6,1) THEN 0 ELSE 1 END)
                ) <= 1
          )
    Результат выполнения
     carnumber 
    -----------
     123abb
     564kpa
     745cko
    (3 rows)
  5. В ordered_gate получаем записи журнала, пронумерованные в нужном порядке, причём берём их из исходной таблицы, чтобы присутствовали дубли строк

    SELECT id, carnumber, ts,
           POSITION('?' IN carnumber) > 0 AS is_erroneous,
           EXISTS(SELECT 1 FROM suspicious s WHERE s.carnumber = gate.carnumber) AS is_suspicious,
           ROW_NUMBER() OVER (ORDER BY ts, id) AS rn
      FROM gate
    Результат выполнения
     id | carnumber |         ts          | is_erroneous | is_suspicious | rn 
    ----+-----------+---------------------+--------------+---------------+----
      5 | 564kpa    | 2026-03-01 07:50:00 | f            | t             |  1
      1 | 123abc    | 2026-03-01 08:00:00 | f            | f             |  2
      9 | 845cko    | 2026-03-01 10:01:00 | f            | f             |  3
      6 | 56?kpa    | 2026-03-01 13:05:00 | t            | f             |  4
      7 | 565kpa    | 2026-03-01 13:50:00 | f            | f             |  5
     10 | 845cko    | 2026-03-01 17:16:00 | f            | f             |  6
      8 | 565kpa    | 2026-03-01 17:50:00 | f            | f             |  7
      2 | 123abc    | 2026-03-01 18:13:00 | f            | f             |  8
      3 | 123abc    | 2026-03-02 08:05:00 | f            | f             |  9
     11 | 745cko    | 2026-03-02 13:01:00 | f            | t             | 10
     12 | 845c?o    | 2026-03-02 17:55:00 | t            | f             | 11
      4 | 123abb    | 2026-03-02 18:15:00 | f            | t             | 12
    (12 rows)
  6. Основная логика замены — запрос rec. Рекурсивно проходим по журналу и заменяем подозрительные и ошибочные номера.

    rec AS (
        SELECT 
            0::BIGINT AS id, 
            0::BIGINT AS rn, 
            ''::TEXT AS carnumber, 
            '1970-01-01'::TIMESTAMP AS ts,
            ARRAY[]::TEXT[] AS hist_cars,
            ARRAY[]::TIMESTAMP[] AS hist_ts
        UNION ALL
        SELECT 
            g.id, g.rn, calc.new_carnumber, g.ts,
            prev.hist_cars || calc.new_carnumber,
            prev.hist_ts || g.ts
        FROM rec prev
        JOIN ordered_gate g ON g.rn = prev.rn + 1
        CROSS JOIN LATERAL (
            SELECT COALESCE(
                CASE 
                    WHEN g.is_erroneous THEN 
                        COALESCE(
                            (SELECT h_car FROM (
                                SELECT u.h_car, COUNT(*) as f, MIN(u.h_ts) as t
                                FROM UNNEST(prev.hist_cars, prev.hist_ts) AS u(h_car, h_ts)
                                WHERE u.h_car LIKE REPLACE(g.carnumber, '?', '_')
                                GROUP BY u.h_car
                             ) sub ORDER BY f DESC, t ASC LIMIT 1),
                            (SELECT os.carnumber FROM orig_stats os 
                             WHERE os.carnumber LIKE REPLACE(g.carnumber, '?', '_')
                             ORDER BY os.freq DESC, os.first_ts ASC LIMIT 1)
                        )
                    WHEN g.is_suspicious THEN
                        (SELECT h_car FROM (
                            SELECT u.h_car, COUNT(*) as f, MIN(u.h_ts) as t
                            FROM UNNEST(prev.hist_cars, prev.hist_ts) AS u(h_car, h_ts)
                            WHERE (
                                (CASE WHEN SUBSTR(g.carnumber,1,1) = SUBSTR(u.h_car,1,1) THEN 0 ELSE 1 END) +
                                (CASE WHEN SUBSTR(g.carnumber,2,1) = SUBSTR(u.h_car,2,1) THEN 0 ELSE 1 END) +
                                (CASE WHEN SUBSTR(g.carnumber,3,1) = SUBSTR(u.h_car,3,1) THEN 0 ELSE 1 END) +
                                (CASE WHEN SUBSTR(g.carnumber,4,1) = SUBSTR(u.h_car,4,1) THEN 0 ELSE 1 END) +
                                (CASE WHEN SUBSTR(g.carnumber,5,1) = SUBSTR(u.h_car,5,1) THEN 0 ELSE 1 END) +
                                (CASE WHEN SUBSTR(g.carnumber,6,1) = SUBSTR(u.h_car,6,1) THEN 0 ELSE 1 END)
                            ) <= 1
                            GROUP BY u.h_car
                         ) sub ORDER BY f DESC, t ASC LIMIT 1)
                    ELSE g.carnumber
                END,
                g.carnumber
            ) AS new_carnumber
        ) calc
    )
    Результат выполнения
     id | rn | carnumber |         ts          |                                       hist_cars                                       |                                                                                                                                  hist_ts                                                                                                                                  
    ----+----+-----------+---------------------+---------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
      0 |  0 |           | 1970-01-01 00:00:00 | {}                                                                                    | {}
      5 |  1 | 564kpa    | 2026-03-01 07:50:00 | {564kpa}                                                                              | {"2026-03-01 07:50:00"}
      1 |  2 | 123abc    | 2026-03-01 08:00:00 | {564kpa,123abc}                                                                       | {"2026-03-01 07:50:00","2026-03-01 08:00:00"}
      9 |  3 | 845cko    | 2026-03-01 10:01:00 | {564kpa,123abc,845cko}                                                                | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00"}
      6 |  4 | 564kpa    | 2026-03-01 13:05:00 | {564kpa,123abc,845cko,564kpa}                                                         | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00","2026-03-01 13:05:00"}
      7 |  5 | 565kpa    | 2026-03-01 13:50:00 | {564kpa,123abc,845cko,564kpa,565kpa}                                                  | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00","2026-03-01 13:05:00","2026-03-01 13:50:00"}
     10 |  6 | 845cko    | 2026-03-01 17:16:00 | {564kpa,123abc,845cko,564kpa,565kpa,845cko}                                           | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00","2026-03-01 13:05:00","2026-03-01 13:50:00","2026-03-01 17:16:00"}
      8 |  7 | 565kpa    | 2026-03-01 17:50:00 | {564kpa,123abc,845cko,564kpa,565kpa,845cko,565kpa}                                    | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00","2026-03-01 13:05:00","2026-03-01 13:50:00","2026-03-01 17:16:00","2026-03-01 17:50:00"}
      2 |  8 | 123abc    | 2026-03-01 18:13:00 | {564kpa,123abc,845cko,564kpa,565kpa,845cko,565kpa,123abc}                             | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00","2026-03-01 13:05:00","2026-03-01 13:50:00","2026-03-01 17:16:00","2026-03-01 17:50:00","2026-03-01 18:13:00"}
      3 |  9 | 123abc    | 2026-03-02 08:05:00 | {564kpa,123abc,845cko,564kpa,565kpa,845cko,565kpa,123abc,123abc}                      | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00","2026-03-01 13:05:00","2026-03-01 13:50:00","2026-03-01 17:16:00","2026-03-01 17:50:00","2026-03-01 18:13:00","2026-03-02 08:05:00"}
     11 | 10 | 845cko    | 2026-03-02 13:01:00 | {564kpa,123abc,845cko,564kpa,565kpa,845cko,565kpa,123abc,123abc,845cko}               | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00","2026-03-01 13:05:00","2026-03-01 13:50:00","2026-03-01 17:16:00","2026-03-01 17:50:00","2026-03-01 18:13:00","2026-03-02 08:05:00","2026-03-02 13:01:00"}
     12 | 11 | 845cko    | 2026-03-02 17:55:00 | {564kpa,123abc,845cko,564kpa,565kpa,845cko,565kpa,123abc,123abc,845cko,845cko}        | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00","2026-03-01 13:05:00","2026-03-01 13:50:00","2026-03-01 17:16:00","2026-03-01 17:50:00","2026-03-01 18:13:00","2026-03-02 08:05:00","2026-03-02 13:01:00","2026-03-02 17:55:00"}
      4 | 12 | 123abc    | 2026-03-02 18:15:00 | {564kpa,123abc,845cko,564kpa,565kpa,845cko,565kpa,123abc,123abc,845cko,845cko,123abc} | {"2026-03-01 07:50:00","2026-03-01 08:00:00","2026-03-01 10:01:00","2026-03-01 13:05:00","2026-03-01 13:50:00","2026-03-01 17:16:00","2026-03-01 17:50:00","2026-03-01 18:13:00","2026-03-02 08:05:00","2026-03-02 13:01:00","2026-03-02 17:55:00","2026-03-02 18:15:00"}
    (13 rows)
  7. Осталось получить итог. Уберём фиктивную строку с rn=0 и оставим только значимые строки журнала.

    SELECT id, carnumber, ts 
      FROM rec 
     WHERE rn > 0 
     ORDER BY id
Решение полностью
WITH RECURSIVE
orig_unique AS (
    SELECT carnumber, ts
    FROM gate
    GROUP BY carnumber, ts
),
orig_correct AS (
    SELECT carnumber, ts
    FROM orig_unique
    WHERE POSITION('?' IN carnumber) = 0
)
,
orig_stats AS (
    SELECT carnumber, COUNT(*) AS freq, MIN(ts) AS first_ts
    FROM orig_correct
    GROUP BY carnumber
)
,
suspicious AS (
    SELECT o1.carnumber
    FROM orig_stats o1
    WHERE o1.freq = 1
      AND EXISTS (
          SELECT 1 FROM orig_stats o2
          WHERE o2.carnumber != o1.carnumber
            AND (
              (CASE WHEN SUBSTR(o1.carnumber,1,1) = SUBSTR(o2.carnumber,1,1) THEN 0 ELSE 1 END) +
              (CASE WHEN SUBSTR(o1.carnumber,2,1) = SUBSTR(o2.carnumber,2,1) THEN 0 ELSE 1 END) +
              (CASE WHEN SUBSTR(o1.carnumber,3,1) = SUBSTR(o2.carnumber,3,1) THEN 0 ELSE 1 END) +
              (CASE WHEN SUBSTR(o1.carnumber,4,1) = SUBSTR(o2.carnumber,4,1) THEN 0 ELSE 1 END) +
              (CASE WHEN SUBSTR(o1.carnumber,5,1) = SUBSTR(o2.carnumber,5,1) THEN 0 ELSE 1 END) +
              (CASE WHEN SUBSTR(o1.carnumber,6,1) = SUBSTR(o2.carnumber,6,1) THEN 0 ELSE 1 END)
            ) <= 1
      )
)
,
ordered_gate AS (
    SELECT id, carnumber, ts,
           POSITION('?' IN carnumber) > 0 AS is_erroneous,
           EXISTS(SELECT 1 FROM suspicious s WHERE s.carnumber = gate.carnumber) AS is_suspicious,
           ROW_NUMBER() OVER (ORDER BY ts, id) AS rn
    FROM gate
)
,
rec AS (
    SELECT 
        0::BIGINT AS id, 
        0::BIGINT AS rn, 
        ''::TEXT AS carnumber, 
        '1970-01-01'::TIMESTAMP AS ts,
        ARRAY[]::TEXT[] AS hist_cars,
        ARRAY[]::TIMESTAMP[] AS hist_ts
    UNION ALL
    SELECT 
        g.id, g.rn, calc.new_carnumber, g.ts,
        prev.hist_cars || calc.new_carnumber,
        prev.hist_ts || g.ts
    FROM rec prev
    JOIN ordered_gate g ON g.rn = prev.rn + 1
    CROSS JOIN LATERAL (
        SELECT COALESCE(
            CASE 
                WHEN g.is_erroneous THEN 
                    COALESCE(
                        (SELECT h_car FROM (
                            SELECT u.h_car, COUNT(*) as f, MIN(u.h_ts) as t
                            FROM UNNEST(prev.hist_cars, prev.hist_ts) AS u(h_car, h_ts)
                            WHERE u.h_car LIKE REPLACE(g.carnumber, '?', '_')
                            GROUP BY u.h_car
                         ) sub ORDER BY f DESC, t ASC LIMIT 1),
                        (SELECT os.carnumber FROM orig_stats os 
                         WHERE os.carnumber LIKE REPLACE(g.carnumber, '?', '_')
                         ORDER BY os.freq DESC, os.first_ts ASC LIMIT 1)
                    )
                WHEN g.is_suspicious THEN
                    (SELECT h_car FROM (
                        SELECT u.h_car, COUNT(*) as f, MIN(u.h_ts) as t
                        FROM UNNEST(prev.hist_cars, prev.hist_ts) AS u(h_car, h_ts)
                        WHERE (
                            (CASE WHEN SUBSTR(g.carnumber,1,1) = SUBSTR(u.h_car,1,1) THEN 0 ELSE 1 END) +
                            (CASE WHEN SUBSTR(g.carnumber,2,1) = SUBSTR(u.h_car,2,1) THEN 0 ELSE 1 END) +
                            (CASE WHEN SUBSTR(g.carnumber,3,1) = SUBSTR(u.h_car,3,1) THEN 0 ELSE 1 END) +
                            (CASE WHEN SUBSTR(g.carnumber,4,1) = SUBSTR(u.h_car,4,1) THEN 0 ELSE 1 END) +
                            (CASE WHEN SUBSTR(g.carnumber,5,1) = SUBSTR(u.h_car,5,1) THEN 0 ELSE 1 END) +
                            (CASE WHEN SUBSTR(g.carnumber,6,1) = SUBSTR(u.h_car,6,1) THEN 0 ELSE 1 END)
                        ) <= 1
                        GROUP BY u.h_car
                     ) sub ORDER BY f DESC, t ASC LIMIT 1)
                ELSE g.carnumber
            END,
            g.carnumber
        ) AS new_carnumber
    ) calc
)
SELECT id, carnumber, ts 
FROM rec 
WHERE rn > 0 
ORDER BY id;
Результат вывода запроса:
 id | carnumber |         ts          
----+-----------+---------------------
  1 | 123abc    | 2026-03-01 08:00:00
  2 | 123abc    | 2026-03-01 18:13:00
  3 | 123abc    | 2026-03-02 08:05:00
  4 | 123abc    | 2026-03-02 18:15:00
  5 | 564kpa    | 2026-03-01 07:50:00
  6 | 564kpa    | 2026-03-01 13:05:00
  7 | 565kpa    | 2026-03-01 13:50:00
  8 | 565kpa    | 2026-03-01 17:50:00
  9 | 845cko    | 2026-03-01 10:01:00
 10 | 845cko    | 2026-03-01 17:16:00
 11 | 845cko    | 2026-03-02 13:01:00
 12 | 845cko    | 2026-03-02 17:55:00
(12 rows)

Задача 3: Свободное место на парковке

Условие

У организации имеется парковка с ограниченным количеством мест. Для въезда и выезда используется один общий шлагбаум. Рядом с ним установлена камера с системой распознавания автомобильных номеров. Камера фиксирует как въезд, так и выезд автомобиля, сохраняя только распознанный номер и отметку времени. В журнале могут присутствовать повторяющиеся записи.

В предыдущей задаче вы очистили журнал шлагбаума парковки. То же самое проделал и программист Василий. Теперь у него тоже есть запрос, получающий исправленный журнал.

Программист Василий едет на в офис на своём автомобиле и, подъезжая к парковке, хочет узнать, есть ли на ней свободные места. Количество мест на парковке, а также дата и время, когда Василий едет на работу, задаются параметрами.

Вам необходимо помочь Василию и сделать запрос, который бы выводил количество автомобилей на парковке в момент запроса, а также количество свободных мест или строку «нет». Если количество машин на парковке превышает количество парковочных мест, запрос должен вывести «ошибка» в поле «Свободно мест».

Для регистрации въездов и выездов в системе создана таблица:

create table gate (
   id        bigserial primary key,      -- идентификатор записи журнала
   carnumber varchar(6) not null,        -- автомобильный номер
   ts        timestamp not null          -- временная метка
);

Для задания количества мест на парковке, а также даты и времени запроса используются параметры:

gate.number   – количество мест на парковке
gate.ts       – дата и время запроса

Пример

Для следующих входных данных:

insert into gate(carnumber,ts) values ('123abc','2026-03-01 08:00:00');
insert into gate(carnumber,ts) values ('123abc','2026-03-01 18:13:00');
insert into gate(carnumber,ts) values ('123abc','2026-03-02 08:05:00');
insert into gate(carnumber,ts) values ('123abb','2026-03-02 18:15:00');
insert into gate(carnumber,ts) values ('564kpa','2026-03-01 07:50:00');
insert into gate(carnumber,ts) values ('56?kpa','2026-03-01 13:05:00');
insert into gate(carnumber,ts) values ('565kpa','2026-03-01 13:50:00');
insert into gate(carnumber,ts) values ('565kpa','2026-03-01 17:50:00');
insert into gate(carnumber,ts) values ('845cko','2026-03-01 10:01:00');
insert into gate(carnumber,ts) values ('845cko','2026-03-01 17:16:00');
insert into gate(carnumber,ts) values ('745cko','2026-03-02 13:01:00');
insert into gate(carnumber,ts) values ('845c?o','2026-03-02 17:55:00');

и параметров:

set gate.number=3;
set gate.ts='2026-03-02 12:05:00';

Результат:

 На стоянке авто | Свободно мест 
-----------------+---------------
               1 | 2
(1 row)

Разбор решения

Задача — прямое продолжение предыдущей. По сути, это была одна задача, которую разделили на две логические части. Поэтому в первой части требовалось сохранить дубликаты строк в журнале, заменив только подозрительные и ошибочные номера. Теперь же, имея исправленный журнал, нужно ответить на вопрос: сколько машин находится на парковке в конкретный момент времени?

От журнала к интервалам

В исходном журнале камера не фиксирует тип события, въезд это или выезд. Есть только номер и временнáя метка. Логика определения статуса машины держится на простом правиле:

  • первое появление номера — машина заехала на парковку;

  • второе — выехала.

Весь временной интервал между этими двумя событиями машина находится на парковке.

Однако в журнале могут присутствовать дубликаты — строки с одинаковым номером и одинаковой временной меткой. Если их оставить, правило ломается. Представьте: машина заехала, а камера записала это дважды подряд. Формально получаются две строки с одним временем — «въезд и сразу выезд», что физически невозможно.

Поэтому перед подсчётом дубликаты необходимо убрать. В отличие от предыдущей задачи, где мы сохраняли их как часть исходных данных, здесь они мешают корректно определить состояние парковки. Убираем строки, у которых (carnumber, ts) совпадает с предыдущей записью для того же номера.

Построение интервалов

Логику «нечётная запись — въезд, чётная — выезд» удобно оформить путём построения интервалов пребывания каждой машины на парковке. Это нагляднее, чем манипуляции с чётностью, и лучше соответствует семантике задачи. Хотя, надо заметить, многие участники определяли, находится машина на парковке или нет, как раз через чётность/нечётность порядкового номера временнóй метки. И такой подход тоже рабочий.

После удаления дубликатов записи каждого номера идут в хронологическом порядке и гарантированно чередуются: въезд, выезд, въезд, выезд… Нумеруем их по порядку и формируем пары:

  • нечётная запись (1, 3, 5…) — начало интервала;

  • следующая за ней чётная (2, 4, 6…) — конец интервала.

Если у номера нечётное количество записей, последний интервал остаётся открытым: машина заехала, но ещё не выехала.

Дальше всё просто: остаётся только найти все интервалы, пересекающиеся с заданной временной меткой, и посчитать, для скольких машин такое пересечение есть. Это и будет количество автомобилей на парковке.

Крайние случаи

Нужно не забыть про граничные ситуации. Если временнáя метка находится раньше всех записей в журнале или позже всех, парковка пуста, машин нет.

Также количество автомобилей на парковке может превышать количество мест, в условии задачи это оговорено явно. Такое возможно, если из-за ошибок распознавания разные машины с похожими номерами были исправлены на один и тот же корректный номер, и теперь их интервалы накладываются друг на друга. В этом случае в поле «Свободно мест» нужно вывести «Ошибка».

Разбор решения участника

Перейдём к рассмотрению решения, представленного конкурсантом с идентификатором 45533. Первую часть запроса, отвечающую за очистку журнала от ошибочных и подозрительных номеров, рассматривать не будем, она разобрана в решении предыдущей задачи. Начнём с части, где выполняется дедупликация и дальнейшее построение интервалов.

  1. Получим исходные данные из переменных для использования в запросах. Именно в этом месте было много ошибок у конкурсантов, почему-то с такой простой задачей они не справились. При получении сразу делаем проверку на корректность входных данных.

    select case
       when current_setting('gate.number', true) ~ '^\d+$'
       then current_setting('gate.number')::int
     end max_slot,
     case
      when current_setting('gate.ts', true) ~ '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
      then current_setting('gate.ts', true)::timestamp end target_ts
    Результат
     max_slot |      target_ts      
    ----------+---------------------
            3 | 2026-03-02 12:05:00
    (1 row)
  2. Запрос flagged_data маркирование въездов и выездов для данных без дубликатов

    select carnumber, ts, 
    case row_number() over (partition by carnumber order by ts) % 2 
        when 1 then 1 else -1 end io_flag 
    from result_2nd
    Результат
     carnumber |         ts          | io_flag 
    -----------+---------------------+---------
     123abc    | 2026-03-01 08:00:00 |       1
     123abc    | 2026-03-01 18:13:00 |      -1
     123abc    | 2026-03-02 08:05:00 |       1
     123abc    | 2026-03-02 18:15:00 |      -1
     564kpa    | 2026-03-01 07:50:00 |       1
     564kpa    | 2026-03-01 13:05:00 |      -1
     565kpa    | 2026-03-01 13:50:00 |       1
     565kpa    | 2026-03-01 17:50:00 |      -1
     845cko    | 2026-03-01 10:01:00 |       1
     845cko    | 2026-03-01 17:16:00 |      -1
     845cko    | 2026-03-02 13:01:00 |       1
     845cko    | 2026-03-02 17:55:00 |      -1
    (12 rows)
  3. В parking_timeline формируем интервалы занятости мест парковки и определяем количество оставшихся мест.

    select carnumber, 
        tsrange(ts, lead(ts) over ts_order, '[)') tsrange, 
        max_slot - sum(io_flag) over ts_order remain
    from flagged_data
    join prepare_input on true
    window ts_order as (order by ts)
    Результат
     carnumber |                    tsrange                    | remain 
    -----------+-----------------------------------------------+--------
     564kpa    | ["2026-03-01 07:50:00","2026-03-01 08:00:00") |      2
     123abc    | ["2026-03-01 08:00:00","2026-03-01 10:01:00") |      1
     845cko    | ["2026-03-01 10:01:00","2026-03-01 13:05:00") |      0
     564kpa    | ["2026-03-01 13:05:00","2026-03-01 13:50:00") |      1
     565kpa    | ["2026-03-01 13:50:00","2026-03-01 17:16:00") |      0
     845cko    | ["2026-03-01 17:16:00","2026-03-01 17:50:00") |      1
     565kpa    | ["2026-03-01 17:50:00","2026-03-01 18:13:00") |      2
     123abc    | ["2026-03-01 18:13:00","2026-03-02 08:05:00") |      3
     123abc    | ["2026-03-02 08:05:00","2026-03-02 13:01:00") |      2
     845cko    | ["2026-03-02 13:01:00","2026-03-02 17:55:00") |      1
     845cko    | ["2026-03-02 17:55:00","2026-03-02 18:15:00") |      2
     123abc    | ["2026-03-02 18:15:00",)                      |      3
    (12 rows)
  4. Формируем окончательный результат с обработкой крайних случаев и ошибочных ситуаций. Проверяем пересечение временнóй метки из параметров с построенными интервалами.

    select
    max_slot - coalesce(remain, max_slot) "На стоянке авто", 
    case 
        when coalesce(remain, max_slot) < 0 then 'ошибка'
        when coalesce(remain, max_slot) = 0 then 'нет'
        else coalesce(remain, max_slot)::text end "Свободно мест" 
    from prepare_input
    left join parking_timeline on tsrange @> target_ts
Решение полностью
with remove_duplicate as (
select distinct carnumber, ts
from gate 
)
,input_data as (
select t1.carnumber, t1.ts, 
case when strpos(t1.carnumber, '?') > 0
          then 1          
     when count(t1.carnumber) over (partition by t1.carnumber) = 1 
          and exists(select 1 
                     from remove_duplicate t2
                     where 
                     strpos(t2.carnumber, '?') <= 0
                     and (select sum((substring(t1.carnumber, i, 1) != substring(t2.carnumber, i, 1))::int)
                            from generate_series(1, 6) as g(i)) = 1
                     and t1.ts > t2.ts 
                    )
          then 2
     else 0 end datatype
from remove_duplicate t1
)
,processed_data as (
select t1.carnumber, t1.ts, t1.datatype, t2.carnumber candidate, 
t2.ts - t1.ts delta,
count(t2.carnumber) 
    filter (where t2.ts - t1.ts < interval '0')
    over (
        partition by t1.carnumber, t1.ts, t2.carnumber 
    ) prev_cnt,
count(t2.carnumber) 
    filter (where t2.ts - t1.ts > interval '0')
    over (
        partition by t1.carnumber, t1.ts, t2.carnumber 
    ) next_cnt 
from input_data t1
left join input_data t2 
    on t1.carnumber != t2.carnumber 
    and case when t1.datatype = 1 then t2.carnumber LIKE replace(t1.carnumber, '?', '_')
             when t1.datatype = 2 then (select sum((substring(t1.carnumber, i, 1) <> substring(t2.carnumber, i, 1))::int) 
                                        from generate_series(1, 6) as g(i)) = 1 end 
where t1.datatype != 0 and t2.datatype = 0
)
,result_2nd as (
select distinct 
coalesce (first_value(candidate) over (
        partition by carnumber, ts 
        order by 
            prev_cnt desc, 
            case when delta < interval '0' then delta else null end,
            next_cnt desc,  
            case when delta > interval '0' then delta else null end), carnumber) carnumber,
ts
from processed_data 
right join remove_duplicate using(carnumber, ts)
)
,prepare_input as (
select case
           when current_setting('gate.number', true) ~ '^\d+$'
           then current_setting('gate.number')::int
       end max_slot,
       case
         when current_setting('gate.ts', true) ~
              '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
         then current_setting('gate.ts', true)::timestamp end target_ts
)
,flagged_data  as (
select carnumber, ts, 
case row_number() over (partition by carnumber order by ts) % 2 
    when 1 then 1 else -1 end io_flag 
from result_2nd
)
,parking_timeline as(
select carnumber, 
    tsrange(ts, lead(ts) over ts_order, '[)') tsrange, 
    max_slot - sum(io_flag) over ts_order remain
from flagged_data
join prepare_input on true
window ts_order as (order by ts)
)
select
max_slot - coalesce(remain, max_slot) "На стоянке авто", 
case 
    when coalesce(remain, max_slot) < 0 then 'ошибка'
    when coalesce(remain, max_slot) = 0 then 'нет'
    else coalesce(remain, max_slot)::text end "Свободно мест" 
from prepare_input
left join parking_timeline on tsrange @> target_ts;
Результат запроса:
 На стоянке авто | Свободно мест 
-----------------+---------------
               1 | 2
(1 row)

Задача 4: Турнирная таблица НХЛ

Условие

Незнайка и Гунька основали Настольную Хоккейную Лигу (НХЛ). Для детального учёта результатов Знайка создал таблицу scores, в которую заносится счёт отдельно за каждый этап матча: основное время, овертайм и серию послематчевых бросков (буллитов).

Таблица создана следующей командой:

create table scores
( idscore serial primary key,-- идентификатор матча
  team1 text not null,       -- название первой команды
  team2  text not null,      -- название второй команды
  g1 int not null,           -- количество голов, забитых первой командой в основное время
  g2 int not null,           -- количество голов, забитых второй командой в основное время
  ot1 int,                   -- количество голов, забитых первой командой в овертайме
  ot2 int,                   -- количество голов, забитых второй командой в овертайме
  so1 int,                   -- количество буллитов, забитых первой командой
  so2 int                    -- количество буллитов, забитых второй командой
);

Коротышки аккуратно вели таблицу и заносили в неё результаты всех матчей. В таблицу заносились данные о количестве голов на каждом этапе игры.

Структура матча и фиксация результата

Матч состоит из трёх периодов (основное время).

Если по итогам основного времени зафиксирована ничья, то назначается овертайм, ограниченный по времени, который продолжается до первого гола («золотой гол») или окончания времени.

Если овертайм не выявил победителя, проводится серия послематчевых бросков (буллитов). Серия длится до первого гола при равном количестве бросков.

Финальный счёт матча формируется так:

  • Голы, забитые в основное время и овертайме, суммируются.

  • Победа в серии буллитов добавляет 1 гол к общему счёту победившей команды. Этот гол учитывается только в финальном результате и не относится к этапам игры.

Система начисления очков

На основании результата матча каждой команде начисляются очки, сумма которых будет основным критерием при построении рейтинга команд.

Очки за матч определяются по следующим правилам:

  • Победитель получает 2 очка.

  • Проигравший в основное время получает 0 очков.

  • Проигравший в овертайме или по серии послематчевых буллитов получает 1 очко.

Определение места в итоговом рейтинге

Главный критерий — сумма набранных за все матчи очков.

В случае равенства очков места между командами определяются последовательно по следующим правилам:

  • Личные встречи: большее количество очков в матчах между командами с равным количеством очков.

  • Разность голов: наибольшая разница забитых и пропущенных голов (по финальному счёту матчей) в матчах между командами с равным количеством очков.

  • Общая результативность: большее суммарное количество забитых голов во всех матчах чемпионата (по финальному счёту).

  • Количество побед: большее число побед (любого типа) во всех матчах.

  • Алфавитный порядок: команда, чьё название идет первым по алфавиту (от А к Я), получает более высокое место.

Помогите коротышкам написать запрос для получения итоговой таблицы с результатами и местами команд.

В итоговой таблице должны быть следующие поля:

  • Team — название команды;

  • Games — общее количество игр;

  • Points — набранные очки во всех играх;

  • Wins — игры, где команда победила;

  • Losses — игры, где команда проиграла;

  • Overtime losses — игры, где команда проиграла в добавленное время или в серии послематчевых бросков;

  • Goals for — количество забитых голов во всех встречах;

  • Goals against — количество пропущенных голов во всех встречах;

  • goal differential — разность между забитыми и пропущенными голами во всех встречах;

  • rn — итоговое место команды.

Пример

Для следующих входных данных:

insert into scores(team1,team2,g1,g2) values ('Незнайка','Гунька',3,5);
insert into scores(team1,team2,g1,g2) values ('Гунька','Незнайка',1,4);
insert into scores(team1,team2,g1,g2) values ('Незнайка','Знайка',3,2);
insert into scores(team1,team2,g1,g2,ot1,ot2) values ('Знайка','Незнайка',1,1,0,1);
insert into scores(team1,team2,g1,g2) values ('Незнайка','Сиропчик',5,1);
insert into scores(team1,team2,g1,g2) values ('Сиропчик','Незнайка',3,4);
insert into scores(team1,team2,g1,g2,ot1,ot2,so1,so2) values ('Гунька','Знайка',4,4,0,0,2,4);
insert into scores(team1,team2,g1,g2,ot1,ot2) values ('Знайка','Гунька',2,2,1,0);
insert into scores(team1,team2,g1,g2,ot1,ot2) values ('Гунька','Сиропчик',3,3,0,1);
insert into scores(team1,team2,g1,g2) values ('Сиропчик','Гунька',3,4);
insert into scores(team1,team2,g1,g2,ot1,ot2,so1,so2) values ('Знайка','Сиропчик',2,2,0,0,3,1);
insert into scores(team1,team2,g1,g2) values ('Сиропчик','Знайка',3,2);

Результат:

   team   | games | points | wins | losses | overtime losses | goals for | goals against | goal differential | rn 
----------+-------+--------+------+--------+-----------------+-----------+---------------+-------------------+----
 Незнайка |     6 |     10 |    5 |      1 |               0 |        21 |            13 |                 8 |  1
 Знайка   |     6 |      7 |    3 |      2 |               1 |        16 |            16 |                 0 |  2
 Гунька   |     6 |      7 |    2 |      1 |               3 |        19 |            22 |                -3 |  3
 Сиропчик |     6 |      5 |    2 |      3 |               1 |        16 |            21 |                -5 |  4
(4 rows)

Разбор решения

Формализуем правила:

Это задача на использование аналитических функций и ранжирования с разными критериями.

Следует обратить внимание, что каждая игра в итоговой таблице записана одной строкой. Команды всегда в полях team1 и team2. Чтобы получить статистику для каждой команды независимо от того, где она записана, нужно «развернуть» каждую строку в две: одну — для team1, другую — для team2.

Счёт матча

Главный подводный камень задачи — нельзя просто сложить g1 + ot1 + so1 и получить финальный счёт. По условию:

  • Голы основного времени (g1, g2) и овертайма (ot1, ot2) суммируются.

  • Буллиты (so1, so2) не суммируются в голы. Победа в серии буллитов добавляет ровно 1 гол к финальному счёту победившей команды.

Это означает, что при вычислении забитых и пропущенных голов (Goals for, Goals against) нужно считать голы этапов отдельно, а буллитный гол добавлять как +1 победителю серии. Например, если so1 = 3 и so2 = 1, команда 1 получает +1 гол, а команда 2 — 0 голов к финальному счёту, несмотря на то что реализовала 1 бросок.

Определение исхода матча

Для начисления очков нужно понять, как именно завершилась игра:

  • Победа в основное время: g1 != g2. Победитель получает 2 очка, проигравший — 0.

  • Победа в овертайме: g1 = g2 и ot1 != ot2. Победитель — 2 очка, проигравший — 1 очко (Overtime loss).

  • Победа по буллитам: g1 = g2 и ot1 = ot2 и so1 != so2. Победитель — 2 очка, проигравший — 1 очко (тоже Overtime loss).

  • Ничьей быть не может: регламент гарантирует выявление победителя.

Поля ot1, ot2, so1, so2 могут быть NULL — это означает, что до соответствующего этапа матч не дошёл. В таких случаях NULL нужно трактовать как 0 при сравнении, но не подменять им реальное отсутствие этапа.

Два типа поражений

В выводе нужны два разных поля:

  • Losses — поражения в основное время (0 очков);

  • Overtime losses — поражения в овертайме или по буллитам (1 очко).

Их нужно считать раздельно.

Многоступенчатое ранжирование

Критерии ранжирования применяются последовательно. Только если команды равны по первому критерию, переходят ко второму и так далее. Полный список:

  • Сумма набранных очков.

  • Личные встречи: больше очков в матчах между собой.

  • Разность голов в личных встречах.

  • Общая результативность: больше забитых голов во всех матчах.

  • Количество побед (любого типа).

  • Алфавитный порядок названий.

Самые сложные для реализации — критерии личных встреч (второй и третий в списке). Они требуют пересчёта очков и разности голов только в матчах между командами, имеющими равные общие очки. Это значит, что сначала нужно посчитать общие очки, выявить группы с равенством, а затем внутри каждой группы пересчитать очки и разность по личным встречам.

Разбор решения участника

Перейдём к рассмотрению оригинального решения конкурсанта с идентификатором 45533. В нём запрос работает над данными, которые предварительно собираются в json. Это используется для ранжирования команд с одинаковым количеством очков, когда необходимо смотреть личные встречи.

  1. Запрос match_meta рассчитывает исходные данные по очкам и голам:

    select
       team1,
       team2,
       case
          when g1 > g2 then 2
          when g1 < g2 then 0
          when coalesce(ot1, 0) > coalesce(ot2, 0) then 2
          when coalesce(ot1, 0) < coalesce(ot2, 0) then 1
          when coalesce(so1, 0) > coalesce(so2, 0) then 2
          when coalesce(so1, 0) < coalesce(so2, 0) then 1
       end as point1,
       g1
         + coalesce(ot1, 0)
         + case
              when coalesce(so1, 0) > coalesce(so2, 0) then 1
              else 0
           end as goal1,
        case
           when g2 > g1 then 2
           when g2 < g1 then 0
           when coalesce(ot2, 0) > coalesce(ot1, 0) then 2
           when coalesce(ot2, 0) < coalesce(ot1, 0) then 1
           when coalesce(so2, 0) > coalesce(so1, 0) then 2
           when coalesce(so2, 0) < coalesce(so1, 0) then 1
        end as point2,
        g2
          + coalesce(ot2, 0)
          + case
               when coalesce(so2, 0) > coalesce(so1, 0) then 1
               else 0
            end as goal2
    from scores
    Результат
      team1   |  team2   | point1 | goal1 | point2 | goal2 
    ----------+----------+--------+-------+--------+-------
     Незнайка | Гунька   |      0 |     3 |      2 |     5
     Гунька   | Незнайка |      0 |     1 |      2 |     4
     Незнайка | Знайка   |      2 |     3 |      0 |     2
     Знайка   | Незнайка |      1 |     1 |      2 |     2
     Незнайка | Сиропчик |      2 |     5 |      0 |     1
     Сиропчик | Незнайка |      0 |     3 |      2 |     4
     Гунька   | Знайка   |      1 |     4 |      2 |     5
     Знайка   | Гунька   |      2 |     3 |      1 |     2
     Гунька   | Сиропчик |      1 |     3 |      2 |     4
     Сиропчик | Гунька   |      0 |     3 |      2 |     4
     Знайка   | Сиропчик |      2 |     3 |      1 |     2
     Сиропчик | Знайка   |      2 |     3 |      0 |     2
    (12 rows)
  2. В запросе metadata упаковываем данные каждой команды в jsonb-массив:

    select
       jsonb_build_array(
            jsonb_build_object(
               'team', team1,
               'point', point1,
               'goal', goal1,
               'goal_diff', goal1 - goal2
            ),
            jsonb_build_object(
               'team', team2,
               'point', point2,
               'goal', goal2,
               'goal_diff', goal2 - goal1
            )
       ) as meta_json
    from match_meta
    Результат
                                                              meta_json                                                          
    -----------------------------------------------------------------------------------------------------------------------------
     [{"goal": 3, "team": "Незнайка", "point": 0, "goal_diff": -2}, {"goal": 5, "team": "Гунька", "point": 2, "goal_diff": 2}]
     [{"goal": 1, "team": "Гунька", "point": 0, "goal_diff": -3}, {"goal": 4, "team": "Незнайка", "point": 2, "goal_diff": 3}]
     [{"goal": 3, "team": "Незнайка", "point": 2, "goal_diff": 1}, {"goal": 2, "team": "Знайка", "point": 0, "goal_diff": -1}]
     [{"goal": 1, "team": "Знайка", "point": 1, "goal_diff": -1}, {"goal": 2, "team": "Незнайка", "point": 2, "goal_diff": 1}]
     [{"goal": 5, "team": "Незнайка", "point": 2, "goal_diff": 4}, {"goal": 1, "team": "Сиропчик", "point": 0, "goal_diff": -4}]
     [{"goal": 3, "team": "Сиропчик", "point": 0, "goal_diff": -1}, {"goal": 4, "team": "Незнайка", "point": 2, "goal_diff": 1}]
     [{"goal": 4, "team": "Гунька", "point": 1, "goal_diff": -1}, {"goal": 5, "team": "Знайка", "point": 2, "goal_diff": 1}]
     [{"goal": 3, "team": "Знайка", "point": 2, "goal_diff": 1}, {"goal": 2, "team": "Гунька", "point": 1, "goal_diff": -1}]
     [{"goal": 3, "team": "Гунька", "point": 1, "goal_diff": -1}, {"goal": 4, "team": "Сиропчик", "point": 2, "goal_diff": 1}]
     [{"goal": 3, "team": "Сиропчик", "point": 0, "goal_diff": -1}, {"goal": 4, "team": "Гунька", "point": 2, "goal_diff": 1}]
     [{"goal": 3, "team": "Знайка", "point": 2, "goal_diff": 1}, {"goal": 2, "team": "Сиропчик", "point": 1, "goal_diff": -1}]
     [{"goal": 3, "team": "Сиропчик", "point": 2, "goal_diff": 1}, {"goal": 2, "team": "Знайка", "point": 0, "goal_diff": -1}]
    (12 rows)
  3. В запросе earned_scores считаем очки для каждой команды:

    select distinct
         sum((value ->> 'point')::int) total_point,
         array_agg(value ->> 'team') over (partition by sum((value ->> 'point')::int)) teams 
      from metadata m
     cross join lateral jsonb_array_elements(m.meta_json)
     group by value ->> 'team'
    Результат
     total_point |      teams      
    -------------+-----------------
               5 | {Сиропчик}
               7 | {Знайка,Гунька}
              10 | {Незнайка}
    (3 rows)
  4. Запрос duel_info получает данные по командам в группе с одинаковым количеством очков:

    select distinct total_point, 
       coalesce(value ->> 'team', teams[1]) team, 
       sum((value ->>'point')::int) over byteam point_between, 
       sum((value ->> 'goal_diff')::int) over byteam maxdiff_betwen
      from earned_scores
      left join metadata m1 on  array[(meta_json -> 0) ->> 'team', (meta_json -> 1) ->> 'team'] <@ teams
    left join lateral jsonb_array_elements(meta_json) as e(value) on true
    window byteam as (partition by value ->> 'team')
    Результат
     total_point |   team   | point_between | maxdiff_betwen 
    -------------+----------+---------------+----------------
               5 | Сиропчик |               |               
              10 | Незнайка |               |               
               7 | Гунька   |             2 |             -2
               7 | Знайка   |             4 |              2
    (4 rows)
  5. Итоговый запрос — получение всех данных с ранжированием команд:

    select 
       Team, count(*) Games,
       total_point  Points,
       count(*) filter (where (value ->>'point')::int = 2) Wins,
       count(*) filter (where (value ->>'point')::int = 0) Losses,
       count(*) filter (where (value ->>'point')::int = 1) "overtime losses",
       sum((value ->> 'goal')::int) "goals for", 
       sum((value ->> 'goal')::int -  (value -> 'goal_diff')::int) "goals against",
       sum((value ->> 'goal_diff')::int) "goal differential",
       dense_rank() over 
          (order by total_point desc,
                    point_between desc,
                    maxdiff_betwen desc, 
                    sum((value ->> 'goal')::int) desc,
                    count(*) filter (where (value ->> 'point')::int = 2) desc,
                    team asc) rn
      from duel_info
      left join metadata m1 on  array[(meta_json -> 0) ->> 'team', (meta_json -> 1) ->> 'team'] @> array[team]
      left join lateral jsonb_array_elements(meta_json) as e(value) on true
     where value ->> 'team' = team
    group by total_point, team, point_between, maxdiff_betwen
Решение полностью:
with match_meta as (
    select
        team1,
        team2,
        case
            when g1 > g2 then 2
            when g1 < g2 then 0
            when coalesce(ot1, 0) > coalesce(ot2, 0) then 2
            when coalesce(ot1, 0) < coalesce(ot2, 0) then 1
            when coalesce(so1, 0) > coalesce(so2, 0) then 2
            when coalesce(so1, 0) < coalesce(so2, 0) then 1
        end as point1,
        g1
        + coalesce(ot1, 0)
        + case
              when coalesce(so1, 0) > coalesce(so2, 0) then 1
              else 0
          end as goal1,
        case
            when g2 > g1 then 2
            when g2 < g1 then 0
            when coalesce(ot2, 0) > coalesce(ot1, 0) then 2
            when coalesce(ot2, 0) < coalesce(ot1, 0) then 1
            when coalesce(so2, 0) > coalesce(so1, 0) then 2
            when coalesce(so2, 0) < coalesce(so1, 0) then 1
        end as point2,
        g2
        + coalesce(ot2, 0)
        + case
              when coalesce(so2, 0) > coalesce(so1, 0) then 1
              else 0
          end as goal2
    from scores
)
,
metadata as (
    select
        jsonb_build_array(
            jsonb_build_object(
                'team', team1,
                'point', point1,
                'goal', goal1,
                'goal_diff', goal1 - goal2
            ),
            jsonb_build_object(
                'team', team2,
                'point', point2,
                'goal', goal2,
                'goal_diff', goal2 - goal1
            )
        ) as meta_json
    from match_meta
)
,earned_scores as
(
select distinct
 sum((value ->> 'point')::int) total_point,
 array_agg(value ->> 'team') over (partition by sum((value ->> 'point')::int)) teams 
from metadata m
cross join lateral jsonb_array_elements(m.meta_json)
group by value ->> 'team'
)
,duel_info as (
select distinct total_point, 
coalesce(value ->> 'team', teams[1]) team, 
sum((value ->>'point')::int) over byteam point_between, 
sum((value ->> 'goal_diff')::int) over byteam maxdiff_betwen
from earned_scores
left join metadata m1 on  array[(meta_json -> 0) ->> 'team', (meta_json -> 1) ->> 'team'] <@ teams
left join lateral jsonb_array_elements(meta_json) as e(value) on true
window byteam as (partition by value ->> 'team')
)
select 
Team, count(*) Games,
total_point  Points,
count(*) filter (where (value ->>'point')::int = 2) Wins,
count(*) filter (where (value ->>'point')::int = 0) Losses,
count(*) filter (where (value ->>'point')::int = 1) "overtime losses",
sum((value ->> 'goal')::int) "goals for", 
sum((value ->> 'goal')::int -  (value -> 'goal_diff')::int) "goals against",
sum((value ->> 'goal_diff')::int) "goal differential",
dense_rank() over 
    (order by total_point desc,
              point_between desc,
              maxdiff_betwen desc, 
              sum((value ->> 'goal')::int) desc,
              count(*) filter (where (value ->> 'point')::int = 2) desc,
              team asc) rn
from duel_info
left join metadata m1 on  array[(meta_json -> 0) ->> 'team', (meta_json -> 1) ->> 'team'] @> array[team]
left join lateral jsonb_array_elements(meta_json) as e(value) on true
where value ->> 'team' = team
group by total_point, team, point_between, maxdiff_betwen;
Результат
   team   | games | points | wins | losses | overtime losses | goals for | goals against | goal differential | rn 
----------+-------+--------+------+--------+-----------------+-----------+---------------+-------------------+----
 Незнайка |     6 |     10 |    5 |      1 |               0 |        21 |            13 |                 8 |  1
 Знайка   |     6 |      7 |    3 |      2 |               1 |        16 |            16 |                 0 |  2
 Гунька   |     6 |      7 |    2 |      1 |               3 |        19 |            22 |                -3 |  3
 Сиропчик |     6 |      5 |    2 |      3 |               1 |        16 |            21 |                -5 |  4
(4 rows)

Задача 5: Мобильная сеть

Условие

Знайка с друзьями строят сотовую связь. Вышки сотовой связи уже установлены. Необходимо подключить их к электроэнергии. У коротышек есть карта, которая представляет собой прямоугольную сетку, на которую нанесены вышки сотовой связи и болота. Вам необходимо найти все возможные расположения трансформаторов рядом с вышками сотовой связи.

Правила расположения трансформаторов:

  1. Рядом (по горизонтали, вертикали или диагонали) с каждой вышкой сотовой связи должен быть ровно один трансформатор.

  2. Трансформатор не может находится рядом (по горизонтали, вертикали или диагонали) с другим трансформатором.

  3. Трансформатор не может располагаться в болоте.

Так как установка трансформаторов может помешать сезонному перемещению кузнечиков, имеется ограничение на количество трансформаторов для каждой строки и столбца исходной карты.

Данные хранятся в следующих таблицах:

Таблица objects:

create table objects(
     objecttype text not null 
        check (objecttype in ('T','#')),  -- тип объекта T - вышка, # - болото
     r int,                               -- строка
     c int,                               -- столбец
     primary key (r,c)
);

В таблице хранится координаты объектов: T — вышка, # — болото.

Таблица rows_limit:

create table rows_limit(
   r int primary key,     -- строка
   lim int not null       -- количество трансформаторов
);

В таблице хранятся ограничения по количеству трансформаторов на каждой строке карты.

Таблица cols_limit:

create table cols_limit(
   c int primary key,    -- столбец
     lim int not null      -- количество трансформаторов
);

В таблице хранятся ограничения по количеству трансформаторов в каждом столбце карты.

Вам необходимо получить все возможные размещения трансформаторов и сформировать прямоугольную карту с нанесёнными на неё трансформаторами (обозначив их буквой X), вышками (T) и болотами (#). Пустые клетки обозначьте буквой «o».

Размер карты определяется на основании исходных данных.

Каждую возможную карту следует вывести в текстовом виде, пронумеровав её строки и сконкатенировав их через запятую. Смотри пример вывода ниже.

Пример

Для следующих входных данных:

insert into objects(objecttype,r,c) values ('T',2,2),('T',2,6),('T',5,3),('T',6,7),('T',8,4),('T',1,4);
insert into objects(objecttype,r,c) values ('#',1,7),('#',2,8),('#',4,3),('#',5,2),('#',6,8),('#',7,6);
insert into rows_limit(r,lim) values (1,1),(2,2),(3,0),(4,1),(5,1),(6,0),(7,1),(8,0);
insert into cols_limit(c,lim) values (1,1),(2,1),(3,0),(4,2),(5,0),(6,0),(7,1),(8,1);

Результат:

                                        solution                                         
-----------------------------------------------------------------------------------------
 1:XooToo#o,2:oToXoTX#,3:oooooooo,4:oX#ooooo,5:o#TooooX,6:ooooooT#,7:oooXo#oo,8:oooToooo
(1 row)

Разбор решения

Формализуем правила

В основе задачи — известная головоломка Tents and Trees («Палатки и деревья») на размещение объектов с ограничениями, переложенная на вышки и трансформаторы: нужно не найти спрятанные объекты, а расставить новые, соблюдая правила соседства и ограничения.

Исходные данные

Прямоугольная сетка, на которой уже размещены два типа объектов:

  • T — вышка сотовой связи;

  • # — болото.

Размер сетки явно не задан и определяется по координатам объектов в таблицах как максимальные значения r и c среди всех переданных данных.

Дополнительно заданы ограничения:

  • для каждой строки — сколько трансформаторов может в ней находиться (rows_limit);

  • для каждого столбца — сколько трансформаторов может в нём находиться (cols_limit).

Необходимо

Найти все возможные размещения трансформаторов (обозначаются X), удовлетворяющие трём правилам, и вывести каждое решение в виде текстовой карты.

Правила размещения

Правило 1: Рядом с каждой вышкой ровно 1 трансформатор

Находящимися рядом считаются восемь соседних клеток (горизонталь, вертикаль, диагональ). Трансформатор должен быть ровно один — не ноль и не два. Это главное ограничение, связывающее вышки и трансформаторы.

Из этого правила следует важный вывод: трансформаторы можно ставить только в соседних с вышками клетках. Клетка, у которой нет вышек по соседству, никогда не будет содержать трансформатор. Это резко сужает пространство перебора.

Правило 2: Трансформаторы не могут стоять рядом друг с другом

Запрещено любое соседство трансформаторов, включая расположение диагонально. Это дополнительное ограничение совместимости: если выбрана клетка для трансформатора, все восемь её соседей автоматически исключаются из рассмотрения для других трансформаторов.

Правило 3: Трансформатор не может располагаться в болоте

Клетки с # запрещены для размещения. Если вышка окружена болотами так, что для трансформатора не остаётся ни одной допустимой клетки, задача для таких входных данных не имеет решения.

Ограничения по строкам и столбцам

Даже после учёта трёх правил может остаться несколько вариантов размещения. Ограничения rows_limit и cols_limit добивают неоднозначность или, наоборот, отсекают все варианты, если ограничения противоречат правилам соседства.

Важный нюанс: ограничения заданы для строк и столбцов исходной карты, а не для какого-то подмножества. Это означает, что строка с lim = 0 не может содержать ни одного трансформатора. Если в такой строке есть вышка, у которой все соседние клетки находятся в этой же строке — решения нет.

Размер карты и вывод

Размер карты определяется по исходным данным — как прямоугольник от (1,1) до (max_r, max_c). Пустые клетки (не вышка, не болото, не трансформатор) обозначаются символом «o».

Формат вывода: каждая найденная карта — это одна строка, в которой строки пронумерованы и перечислены через запятую. Пример из условия показывает ожидаемый формат.

Особенности

  • Несколько решений. Условие требует «все возможные размещения». Если решений несколько, вывести нужно все.

  • Отсутствие решений. При противоречивых входных данных (вышка в окружении болот, нулевое ограничение строки при вышке в этой строке) решений может не быть. Запрос должен корректно отработать и вернуть пустой результат.

  • Границы карты. Их нужно вычислить, а не брать фиксированными. В примере карта 8×8, но данные могут быть другими.

  • Нумерация строк в выводе. Формат вывода требует пронумеровать строки карты. Имеется в виду номер строки сетки, а не порядковый номер выводимой строки.

  • Уникальность трансформаторов. Количество трансформаторов равно количеству вышек, каждой вышке — свой трансформатор. Одна клетка не может обслуживать две вышки.

Разбор решения участника

Рассмотрим решение участника с идентификатором 121097.

  1. Запрос towers получает исходные данные для вышек связи и сразу их нумерует:

        select
            row_number() over (order by o.r, o.c)::int as tid,
            o.r,
            o.c
        from objects o
        where o.objecttype = 'T'
    Результат
     tid | r | c 
    -----+---+---
       1 | 1 | 4
       2 | 2 | 2
       3 | 2 | 6
       4 | 5 | 3
       5 | 6 | 7
       6 | 8 | 4
    (6 rows)
  2. В запросе grid получаем все клетки карты исходя из ограничений для строк и столбцов:

        select
            rl.r,
            cl.c
        from rows_limit rl
        cross join cols_limit cl
    Результат

    В результате может получиться много строк, например в рассматриваемом примере их будет 64, так как поле 8x8. Поэтому покажем результат частично

     r | c 
    ---+---
     1 | 1
     1 | 2
     1 | 3
     1 | 4
    ...
     8 | 7
     8 | 8
    (64 rows)
  3. Запрос candidates. Получаем список возможных размещений трансформаторов, для каждого размещения отмечаем номер вышки связи, которая может использовать данный трансформатор:

        select
            row_number() over (order by g.r, g.c)::int as cid,
            g.r,
            g.c,
            array_agg(t.tid order by t.tid)::int[] as tower_ids
        from grid g
        join towers t
          on abs(t.r - g.r) <= 1
         and abs(t.c - g.c) <= 1
        left join objects o
          on o.r = g.r
         and o.c = g.c
        where o.r is null
        group by g.r, g.c
    Результат
    cid | r | c | tower_ids 
    -----+---+---+-----------
       1 | 1 | 1 | {2}
       2 | 1 | 2 | {2}
       3 | 1 | 3 | {1,2}
       4 | 1 | 5 | {1,3}
       5 | 1 | 6 | {3}
       6 | 2 | 1 | {2}
       7 | 2 | 3 | {1,2}
       8 | 2 | 4 | {1}
       9 | 2 | 5 | {1,3}
      10 | 2 | 7 | {3}
      11 | 3 | 1 | {2}
      12 | 3 | 2 | {2}
      13 | 3 | 3 | {2}
      14 | 3 | 5 | {3}
      15 | 3 | 6 | {3}
      16 | 3 | 7 | {3}
      17 | 4 | 2 | {4}
      18 | 4 | 4 | {4}
      19 | 5 | 4 | {4}
      20 | 5 | 6 | {5}
      21 | 5 | 7 | {5}
      22 | 5 | 8 | {5}
      23 | 6 | 2 | {4}
      24 | 6 | 3 | {4}
      25 | 6 | 4 | {4}
      26 | 6 | 6 | {5}
      27 | 7 | 3 | {6}
      28 | 7 | 4 | {6}
      29 | 7 | 5 | {6}
      30 | 7 | 7 | {5}
      31 | 7 | 8 | {5}
      32 | 8 | 3 | {6}
      33 | 8 | 5 | {6}
    (33 rows)
  4. Рекурсивный запрос search находит вышку сотовой связи с минимальным идентификатором, для которой не определён трансформатор, и просматривает возможные варианты расстановки трансформаторов, проверяя все условия задачи. Таким образом получаем список возможных размещений трансформаторов, не нарушающих условий задачи:

        select
            array[]::int[] as chosen_ids,
            array[]::int[] as covered_ids
        union all
        select
            s.chosen_ids || c.cid,
            s.covered_ids || c.tower_ids
        from search s
        join lateral (
            select min(t.tid)::int as next_tid
            from towers t
            where t.tid <> all(s.covered_ids)
        ) nt
          on nt.next_tid is not null
        join candidates c
          on nt.next_tid = any(c.tower_ids)
        where not (c.tower_ids && s.covered_ids)
          and not exists (
              select 1
              from unnest(s.chosen_ids) as x(cid)
              join candidates pc
                on pc.cid = x.cid
              where abs(pc.r - c.r) <= 1
                and abs(pc.c - c.c) <= 1
          )
          and 1 + (
              select count(*)
              from unnest(s.chosen_ids) as x(cid)
              join candidates pc
                on pc.cid = x.cid
              where pc.r = c.r
          ) <= (
              select rl.lim
              from rows_limit rl
              where rl.r = c.r
          )
          and 1 + (
              select count(*)
              from unnest(s.chosen_ids) as x(cid)
              join candidates pc
                on pc.cid = x.cid
              where pc.c = c.c
          ) <= (
              select cl.lim
              from cols_limit cl
              where cl.c = c.c
          )
    Результат
        chosen_ids     |  covered_ids  
    -------------------+---------------
     {}                | {}
     {8}               | {1}
     {8,1}             | {1,2}
     {8,2}             | {1,2}
     {8,6}             | {1,2}
     {8,1,10}          | {1,2,3}
     {8,2,10}          | {1,2,3}
     {8,1,10,17}       | {1,2,3,4}
     {8,1,10,18}       | {1,2,3,4}
     {8,2,10,18}       | {1,2,3,4}
     {8,1,10,19}       | {1,2,3,4}
     {8,2,10,19}       | {1,2,3,4}
     {8,1,10,17,22}    | {1,2,3,4,5}
     {8,1,10,18,22}    | {1,2,3,4,5}
     {8,2,10,18,22}    | {1,2,3,4,5}
     {8,1,10,17,31}    | {1,2,3,4,5}
     {8,1,10,18,31}    | {1,2,3,4,5}
     {8,2,10,18,31}    | {1,2,3,4,5}
     {8,1,10,19,31}    | {1,2,3,4,5}
     {8,2,10,19,31}    | {1,2,3,4,5}
     {8,1,10,17,22,28} | {1,2,3,4,5,6}
    (21 rows)
  5. Запрос solutions оставляет только полные размещения трансформаторов, где их количество совпадает с количеством вышек связи и выполнены все ограничения:

        select s.chosen_ids
        from search s
        where cardinality(s.covered_ids) = (select count(*) from towers)
          and not exists (
              select 1
              from rows_limit rl
              where rl.lim <> (
                  select count(*)
                  from unnest(s.chosen_ids) as x(cid)
                  join candidates c
                    on c.cid = x.cid
                  where c.r = rl.r
              )
          )
          and not exists (
              select 1
              from cols_limit cl
              where cl.lim <> (
                  select count(*)
                  from unnest(s.chosen_ids) as x(cid)
                  join candidates c
                    on c.cid = x.cid
                  where c.c = cl.c
              )
          )
    Результат
        chosen_ids     
    -------------------
     {8,1,10,17,22,28}
    (1 row)
  6. В solution_rows для каждого решения строим строки карты:

        select
            s.chosen_ids,
            rl.r,
            rl.r::text || ':' ||
            string_agg(
                case
                    when exists (
                        select 1
                        from unnest(s.chosen_ids) as x(cid)
                        join candidates c
                          on c.cid = x.cid
                        where c.r = rl.r
                          and c.c = cl.c
                    ) then 'X'
                    when exists (
                        select 1
                        from objects o
                        where o.r = rl.r
                          and o.c = cl.c
                          and o.objecttype = 'T'
                    ) then 'T'
                    when exists (
                        select 1
                        from objects o
                        where o.r = rl.r
                          and o.c = cl.c
                          and o.objecttype = '#'
                    ) then '#'
                    else 'o'
                end,
                '' order by cl.c
            ) as row_txt
        from solutions s
        cross join rows_limit rl
        cross join cols_limit cl
        group by s.chosen_ids, rl.r
    Результат
        chosen_ids     | r |  row_txt   
    -------------------+---+------------
     {8,1,10,17,22,28} | 1 | 1:XooToo#o
     {8,1,10,17,22,28} | 2 | 2:oToXoTX#
     {8,1,10,17,22,28} | 3 | 3:oooooooo
     {8,1,10,17,22,28} | 4 | 4:oX#ooooo
     {8,1,10,17,22,28} | 5 | 5:o#TooooX
     {8,1,10,17,22,28} | 6 | 6:ooooooT#
     {8,1,10,17,22,28} | 7 | 7:oooXo#oo
     {8,1,10,17,22,28} | 8 | 8:oooToooo
    (8 rows)
  7. Осталось собрать одно решение в одну строку:

    select
        string_agg(sr.row_txt, ',' order by sr.r) as solution
    from solution_rows sr
    group by sr.chosen_ids
    order by solution
Решение полностью
with recursive
towers as (
    select
        row_number() over (order by o.r, o.c)::int as tid,
        o.r,
        o.c
    from objects o
    where o.objecttype = 'T'
)
,
grid as (
    select
        rl.r,
        cl.c
    from rows_limit rl
    cross join cols_limit cl
)
,
candidates as (
    select
        row_number() over (order by g.r, g.c)::int as cid,
        g.r,
        g.c,
        array_agg(t.tid order by t.tid)::int[] as tower_ids
    from grid g
    join towers t
      on abs(t.r - g.r) <= 1
     and abs(t.c - g.c) <= 1
    left join objects o
      on o.r = g.r
     and o.c = g.c
    where o.r is null
    group by g.r, g.c
)
,
search as (
    select
        array[]::int[] as chosen_ids,
        array[]::int[] as covered_ids
    union all
    select
        s.chosen_ids || c.cid,
        s.covered_ids || c.tower_ids
    from search s
    join lateral (
        select min(t.tid)::int as next_tid
        from towers t
        where t.tid <> all(s.covered_ids)
    ) nt
      on nt.next_tid is not null
    join candidates c
      on nt.next_tid = any(c.tower_ids)
    where not (c.tower_ids && s.covered_ids)
      and not exists (
          select 1
          from unnest(s.chosen_ids) as x(cid)
          join candidates pc
            on pc.cid = x.cid
          where abs(pc.r - c.r) <= 1
            and abs(pc.c - c.c) <= 1
      )
      and 1 + (
          select count(*)
          from unnest(s.chosen_ids) as x(cid)
          join candidates pc
            on pc.cid = x.cid
          where pc.r = c.r
      ) <= (
          select rl.lim
          from rows_limit rl
          where rl.r = c.r
      )
      and 1 + (
          select count(*)
          from unnest(s.chosen_ids) as x(cid)
          join candidates pc
            on pc.cid = x.cid
          where pc.c = c.c
      ) <= (
          select cl.lim
          from cols_limit cl
          where cl.c = c.c
      )
)
,
solutions as (
    select s.chosen_ids
    from search s
    where cardinality(s.covered_ids) = (select count(*) from towers)
      and not exists (
          select 1
          from rows_limit rl
          where rl.lim <> (
              select count(*)
              from unnest(s.chosen_ids) as x(cid)
              join candidates c
                on c.cid = x.cid
              where c.r = rl.r
          )
      )
      and not exists (
          select 1
          from cols_limit cl
          where cl.lim <> (
              select count(*)
              from unnest(s.chosen_ids) as x(cid)
              join candidates c
                on c.cid = x.cid
              where c.c = cl.c
          )
      )
)
,
solution_rows as (
    select
        s.chosen_ids,
        rl.r,
        rl.r::text || ':' ||
        string_agg(
            case
                when exists (
                    select 1
                    from unnest(s.chosen_ids) as x(cid)
                    join candidates c
                      on c.cid = x.cid
                    where c.r = rl.r
                      and c.c = cl.c
                ) then 'X'
                when exists (
                    select 1
                    from objects o
                    where o.r = rl.r
                      and o.c = cl.c
                      and o.objecttype = 'T'
                ) then 'T'
                when exists (
                    select 1
                    from objects o
                    where o.r = rl.r
                      and o.c = cl.c
                      and o.objecttype = '#'
                ) then '#'
                else 'o'
            end,
            '' order by cl.c
        ) as row_txt
    from solutions s
    cross join rows_limit rl
    cross join cols_limit cl
    group by s.chosen_ids, rl.r
)
select
    string_agg(sr.row_txt, ',' order by sr.r) as solution
from solution_rows sr
group by sr.chosen_ids
order by solution;
Результат
                                        solution                                         
-----------------------------------------------------------------------------------------
 1:XooToo#o,2:oToXoTX#,3:oooooooo,4:oX#ooooo,5:o#TooooX,6:ooooooT#,7:oooXo#oo,8:oooToooo
(1 row)

Некоторые итоги

  1. Очень странно, но многие присланные решения не проходили тест из формулировки. Они либо выдавали другое решение, либо вообще не выполнялись, сразу показывали ошибку. В общем, причины разные, но факт, что участники даже не пробовали запускать свои решения. Другого объяснения я не вижу.

  2. Также следует отметить много абсолютно одинаковых решений. Понятно, что сейчас без ИИ никто не будет выполнять такие задачи, но хорошо бы посмотреть на полученный запрос, осмыслить его и, может быть, исправить (ведь он, скорее всего, содержит ошибки и не будет работать).

  3. Порадовало, что есть те, кто решал сам. Такие решения сразу видны, они используют весь арсенал возможностей, которые даёт PostgreSQL: это и использование CTE, и JSON, и запросы lateral, и интервальные типы, и многое другое. Это говорит о глубоких знаниях конкурсантов.

  4. Не знаю, интересно ли было конкурсантам решать задачи, но мы точно получили удовольствие, придумывая задачи и решения к ним.

Ждём вас на следующих наших конкурсах.