Автор задачі: | Андрій Холод (Connect) |
Підготував: | Андрій Холод (Connect) |
Розбір: | Андрій Холод (Connect) |
Ідея
Для кожного запиту [l,r,k] ми застосовуємо прожекторне сортування до підвідрізка , а потім шукаємо mex(a[l..k]).
На перший погляд це виглядає як якась дивна симуляція сортувань усередині багатьох блоків. Але тут є дуже важливе спостереження:
у відповідь входять усі повністю покриті блоки як є, а з останнього блока — лише кілька найменших елементів.
1) Як виглядає префікс після прожекторного сортування
Нехай ми починаємо в позиції . Тоді підвідрізок розбивається на блоки довжин 1,2,3,\dots поки елементи не закінчаться.
Нехай позиція лежить у деякому блоці довжини .
Тоді всі попередні блоки цілком потрапляють у префікс . Після сортування кожен із них просто переставляється всередині себе, але множина елементів блока не змінюється. Отже для важлива лише наявність чисел, а не їхній порядок.
А от із блока, який містить , у префікс потрапляють лише перші t+1 елементів, де — позиція всередині цього блока.
Після сортування блока ці перші t+1 елементів — це просто t+1 найменших елементів блока.
Отже відповідь на запит — це множини:
усіх елементів із повністю покритих блоків;
плюс t+1 найменших елементів з поточного блока.
2) Як опрацьовувати запит
Для кожного запиту знаходимо блок, у якому лежить .
Нехай:
— початковий ,
[L, l-1] — усі повністю покриті блоки,
[l, r_1] — блок, який містить ,
k' — позиція всередині цього блока.
Тобто:
[l, r] — це велика частина, яку можна обробляти офлайн;
[l1, r1] — маленький блок, з якого треба взяти лише кілька найменших;
k — скільки саме елементів беремо з цього блока.
3) Чому маленький блок справді маленький
Розміри блоків — це 1,2,3,\dots. Якщо позиція лежить у блоці довжини , то до нього вже покрито приблизно 1 + 2 + \dots + c = \frac{c(c+1)}{2} елементів.
Звідси
c = O(\sqrt{n}).
При n \le 2 \cdot 10^5 маємо c \lesssim 650, тому блок дуже маленький.
Саме тому з останнім блоком можна працювати окремо напряму.
4) Що робимо з повністю покритою частиною
Для всіх запитів треба знати множину значень на підвідрізку a[Q_l,...,Q_r].
Це класична офлайн-обробка через Mo's algorithm.
Запити сортуються за блоком лівої межі та правою межею. Далі ми підтримуємо поточний відрізок [l,r] і масив частот cnt[x]. Для треба знати, які значення присутні, а які ні.
5) Як швидко підтримується mex
Оскільки для масиву довжини не більше ніколи не перевищує , усі значення можна обрізати.
Це коректно, бо числа більші за ніколи не вплинуть на .
Далі значення 0,1,\dots,n діляться на блоки довжини C=400.
cnt[x] — скільки разів число входить у поточну множину;
block[b] — скільки чисел із цього блоку присутні хоча б раз.
Тоді, щоб знайти :
знаходимо перший блок, де не всі чисел присутні;
всередині нього лінійно шукаємо перше число з нульовою частотою.
6) Що робимо з останнім блоком
Останній блок малий, тому можна просто скопіювати його елементи. Потрібно взяти лише i.k + 1 найменших елементів цього блока. Після копіювання гарантується, що:
елемент на позиції i.k стоїть так, як у відсортованому масиві;
усі елементи лівіше не більші за нього.
Тобто перші i.k+1 елементів — це саме деякий порядок із i.k+1 найменших елементів блока. Для порядок неважливий, важлива лише множина, тому цього повністю достатньо.
8) Асимптотика
Нехай B = O(\sqrt{n}) — максимальний розмір блока прожекторного сортування.
Mo's algorithm дає стандартну складність O((n+q)\sqrt{n}) на пересування меж.
Для кожного запиту ми ще обробляємо малий блок за у середньому через nth_element та кілька add/del.
Тому загальна складність укладається в обмеження: O((n+q)\sqrt{n}).
//#pragma GCC optimize("Ofast", "unroll-loops")
//#pragma GCC target("sse", "sse2", "sse3", "ssse3", "sse4")
#include <bits/stdc++.h>
#define all(a) a.begin(),a.end()
#define len(a) (int)(a.size())
#define mp make_pair
#define pb push_back
#define fi first
#define se second
using namespace std;
typedef pair<int, int> pii;
typedef long long ll;
typedef long double ld;
template<class T>
using vec = vector<T>;
template<typename T>
bool umin(T &a, T b) {
if (b < a) {
a = b;
return true;
}
return false;
}
template<typename T>
bool umax(T &a, T b) {
if (a < b) {
a = b;
return true;
}
return false;
}
#ifdef KoRoVa
#define DEBUG for (bool _FLAG = true; _FLAG; _FLAG = false)
#define LOG(...) print(#__VA_ARGS__" ::", __VA_ARGS__) << endl
template <class ...Ts> auto &print(Ts ...ts) { return ((cerr << ts << " "), ...); }
#else
#define DEBUG while (false)
#define LOG(...)
#endif
const int max_n = 2e5 + 11, inf = 1000111222;
int a[max_n];
const int C = 400;
int id[max_n];
struct seg {
int l, r;
int l1, r1, k;
int id;
bool operator < (const seg &x) const {
if (::id[l] != ::id[x.l]) {
return l < x.l;
}
return r < x.r;
}
};
int cnt[max_n];
int block[max_n / C + 11];
int val[1000];
int main() {
// freopen("input.txt", "r", stdin);
// freopen("output.txt", "w", stdout);
ios_base::sync_with_stdio(0);
cin.tie(0);
for (int i = 0; i < max_n; i++) {
id[i] = i / C;
}
int n, q;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
umin(a[i], n);
}
cin >> q;
vector <seg> Q(q);
vector <int> ans(q);
for (int i = 0, l, r, k; i < q; i++) {
cin >> l >> r >> k;
--l, --r, --k;
k = k - l;
// k from zero
k += l;
int c = 1;
int L = l;
while (l + c - 1 < k) {
l += c;
++c;
}
Q[i].id = i;
Q[i].l = L;
Q[i].r = l - 1;
Q[i].l1 = l;
Q[i].r1 = min(r, l + c - 1);
Q[i].k = k - l;
}
sort(all(Q));
int l = 0, r = -1;
auto add = [&] (int x) {
LOG("+", x);
if (cnt[x]++ == 0) {
++block[id[x]];
}
};
auto del = [&] (int x) {
LOG("-", x);
if (--cnt[x] == 0) {
--block[id[x]];
}
};
for (auto &i : Q) {
LOG(i.l, i.r, l, r);
while (i.r > r) {
++r;
add(a[r]);
}
while (l > i.l) {
--l;
add(a[l]);
}
while (i.r < r) {
del(a[r]);
--r;
}
while (l < i.l) {
del(a[l]);
++l;
}
assert(i.r1 - i.l1 + 1 <= 1000);
for (int j = i.l1; j <= i.r1; j++) {
val[j - i.l1] = a[j];
}
nth_element(val, val + i.k, val + (i.r1 - i.l1 + 1));
for (int j = 0; j <= i.k; j++) {
add(val[j]);
}
int res = 0;
while (block[res] == C) {
++res;
}
res *= C;
while (cnt[res]) {
++res;
}
LOG(i.id, cnt[0]);
ans[i.id] = res;
for (int j = 0; j <= i.k; j++) {
del(val[j]);
}
}
for (auto &i : ans) {
cout << i << '\n';
}
}
/*
KoRoVa!
*/