Перестановка∗ p длины n записана по часовой стр...
تم الإنشاء في: ١٢ نوفمبر ٢٠٢٦
تم الإنشاء في: ١٢ نوفمبر ٢٠٢٦
Перестановка∗
p
длины n
записана по часовой стрелке на круговой шкале.
Выберите начальную позицию s
(1≤s≤n
) и прочитайте один полный круг по часовой стрелке, начиная с ps
и переходя после pn
к началу. Получившаяся последовательность имеет вид
q=[ps,ps+1,…,pn,p1,…,ps−1],
и тоже имеет длину n
.
Для любого непустого префикса этой последовательности [q1,q2,…,qk]
(k≥1
) пусть S
— соответствующее множество значений, то есть S={q1,q2,…,qk}
. Разобьём S
на максимальные отрезки последовательных целых чисел, и назовём эти отрезки блоками множества S
.
Например, у множества S={1,2,5,7,8,9}
есть 3
блока: {1,2}
, {5}
и {7,8,9}
.
Начальная позиция s
называется хорошей тогда и только тогда, когда для каждого непустого префикса q
соответствующее множество S
имеет не более 2
блоков.
Найдите количество хороших начальных позиций.
∗
Перестановкой длины n
является массив, состоящий из n
различных целых чисел от 1
до n
в произвольном порядке. Например, [2,3,1,5,4]
— перестановка, но [1,2,2]
не перестановка (2
встречается в массиве дважды) и [1,3,4]
тоже не перестановка (n=3
, но в массиве встречается 4
).
Входные данные
Каждый тест состоит из нескольких наборов входных данных. В первой строке находится одно целое число t
(1≤t≤104
) — количество наборов входных данных. Далее следует описание наборов входных данных.
Первая строка каждого набора входных данных содержит одно целое число n
(1≤n≤2⋅105
) — длину p
.
Вторая строка каждого набора входных данных содержит n
целых чисел p1,p2,…,pn
(1≤pi≤n
, все pi
различны) — элементы p
.
Гарантируется, что сумма значений n
по всем наборам входных данных не превосходит 2⋅105
.
Выходные данные
Для каждого набора входных данных выведите одно целое число — количество хороших начальных позиций.
реши на с++ с объяснением своей идеи
Решим задачу за на один тест.
Главная идея: будем рассматривать все возможные начальные позиции одновременно и поддерживать количество блоков для каждой из них.
Пусть у нас есть множество , и мы добавляем в него новое число .
Возможны четыре ситуации:
| Условие | Изменение числа блоков |
|---|---|
| Нет ни , ни | |
| Есть только | |
| Есть только | |
| Есть оба соседа |
Таким образом,
Здесь квадратные скобки равны 1, если условие истинно, и 0 иначе.
Это ключевое наблюдение: при добавлении одного числа количество блоков изменяется только на , или .
Чтобы удобно работать с циклическими сдвигами, мысленно допишем перестановку к самой себе:
Каждая начальная позиция соответствует подмассиву длины :
Будем двигать правую границу слева направо.
Для каждой начальной позиции хотим поддерживать количество блоков в множестве
Если в какой-то момент число блоков стало хотя бы 3, позиция навсегда становится плохой.
Важно: число блоков может уменьшаться, поэтому обычные два указателя здесь не подходят.
Пусть сейчас добавляем .
Будем хранить last[v] — последнюю позицию числа перед текущей позицией .
Для начальной позиции число уже находится в рассматриваемом подмассиве тогда и только тогда, когда
Аналогично для .
Поэтому изменение количества блоков можно выполнить тремя операциями:
Все эти операции — прибавление числа на отрезке, что удобно делать деревом отрезков с lazy propagation.
В дереве будем хранить максимальное количество блоков среди рассматриваемых начал.
Если для некоторого начала количество блоков достигло 3, найдём такую позицию и исключим её навсегда, установив значение .
Каждая позиция исключается не более одного раза.
cpp#include <bits/stdc++.h> using namespace std; const int NEG = -1000000000; struct SegTree { int n; vector<int> mx, lazy; SegTree(int n) : n(n) { mx.assign(4 * n + 5, 0); lazy.assign(4 * n + 5, 0); } void apply(int v, int val) { mx[v] += val; lazy[v] += val; } void push(int v) { if (lazy[v] != 0) { apply(v * 2, lazy[v]); apply(v * 2 + 1, lazy[v]); lazy[v] = 0; } } void pull(int v) { mx[v] = max(mx[v * 2], mx[v * 2 + 1]); } void add(int v, int tl, int tr, int l, int r, int val) { if (l > r) return; if (l == tl && r == tr) { apply(v, val); return; } push(v); int tm = (tl + tr) / 2; add(v * 2, tl, tm, l, min(r, tm), val); add(v * 2 + 1, tm + 1, tr, max(l, tm + 1), r, val); pull(v); } void add(int l, int r, int val) { if (l <= r) add(1, 1, n, l, r, val); } // Найти позицию со значением >= 3 // внутри отрезка [l, r]. int find_bad(int v, int tl, int tr, int l, int r) { if (l > r || mx[v] < 3) return -1; if (tl == tr) return tl; push(v); int tm = (tl + tr) / 2; int res = find_bad( v * 2, tl, tm, l, min(r, tm) ); if (res == -1) { res = find_bad( v * 2 + 1, tm + 1, tr, max(l, tm + 1), r ); } return res; } int find_bad(int l, int r) { if (l > r) return -1; return find_bad(1, 1, n, l, r); } void erase(int v, int tl, int tr, int pos) { if (tl == tr) { mx[v] = NEG; lazy[v] = 0; return; } push(v); int tm = (tl + tr) / 2; if (pos <= tm) erase(v * 2, tl, tm, pos); else erase(v * 2 + 1, tm + 1, tr, pos); pull(v); } void erase(int pos) { erase(1, 1, n, pos); } }; void solve() { int n; cin >> n; vector<int> p(n + 1); for (int i = 1; i <= n; i++) cin >> p[i]; SegTree st(n); vector<int> last(n + 2, 0); int ans = n; for (int r = 1; r <= 2 * n - 1; r++) { int x = p[(r - 1) % n + 1]; int R = min(r, n); // Добавляем новый элемент x st.add(1, R, 1); // Если x-1 уже присутствует, // новый блок не создаётся if (x > 1) { st.add(1, min(last[x - 1], n), -1); } // Если x+1 уже присутствует, // блоки могут объединиться if (x < n) { st.add(1, min(last[x + 1], n), -1); } last[x] = r; // Только подмассивы длины <= n int L = max(1, r - n + 1); // Удаляем все плохие начала while (true) { int pos = st.find_bad(L, R); if (pos == -1) break; st.erase(pos); ans--; } } cout << ans << '\n'; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) { solve(); } return 0; }
Рассмотрим перестановку:
Для начальной позиции получаем:
Добавление элементов Шаг из 5
Количество блоков
[Назад] [Далее]
После добавления числа имеем: