[学习笔记] CDQ分治
引入 - 分治
分治,就是将讲原问题不断细分直到规模小到能够解决,然后一层层向上合并得到答案的过程。
归并排序
大致思想:把序列拆成左右两部分,分别归并排序,然后使用两个指针按序合并左右部分。
归并求逆序对
归并求逆序对是分治的一个经典例子。
要做的就是在合并过程中计算逆序对对数。
由于合并的是两个有序数组,那么当 \(num_{left}>num_{right}\) 时,\(num_{left},num_{left+1},...,num_{mid}\) 显然都是大于 \(num_{right}\) 的,那么就可以直接统计对数。
代码实现
const int N = 500010;
int n, a[N], tmp[N];
ll ans;
void merge(int l, int r) {
if (l == r) return ;
int mid = l + r >> 1;
merge(l, mid), merge(mid + 1, r);
int lf = l, rt = mid + 1, now = l;
while (lf <= mid && rt <= r) {
if (a[lf] <= a[rt]) tmp[now++] = a[lf++];
else tmp[now++] = a[rt++], ans += (mid - lf + 1);
}
while (lf <= mid) tmp[now++] = a[lf++];
while (rt <= r) tmp[now++] = a[rt++];
for (int i = l; i <= r; i++) a[i] = tmp[i];
}
int main() {
n = read();
for (int i = 1; i <= n; i++) a[i] = read();
merge(1, n);
printf("%lld\n", ans);
return 0;
}
可以发现在合并过程中,加上了左段对右段的贡献。这是CDQ分治的一个典型过程。
CDQ分治
优点:常数较小,可代替高级数据结构,易实现。
缺点:必须离线。
二维偏序
给 \(n\) 个元素,第 \(i\) 个元素有 \(a_i,b_i\) 两个属性,设 \(f(i)\) 表示满足 \(a_j<a_i\) 且 \(b_j<b_i\) 的 \(j\) 的数量。
对于 \(d \in [0,n)\) ,求 \(f(i)=d\) 的数量。
思路
把第 \(i\) 个元素看成 \((a_i,b_i)\) 的一个点,那么求的就是以该点为左上角的矩形包含多少点。
可以先按 \(x(a_i)\) 排个序,用树状数组维护,\(sum_i\) 表示已经考虑过的点中 \(y\) 小于等于 \(y(b_i)\) 的点的个数。
代码
这题题面没说有多组数据,但是会WA。
const int N = 15010, M = 32010;
int n, mx;
struct point {
int x, y;
bool operator <(const point &b) {return x == b.x ? y < b.y : x < b.x;}
} a[N];
int sum[M];
void add(int x, int p) {
while (x <= mx) {
sum[x] += p;
x += lowbit(x);
}
}
int query(int x) {
int res = 0;
while (x) {
res += sum[x];
x -= lowbit(x);
}
return res;
}
int ans[N];
int main() {
while (cin >> n) {
memset(sum, 0, sizeof(sum)), memset(ans, 0, sizeof(ans));
mx = 0;
for (int i = 1; i <= n; i++) a[i].x = read() + 1, a[i].y = read() + 1, mx = max(mx, a[i].y);
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) {
ans[query(a[i].y)]++;
add(a[i].y, 1);
}
for (int i = 0; i < n; i++) printf("%d\n", ans[i]);
}
return 0;
}
三维偏序
有 \(n\) 个元素,第 \(i\) 个元素有 \(a_i,b_i,c_i\) 三个属性,设 \(f(i)\) 表示满足 \(a_j\leq a_i\) 且 \(b_j\leq b_i\) 且 \(cj\leq c_i\) 且 \(j\neq i\) 的 jj 的数量。
对于 \(d\in[0,n)\),求 \(f(i)=d\) 的数量。
思路
按 \(a\) 排序,归并 \(b\),树状数组维护 \(c\)。
代码实现
const int N = 100010;
int n, k;
struct node {
int a, b, c, cnt, ans;
} a[N], tmp[N];
bool cmp(const node &a, const node &b) {
if (a.a != b.a) return a.a < b.a;
if (a.b != b.b) return a.b < b.b;
return a.c < b.c;
}
int sum[N << 1 | 1];
void add(int x, int p) {
while (x <= N << 1) {
sum[x] += p;
x += lowbit(x);
}
}
int query(int x) {
int res = 0;
while (x) {
res += sum[x];
x -= lowbit(x);
}
return res;
}
ll ans[N];
queue <int> q;
void work(int l, int r) {
if (l == r) return ;
int mid = l + r >> 1;
work(l, mid), work(mid + 1, r);
int lf = l, rt = mid + 1, now = l;
while (lf <= mid && rt <= r) {
if (a[lf].b <= a[rt].b) add(a[lf].c, a[lf].cnt), q.push(lf), tmp[now++] = a[lf++];
else a[rt].ans += query(a[rt].c), tmp[now++] = a[rt++];
}
while (lf <= mid) tmp[now++] = a[lf++];
while (rt <= r) a[rt].ans += query(a[rt].c), tmp[now++] = a[rt++];
while (q.size()) {
int x = q.front(); q.pop();
add(a[x].c, -a[x].cnt);
}
for (int i = l; i <= r; i++) a[i] = tmp[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 + 1 + n, cmp);
int m = n; n = 0;
for (int i = 1; i <= m; i++) {
if (a[i].a == a[n].a && a[i].b == a[n].b && a[i].c == a[n].c) a[n].cnt++;
else a[++n] = a[i];
}
work(1, n);
for (int i = 1; i <= n; i++) ans[a[i].ans + a[i].cnt - 1] += a[i].cnt;
for (int i = 0; i < m; i++) printf("%d\n", ans[i]);
return 0;
}