SilverTests.rupriority_queue · жадная симуляция
построение массива
Задача на кучу отрезков с жадным выбором лучшего
1Суть задачи
Дан массив длины n из нулей. На каждом из n шагов нужно:
- найти самый длинный подотрезок, состоящий из нулей; при равенстве длин — самый левый;
- в серединную клетку этого отрезка записать номер шага
i. Для отрезка чётной длины середина — левая из двух центральных.
После n шагов вывести получившийся массив. В условии сказано, что ответ существует и единственен.
Пример для n = 5 из условия — массив превращается в [2, 4, 1, 3, 5].
Ограничения: до 10⁴ тестов, n ≤ 2·10⁵, суммарно Σn ≤ 2·10⁵. То есть за все тесты мы выполним не более 2·10⁵ действий — важно вложиться в O(n log n).
2Где здесь куча
Посмотрим внимательно на то, что нам нужно на каждом шаге:
- взять лучший элемент (самый длинный/левый отрезок) — это приоритет;
- удалить его из рассмотрения;
- добавить новые кандидаты — две половинки оставшихся нулей.
Это буквально описание приоритетной очереди. Каждый отрезок нулей — это один элемент кучи. Когда мы ставим число в середину отрезка [l; r], этот отрезок разбивается на два: [l; m−1] и [m+1; r], которые мы кладём обратно в кучу. Через n шагов мы сделаем n операций pop и около 2n операций push — итого O(n log n).
Ключевое наблюдение. Отрезки нулей никогда не «склеиваются» обратно — поставили число в середину, и отрезок навсегда разделился на две независимые части. Поэтому достаточно один раз положить все рождающиеся отрезки в кучу и ничего не надо синхронизировать между ними.
3Алгоритм
Что хранить в куче
Каждый отрезок — пара (длина, левая граница). Длина нужна для приоритета, левая граница — для разрешения ничьих. По правой границе можно не хранить: она выводится как l + length − 1.
Как задать приоритет
| Цель |
Как закодировать |
| Сначала длина — по убыванию |
В Python кладём −length (min-heap выдаст самую отрицательную = самую большую по модулю) |
При равенстве — по возрастанию l |
В Python кладём l как есть (min-heap и так выдаст сначала меньшее) |
В итоге в Python ключ кучи — тройка (−length, l, r). В C++ для priority_queue (max-heap) используем (length, −l, r): большая длина выигрывает, при равной длине выигрывает больший −l, то есть меньший l.
Индекс середины (в 0-based)
В условии индексация с 1, и там различаются формулы для чётной и нечётной длины. Если считать с 0, формула одна для всех случаев:
# Независимо от чётности длины:
m = (l + r) // 2
Для нечётной длины это настоящий центр, для чётной — левый из двух центральных. Именно это и нужно в условии.
Пошаговая схема
- Положить в кучу исходный отрезок
[0; n−1] длины n.
- Для
i от 1 до n: достать верх кучи — отрезок [l; r].
- Вычислить
m = (l + r) / 2 и записать a[m] = i.
- Если левая половинка
[l; m−1] непустая — положить её в кучу. То же для правой [m+1; r].
4Пошаговая симуляция
Выбери длину массива и прожми по шагам. Слева массив, справа — текущее состояние кучи отрезков. Самый приоритетный отрезок (тот, что сейчас будет обработан) подсвечен.
Интерактив — нажимай «шаг», чтобы проследить работу алгоритма
Массив a
Нажми «шаг», чтобы начать симуляцию.
Куча отрезков (сверху — самый приоритетный)
шаг 0 / 5
Обрати внимание, как на ничьих побеждает самый левый отрезок: после первой постановки в центр n = 5 получатся два отрезка длины 2, и алгоритм берёт сначала левый [0; 1], только потом правый [3; 4].
5Реализация
import heapq
import sys
input = sys.stdin.readline
def solve(n):
a = [0] * n
# (-length, l, r) — минимум даёт максимальную длину,
# при равенстве — меньший l
heap = [(-n, 0, n - 1)]
for i in range(1, n + 1):
neg_len, l, r = heapq.heappop(heap)
m = (l + r) // 2
a[m] = i
if l <= m - 1:
heapq.heappush(heap, (-(m - l), l, m - 1))
if m + 1 <= r:
heapq.heappush(heap, (-(r - m), m + 1, r))
return a
t = int(input())
out = []
for _ in range(t):
n = int(input())
out.append(' '.join(map(str, solve(n))))
print('\n'.join(out))
Важный момент для Python и больших тестов: sys.stdin.readline вместо обычного input() и накопление ответа в список с последующим print('\n'.join(...)) — иначе можно получить TL из-за медленного ввода-вывода.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
vector<int> a(n, 0);
// (length, -l, r) — max-heap выдаст наибольшую длину,
// при равенстве — наибольший (-l), то есть наименьший l
priority_queue<tuple<int, int, int>> pq;
pq.push({n, 0, n - 1});
for (int i = 1; i <= n; i++) {
auto [len, neg_l, r] = pq.top(); pq.pop();
int l = -neg_l;
int m = (l + r) / 2;
a[m] = i;
if (l <= m - 1) {
pq.push({m - l, -l, m - 1});
}
if (m + 1 <= r) {
pq.push({r - m, -(m + 1), r});
}
}
for (int x : a) cout << x << ' ';
cout << '\n';
}
return 0;
}
Структурированные связывания (auto [len, neg_l, r] = pq.top()) появились в C++17. На олимпиадах стандарт C++17 или C++20 — использовать можно смело.
6Сложность и проверка
За один тест выполняется ровно n операций pop и не более 2n операций push (каждое разбиение рождает не более двух новых отрезков, иногда один или ноль, когда получается пустая половинка). Итого O(n log n) на тест и O(Σn · log(maxn)) на все тесты — укладывается в лимит с большим запасом.
| Оценка |
Сколько операций |
Укладываемся в 2 сек? |
| Σn = 2·10⁵ |
≈ 2·10⁵ · 18 = 3.6·10⁶ сравнений |
да, с запасом |
| Если бы было O(n²) |
≈ 4·10¹⁰ для одного большого теста |
нет, получаем TL |
По памяти — O(n) на массив и кучу, никаких проблем.
7Типичные ошибки
Забытое правило leftmost
Если при равной длине отрезков взять произвольный (а не самый левый), ответ поменяется. В Python если положить в кучу (-length,) без второй компоненты — при равных длинах порядок определит heapq по своему внутреннему счётчику, а это фактически случайно. Нужно обязательно добавить l как вторую компоненту ключа.
Перепутанная середина для чётной длины
В 1-based условии середина чётного отрезка — это (l + r − 1) / 2, а не (l + r + 1) / 2. То есть левый из двух центральных, не правый. В 0-based это естественным образом даёт (l + r) // 2 — никаких дополнительных проверок.
Пустые половинки
Когда отрезок длины 1, после постановки он превращается в две пустые половинки — их не надо класть обратно в кучу. Поэтому проверки l ≤ m − 1 и m + 1 ≤ r обязательны.
Медленный ввод в Python
При t = 10⁴ вызовов input() чистый Python может не успеть только на чтении. Ставим sys.stdin.readline и выводим всё одним print через '\n'.join — это даёт многократное ускорение.
8Что запомнить
Схема задачи. «На каждом шаге: найди лучший, удали, положи два новых» — это типовая схема под priority_queue. Отрезок кодируем как (length, l); приоритет — сначала длина убывающе, потом l возрастающе; середина в 0-based всегда (l + r) / 2. Сложность O(n log n), ровно n операций pop и не более 2n операций push.