CDQ分治学习笔记
作者:互联网
CDQ分治
用于解决偏序问题。
《算法竞赛进阶指南》中,称CDQ分治为“基于时间的分治算法”,其实是偏序问题的一种特殊形式。
二维偏序
在学习线段树和树状数组时,已经可以利用排序+数据结构 \(O(N\log{N})\) 解决二维偏序问题。同样,CDQ分治也行。
有 \(N\) 个元素,每个元素有 \(a,b,c\) 三个属性。求对于每个元素 \(i\),满足 \(a_j≤a_i\) 且 \(b_j≤b_i\) 且 \(j≠i\) 的 \(j\) 的数量。
先将所有元素按 \(a\) 为第一关键字,\(b\) 为第二关键字排序。再在排序好的数组上进行以 \(b\) 为关键字的归并排序,并在归并时进行统计。
具体来说,将数组分成两段,对两段分别进行归并排序。此步骤结束后,要求每段中 \(b\) 有序。而由于原数组已经按照 \(a\) 排序,所以左段中的 \(a\) 一定小于等于右段中的 \(a\)。那么利用双指针可以 \(O(N)\) 求出:对于右段的每个数,左段中有多少个数 \(b\) 不大于它。
是不是感觉思路很像求逆序对?实际上逆序对也是二维偏序问题,而且其中一维(下标)已经有序。
另外,关于两个元素相等的情况,如上的算法显然会漏算。解决的方法也非常简单,只需将相等的元素合并,做上标记,更新答案时特殊处理即可。
三维偏序
如果真正理解了二维偏序,三维偏序也很好解决。套上一个树状数组即可。
将所有元素按 \(a\) 为第一关键字,\(b\) 为第二关键字,\(c\) 为第三关键字排序。再在排序好的数组上进行以 \(b\) 为关键字的归并排序。在归并的过程中维护以 \(c\) 为“下标”,“权值”为出现次数的树状数组,并利用其性质更新答案。
可以自己验证其正确性。
模板题
Code
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e5 + 5, M = 2e5 + 5;
int n, m, t[M], ans[N];
struct Node {
int a, b, c, cnt, res;
bool operator <(const Node &o) const {
return a != o.a ? a < o.a : (b != o.b ? b < o.b : c < o.c);
}
bool operator ==(const Node &o) const {
return a == o.a && b == o.b && c == o.c;
}
} x[N], tmp[N];
int read() {
int x = 0; char c = getchar();
while (c < '0' || c > '9') c = getchar();
while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
return x;
}
void Init() {
sort(x + 1, x + n + 1);
int j = 0;
for (int i = 1; i <= n; i++)
if (j && x[i] == x[j]) x[j].cnt++;
else x[++j] = x[i];
n = j;
}
void Add(int x, int v) {
for (; x <= m; x += x & -x) t[x] += v;
}
int Ask(int x) {
int res = 0;
for (; x; x -= x & -x) res += t[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 = l;
while (i <= mid && j <= r)
if (x[i].b <= x[j].b) {
Add(x[i].c, x[i].cnt); tmp[k++] = x[i++];
}
else {
x[j].res += Ask(x[j].c); tmp[k++] = x[j++];
}
while (i <= mid) {
Add(x[i].c, x[i].cnt); tmp[k++] = x[i++];
}
while (j <= r) {
x[j].res += Ask(x[j].c); tmp[k++] = x[j++];
}
for (i = l; i <= mid; i++) Add(x[i].c, -x[i].cnt); //memset很慢,减回去更快
for (i = l; i <= r; i++) x[i] = tmp[i];
}
int main() {
int s;
s = n = read(); m = read();
for (int i = 1; i <= n; i++) x[i] = (Node){read(), read(), read(), 1};
Init();
CDQ(1, n);
for (int i = 1; i <= n; i++) ans[x[i].res + x[i].cnt - 1] += x[i].cnt;
for (int i = 0; i < s; i++) printf("%d\n", ans[i]);
return 0;
}
基于时间的分治算法
将时间(时间戳)看作一个维度。对查询操作产生影响的修改操作一定在其之前,可以发现有偏序的性质。那么许多在线问题可以转化为离线,利用CDQ分治求解。
例:树状数组模板
先将区间求和转化为求前缀和。加入时间维度,可以发现问题变成了二维偏序,分别为“时间”和“下标”。另外,其实此题还有一个隐藏的维度 \(k\),表示操作种类。设修改为0,查询为1,则可以抽象成更具普适性的三维偏序。然而此题 \(k\) 只有两种取值,无需再维护树状数组,只需朴素地判断即可。做法同二维偏序。
点击查看代码
#include<cstdio>
using namespace std;
const int N = 1e5 + 5;
int n, m, ans[N], q;
struct Node {
int k, p, v, id;
bool operator <(const Node &oth) const {
return p != oth.p ? p < oth.p : k < oth.k;
}
} a[3 * N], tmp[3 * N];
int read() {
int x = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
return x * f;
}
void CDQ(int l, int r) {
if (l == r) return ;
int mid = l + r >> 1;
CDQ(l, mid); CDQ(mid + 1, r);
int sum = 0, i = l, j = mid + 1, tot = l;
while (i <= mid && j <= r)
if (a[i] < a[j]) {
if (a[i].k == 1) sum += a[i].v;
tmp[tot++] = a[i++];
}
else {
if (a[j].k == 2) ans[a[j].id] += sum * a[j].v;
tmp[tot++] = a[j++];
}
while (i <= mid) tmp[tot++] = a[i++];
while (j <= r) {
if (a[j].k == 2) ans[a[j].id] += sum * a[j].v;
tmp[tot++] = a[j++];
}
for (i = l; i <= r; i++) a[i] = tmp[i];
}
int main() {
n = read(); m = read();
for (int i = 1; i <= n; i++) a[i] = (Node){1, i, read()};
for (int i = 1; i <= m; i++) {
int opt = read(), x = read(), y = read();
if (opt == 1) a[++n] = (Node){1, x, y};
else {
a[++n] = (Node){2, y, 1, ++q};
a[++n] = (Node){2, x - 1, -1, q};
}
}
CDQ(1, n);
for (int i = 1; i <= q; i++) printf("%d\n", ans[i]);
return 0;
}
同样,可以再加一维,变成四维(三维)偏序。例:莫基亚
点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 2e5 + 5, M = 5e5 + 5;
int n, m, t[M], idx, ans[N];
struct Node {
int k, x, y, t, v, id;
bool operator <(const Node &o) const {
return t != o.t ? t < o.t : x != o.x ? x < o.x : y != o.y ? y < o.y : k < o.k;
}
} q[N << 2], tmp[N << 2];
int read() {
int x = 0; char c = getchar();
while (c < '0' || c > '9') c = getchar();
while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
return x;
}
void Add(int x, int v) {
for (; x <= m; x += x & -x) t[x] += v;
}
int Ask(int x) {
int res = 0;
for (; x; x -= x & -x) res += t[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 = l;
while (i <= mid && j <= r)
if (q[i].x <= q[j].x) {
if (!q[i].k) Add(q[i].y, q[i].v);
tmp[k++] = q[i++];
}
else {
if (q[j].k) ans[q[j].id] += Ask(q[j].y) * q[j].v;
tmp[k++] = q[j++];
}
while (i <= mid) {
if (!q[i].k) Add(q[i].y, q[i].v);
tmp[k++] = q[i++];
}
while (j <= r) {
if (q[j].k) ans[q[j].id] += Ask(q[j].y) * q[j].v;
tmp[k++] = q[j++];
}
for (i = l; i <= mid; i++)
if (!q[i].k) Add(q[i].y, -q[i].v);
for (i = l; i <= r; i++) q[i] = tmp[i];
}
int main() {
int opt;
m = read();
while (scanf("%d", &opt), opt != 3) {
if (opt == 1) q[++n] = (Node){0, read(), read(), n, read()};
else {
int x1 = read(), y1 = read(), x2 = read(), y2 = read();
q[++n] = (Node){1, x2, y2, n, 1, ++idx};
q[++n] = (Node){1, x2, y1 - 1, n, -1, idx};
q[++n] = (Node){1, x1 - 1, y2, n, -1, idx};
q[++n] = (Node){1, x1 - 1, y1 - 1, n, 1, idx};
}
}
sort(q + 1, q + n + 1);
CDQ(1, n);
for (int i = 1; i <= idx; i++) printf("%d\n", ans[i]);
return 0;
}
CDQ分治的思想还是挺简单的。
标签:偏序,int,分治,mid,笔记,CDQ,数组,排序 来源: https://www.cnblogs.com/Fisher-Y/p/16125895.html