只保证自己看懂。
cdq分治和整体二分是两个比较重要的离线算法。
cdq分治
1.三维偏序
设 (f(i)) 表示满足 (a_j < a_i,b_j<b_i,c_j<c_i) 的 (j) 的数量。
考虑只有一维的时候,就是排序。
考虑只有二维的时候,就是按 (a_j < a_i) 排序之后,从 (1) 到 (n) 枚举 (i) ,把 (b_i) 加入树状数组,则 (f(i)) 为满足树状数组中满足 (b_j < b_i) 的数目。
考虑三维的时候,先按 (a_j < a_i) 排序,然后按 (b_j < b_i) 做归并排序(要保证 (a_j < a_i) 不变,归并排序是稳定的,所以可以保证)。
对于区间 ([l,r]) ,此时 ([l,mid]) 和 ((mid,r]) 已经排好序并贡献答案了,对于 (j in [l,mid],i in (mid,r]) 总有 (b_j < b_i) ,所以我们在 merge
的时候套上一个树状数组,算出 (c_j < c_i) 的数目。
总复杂度为 (O(n log n log ext{值域大小})) ,实际上离散化之后就是 (O(n log^2 n)) 。
例题:Luogu P3810
注意这题为“ (leq) ” ,所以我们要先去重再做,具体细节看下面代码。
#include <cstdio>
#include <cctype>
#include <algorithm>
#define MAX_N (100000 + 5)
#define MAX_K (200000 + 5)
using std::sort;
int n, K;
struct Node {
int a, b, c;
int cnt;
int sum;
bool operator < (const Node &x) const {
if (a != x.a) return a < x.a;
if (b != x.b) return b < x.b;
return c < x.c;
}
bool operator != (const Node &x) const {
return a != x.a || b != x.b || c != x.c;
}
} a[MAX_N], b[MAX_N];
int s[MAX_K];
int ans[MAX_N];
int Read() {
int res = 0;
char ch = getchar();
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) res = res * 10 + ch - '0', ch = getchar();
return res;
}
int lowbit(int x) {
return x & -x;
}
void Add(int x, int val) {
for (; x <= K; x += lowbit(x))
s[x] += val;
}
int Sum(int x) {
int res = 0;
for (; x; x -= lowbit(x))
res += s[x];
return res;
}
void cdq(int l, int r) {
if (l == r) return;
int mid = l + r >> 1;
cdq(l, mid);
cdq(mid + 1, r);
int i = l, j = mid + 1, k = 0;
while (i <= mid && j <= r) {
if (a[i].b <= a[j].b) {
Add(a[i].c, a[i].cnt);
b[++k] = a[i++];
} else {
a[j].sum += Sum(a[j].c);
b[++k] = a[j++];
}
}
while (i <= mid) {
Add(a[i].c, a[i].cnt);
b[++k] = a[i++];
}
while (j <= r) {
a[j].sum += Sum(a[j].c);
b[++k] = a[j++];
}
for (int i = l; i <= mid; ++i)
Add(a[i].c, -a[i].cnt);
for (int i = 1; i <= k; ++i)
a[l + i - 1] = b[i];
}
int main() {
n = Read(), K = Read();
for (int i = 1; i <= n; ++i) {
a[i].a = Read(), a[i].b = Read(), a[i].c = Read();
a[i].cnt = 1;
}
sort(a + 1, a + n + 1);
int m = 0;
for (int i = 1; i <= n; ++i) {
if (a[i] != a[i + 1]) a[++m] = a[i];
else a[i + 1].cnt += a[i].cnt;
}
cdq(1, m);
for (int i = 1; i <= m; ++i)
ans[a[i].sum + a[i].cnt - 1] += a[i].cnt;
for (int i = 0; i < n; ++i)
printf("%d
", ans[i]);
return 0;
}
2.应用
例题:Luogu P3157
(j < i,a_j > a_i) 再加上时间,那就是三维偏序,直接套cdq分治就好了。
整体二分
1.静态区间第 (k) 小
整体二分是什么?
显然是二分啊。
我们先把输入 (a_i) 和询问看作两种操作,一起放入 (q_1,q_2,q_3,cdots,q_{n+m}) 。
于是我们设当前二分的答案区间为 ([l,r]) ,令 (mid) 为其中值。
又设二分的操作序号区间为 ([L,R]) 。
我们先将 ([L,R]) 中的满足 (a_i in [l,mid]) 的 (i) 加入一个数组 (t_i)中,若 (a_i) 满足条件则第 (i) 位 (+1) ,即 (t_i gets t_i+1) 。
然后对于 ([L,R]) 中的每一个询问操作,设询问区间为 ([ql,qr]) ,则设 (res = sumlimits_{i=ql}^{qr}t_i) (即满足 (a_i in [ql,qr]) 的数量)。
若 (k leq res) ,则答案一定在 ([l,mid]) 中;若 (k > res) ,则答案一定在 ((mid,r]) 中。
我们在分别处理 ([l,mid]) 和 ((mid,r]) 即可,注意要同时更新 ([L,R]) 。
至于 (t_i) 其实可以用树状数组维护。
则二分的复杂度为 (O(nlog ext{值域大小})) ,树状数组修改和查询的复杂度为 (O(log n)) ,总复杂度为 (O(n log n log ext{值域大小})) 。
当然,离散化之后可以变成 (O(n log^2 n)) ,这里就不写了。
例题:Luogu P3834
具体细节看代码。
注意 mid = l + r >> 1
是向下取整, mid = (l + r) / 2
是向 (0) 取整。
#include <cstdio>
#include <cctype>
#define MAX_N (200000 + 5)
#define MAX_M (200000 + 5)
int n, m;
struct Opt {
int l, r, k, idx, opt;
} q[MAX_N + MAX_M], q1[MAX_N + MAX_M], q2[MAX_N + MAX_M];
int s[MAX_N];
int ans[MAX_M];
int Read() {
int res = 0, sign = 0;
char ch = getchar();
while (!isdigit(ch)) sign |= ch == '-', ch = getchar();
while (isdigit(ch)) res = res * 10 + ch - '0', ch = getchar();
return sign ? -res : res;
}
int lowbit(int x) {
return x & -x;
}
void Add(int pos, int val) {
for (; pos <= n; pos += lowbit(pos))
s[pos] += val;
}
int Sum(int pos) {
int res = 0;
for (; pos; pos -= lowbit(pos))
res += s[pos];
return res;
}
void Solve(int l, int r, int L, int R) {
if (L > R) return;
if (l == r) {
for (int i = L; i <= R; ++i)
if (q[i].opt) ans[q[i].idx] = l;
return;
}
int mid = l + r >> 1, cnt1 = 0, cnt2 = 0, res;
for (int i = L; i <= R; ++i) {
if (q[i].opt) {
res = Sum(q[i].r) - Sum(q[i].l - 1);
if (res >= q[i].k) q1[++cnt1] = q[i];
else q[i].k -= res, q2[++cnt2] = q[i];
} else {
if (q[i].k <= mid) q1[++cnt1] = q[i], Add(q[i].idx, 1);
else q2[++cnt2] = q[i];
}
}
for (int i = 1; i <= cnt1; ++i)
q[L + i - 1] = q1[i];
for (int i = 1; i <= cnt2; ++i)
q[L + cnt1 + i - 1] = q2[i];
for (int i = 1; i <= cnt1; ++i)
if (!q1[i].opt) Add(q1[i].idx, -1);
Solve(l, mid, L, L + cnt1 - 1);
Solve(mid + 1, r, L + cnt1, R);
}
int main() {
n = Read(), m = Read();
for (int i = 1; i <= n; ++i)
q[i] = (Opt){0, 0, Read(), i, 0};
for (int i = 1; i <= m; ++i)
q[i + n] = (Opt){Read(), Read(), Read(), i, 1};
Solve(-1e9, 1e9, 1, n + m);
for (int i = 1; i <= m; ++i)
printf("%d
", ans[i]);
return 0;
}
2.动态区间第 (k) 小
其实把输入 (a_i) 也看作是操作 ( exttt{C} i a_i) 。
于是可以把输入操作看作 (operatorname{add}(i,1)) ,把修改操作拆成 (operatorname{add}(x,-1)) 和 (operatorname{add}(x,1)) 。
记录每一次 (operatorname{add}) 操作时的 (a_x) 即可。
例题:Luogu P2617
#include <cstdio>
#include <cctype>
#include <cstring>
#define MAX_N (100000 + 5)
#define MAX_M (100000 + 5)
int n, m;
int a[MAX_N];
struct Opt {
char opt;
int l, r, k, idx;
} q[MAX_N + MAX_M * 2], q1[MAX_N + MAX_M * 2], q2[MAX_N + MAX_M * 2];
int s[MAX_N];
int ans[MAX_N];
int Read() {
int res = 0;
char ch = getchar();
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) res = res * 10 + ch - '0', ch = getchar();
return res;
}
int lowbit(int x) {
return x & -x;
}
void Add(int pos, int val) {
for (; pos <= n; pos += lowbit(pos))
s[pos] += val;
}
int Sum(int pos) {
int res = 0;
for (; pos; pos -= lowbit(pos))
res += s[pos];
return res;
}
void Solve(int l, int r, int L, int R) {
if (L > R) return;
if (l == r) {
for (int i = L; i <= R; ++i)
if (q[i].opt == 'Q') ans[q[i].idx] = l;
return;
}
int mid = l + r >> 1, cnt1 = 0, cnt2 = 0, res;
for (int i = L; i <= R; ++i) {
if (q[i].opt == 'C') {
if (q[i].k <= mid) q1[++cnt1] = q[i], Add(q[i].l, q[i].r);
else q2[++cnt2] = q[i];
} else {
res = Sum(q[i].r) - Sum(q[i].l - 1);
if (q[i].k <= res) q1[++cnt1] = q[i];
else q[i].k -= res, q2[++cnt2] = q[i];
}
}
for (int i = 1; i <= cnt1; ++i)
q[L + i - 1] = q1[i];
for (int i = 1; i <= cnt2; ++i)
q[L + cnt1 + i - 1] = q2[i];
for (int i = 1; i <= cnt1; ++i)
if (q1[i].opt == 'C') Add(q1[i].l, -q1[i].r);
Solve(l, mid, L, L + cnt1 - 1);
Solve(mid + 1, r, L + cnt1, R);
}
int main() {
n = Read(), m = Read();
int cnt = 0;
for (int i = 1; i <= n; ++i){
a[i] = Read();
q[++cnt] = (Opt){'C', i, 1, a[i], 0};
}
char ch;
int x, y;
for (int i = 1; i <= m; ++i) {
ch = getchar();
while (!isalpha(ch)) ch = getchar();
x = Read(), y = Read();
if (ch == 'C') {
q[++cnt] = (Opt){'C', x, -1, a[x], 0};
a[x] = y;
q[++cnt] = (Opt){'C', x, 1, y, 0};
} else
q[++cnt] = (Opt){'Q', x, y, Read(), i};
}
memset(ans, -1, sizeof ans);
Solve(0, 1e9, 1, cnt);
for (int i = 1; i <= m; ++i)
if (~ans[i]) printf("%d
", ans[i]);
return 0;
}