CDQ分治
其本质是对分治的进一步理解。
先来看一个问题。
二维偏序
给定
即求满足
很容易想到以
现在我们将该问题升高难度
三维偏序
有
对于
我们发现使用排序和树状数组处理前两位后,最后一位无法处理。
这是我们要用到 CDQ分治。
CDQ分治
假设当前我们在处理
也就是说,我们现在需要在两个子区间中各选一个元素,并计算结果。
由于我们在分治前将原序列以
我们再将两个子区间按照
大致长这样:
// 排序
sort(v + l, v + mid + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
sort(v + mid + 1, v + r + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
// 双指针
int j = l;
for (int i = mid + 1; i <= r; i++) {
for (; j <= mid && v[j].y <= v[i].y; j++) {
Modify(v[j].z, 1); // 树状数组修改
}
v[i].res += Query(v[i].z); // 累加答案
}
但是当前区间所进行的修改会对其父亲区间产生后效,所以我们需要将它撤回(或者清空)。
for (j--; j >= l; j--) {
Modify(v[j].z, -1);
}
还有一点,由于处理当前区间前先处理两个子区间后在进行了排序,所以子区间的排序不会对当前区间产生影响。
整个代码长这样:
void Cdq(int l, int r) {
if (l == r) return; // 边界,只有一个元素,不会产生答案,直接返回
int mid = (l + r) >> 1;
// 先处理好子区间
Cdq(l, mid);
Cdq(mid + 1, r);
sort(v + l, v + mid + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
sort(v + mid + 1, v + r + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
int j = l;
for (int i = mid + 1; i <= r; i++) {
for (; j <= mid && v[j].y <= v[i].y; j++) {
Modify(v[j].z, v[j].s);
}
v[i].res += Query(v[i].z);
}
// 撤回当前区间进行的操作
for (j--; j >= l; j--) {
Modify(v[j].z, -v[j].s);
}
}
纵观整个过程,我们处理的是
至于为什么不直接在分治里取等,原因大致是分类情况太多了。(猜的)
完整代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int kmax = 2e5 + 3;
// 元素,包含三维,数量,结果
struct V {
int x, y, z, s;
int res;
} v[kmax];
int n, m, k;
int c[kmax];
long long res[kmax];
int Lowbit(int x) {
return x & -x;
}
void Modify(int x, int v) {
for (; x < kmax; x += Lowbit(x)) {
c[x] += v;
}
}
int Query(int x) {
int tot = 0;
for (; x; x -= Lowbit(x)) {
tot += c[x];
}
return tot;
}
void Cdq(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
Cdq(l, mid);
Cdq(mid + 1, r);
sort(v + l, v + mid + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
sort(v + mid + 1, v + r + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
int j = l;
for (int i = mid + 1; i <= r; i++) {
for (; j <= mid && v[j].y <= v[i].y; j++) {
Modify(v[j].z, v[j].s); // 注意现在修改的值是该元素的数量,不一定是1
}
v[i].res += Query(v[i].z);
}
for (j--; j >= l; j--) {
Modify(v[j].z, -v[j].s);
}
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &v[i].x, &v[i].y, &v[i].z);
}
// 以x为第一关键字排序
sort(v + 1, v + n + 1, [](V p, V q) { return p.x < q.x || p.x == q.x && p.y < q.y || p.x == q.x && p.y == q.y && p.z < q.z; });
// 元素去重
for (int i = 1; i <= n; i++) {
if (v[i].x != v[m].x || v[i].y != v[m].y || v[i].z != v[m].z) {
v[++m] = {v[i].x, v[i].y, v[i].z, 1};
} else {
v[m].s++;
}
}
// 分治
Cdq(1, m);
// 统计最终答案
for (int i = 1; i <= m; i++) {
int num = v[i].res + v[i].s - 1;
res[num] += v[i].s;
}
for (int i = 0; i < n; i++) {
printf("%lld\n", res[i]);
}
return 0;
}
接下来我们看几道练习题
Mokia
我们尝试将一次询问的区间拆分成四个小询问。即对于一组询问
我们发现每组小询问的起始点都是
然后由于加入了修改,我们还要考虑时间这一维,总共就是三维。
不需要去重,直接套用分治即可。
注意
代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int kmax = 2e6 + 3;
struct V {
int t, x, y;
int op, res;
} p[kmax];
int n, pc;
int c[kmax];
int Lowbit(int x) { return x & -x; }
void Modify(int x, int v) {
for (; x <= n; x += Lowbit(x)) {
c[x] += v;
}
}
int Query(int x) {
int tot = 0;
for (; x; x -= Lowbit(x)) {
tot += c[x];
}
return tot;
}
void Cdq(int l, int r) {
if (l == r)
return;
int mid = (l + r) >> 1;
Cdq(l, mid), Cdq(mid + 1, r);
sort(p + l, p + mid + 1, [](V p, V q) { return p.x < q.x || p.x == q.x && p.y < q.y; });
sort(p + mid + 1, p + r + 1, [](V p, V q) { return p.x < q.x || p.x == q.x && p.y < q.y; });
int j = l;
for (int i = mid + 1; i <= r; i++) {
for (; j <= mid && p[j].x <= p[i].x; j++) {
if (!p[j].op) {
Modify(p[j].y, p[j].res);
}
}
if (p[i].op) {
p[i].res += Query(p[i].y);
}
}
for (j--; j >= l; j--) {
if (!p[j].op) {
Modify(p[j].y, -p[j].res);
}
}
}
int main() {
scanf("%d%d", &n, &n);
n++;
for (int op, x, y, _x, _y, v; scanf("%d", &op);) {
if (op == 3)
break;
if (op == 1) {
scanf("%d%d%d", &x, &y, &v);
p[++pc] = {pc, ++x, ++y, 0, v};
} else {
scanf("%d%d%d%d", &x, &y, &_x, &_y);
// 拆分成四个元素
p[++pc] = {pc, x, y, 1}, p[++pc] = {pc, ++_x, ++_y, 1};
p[++pc] = {pc, _x, y, 1}, p[++pc] = {pc, x, _y, 1};
}
}
Cdq(1, pc);
// 以时间排序
sort(p + 1, p + pc + 1, [](V p, V q) { return p.t < q.t; });
for (int i = 1; i <= pc; i++) {
if (!p[i].op)
continue;
printf("%d\n", p[i].res + p[++i].res - p[++i].res - p[++i].res);
// 四个元素的答案统一到一起
}
return 0;
}
CQOI2011 动态逆序对
我们发现=只要求出初始逆序对,然后每次计算出减少的逆序对数量即可。
对于每一个被删的元素, 其减少的逆序对数量由两部分组成。
-
位于该元素前面,删除时间比该元素晚,权值比该元素大的元素数量。
-
位于该元素后面,删除时间比该元素晚,权值比该元素小的元素数量。
由此不难得出一个元素三个维度的属性,分别是位置、删除时间、权值。
使用CDQ分治即可。
代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int kmax = 1e5 + 3;
struct Node {
int e, v, d;
int o, t;
} c[kmax << 1];
int n, m, ct;
int p[kmax], a[kmax], t[kmax];
long long ans[kmax];
int Lowbit(int x) { return x & (-x); }
void Update(int x, int k) {
for (; x <= n; x += Lowbit(x)) {
t[x] += k;
}
}
int Query(int x) {
int tot = 0;
for (; x; x -= Lowbit(x)) {
tot += t[x];
}
return tot;
}
void Cdq(int l, int r) {
if (l == r)
return;
int mid = (l + r) >> 1;
int j = l;
Cdq(l, mid);
Cdq(mid + 1, r);
sort(c + l, c + mid + 1, [](Node p, Node q) { return p.d < q.d; });
sort(c + mid + 1, c + r + 1, [](Node p, Node q) { return p.d < q.d; });
for (int i = mid + 1; i <= r; i++) {
for (; j <= mid && c[j].d <= c[i].d; j++) {
Update(c[j].v, c[j].e);
}
ans[c[i].o] += c[i].e * (Query(n) - Query(c[i].v));
}
for (int i = l; i < j; i++) {
Update(c[i].v, -c[i].e);
}
j = mid;
for (int i = r; i > mid; i--) {
for (; j >= l && c[j].d >= c[i].d; j--) {
Update(c[j].v, c[j].e);
}
ans[c[i].o] += c[i].e * Query(c[i].v - 1);
}
for (int i = mid; i > j; i--) {
Update(c[i].v, -c[i].e);
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
p[a[i]] = i;
ct++;
c[ct] = { 1, a[i], i, 0, ct };
}
for (int i = 1, x; i <= m; i++) {
cin >> x;
ct++;
c[ct] = { -1, x, p[x], i, ct };
}
Cdq(1, ct);
for (int i = 0; i < m; i++) {
if (i)
ans[i] += ans[i - 1];
cout << ans[i] << '\n';
}
return 0;
}
LIS2
先看看普通的
对于当前考虑的位置
我们尝试用树状数组维护。(我们不会删除元素,故最大值不会减少)
复杂度
但此题是二维的,我们可以尝试分治。
与普通的分治不同的是,我们这里要先处理左子区间,再处理当前区间,最后处理右子区间。因为 dp 数组不能记录当前的答案从哪一位置转移得到,我们只知道这个答案以当前元素结尾。
反之,如果处理完右子区间后在合并处理当前区间,右子区间的答案会算重。
实现细节:
- 离散化。
- 树状数组撤回操作改清空。
- 左
当前 右的顺序。
代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int kmax = 1e5 + 3;
struct V {
int x, y, id;
} p[kmax];
int n, f[kmax];
int b[kmax], m;
int c[kmax];
int res;
int Lowbit(int x) { return x & -x; }
void Modify(int x, int v) {
for (; x <= m; x += Lowbit(x)) {
c[x] = max(c[x], v);
}
}
void Clear(int x) {
for (; x <= m; x += Lowbit(x)) {
c[x] = 0;
}
}
int Query(int x) {
int tot = 0;
for (; x; x -= Lowbit(x)) {
tot = max(tot, c[x]);
}
return tot;
}
void Cdq(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
Cdq(l, mid);
sort(p + l, p + mid + 1, [](V p, V q) { return p.x < q.x; });
sort(p + mid + 1, p + r + 1, [](V p, V q) { return p.x < q.x; });
int j = l;
for (int i = mid + 1; i <= r; i++) {
for (; j <= mid && p[j].x < p[i].x; j++) {
Modify(p[j].y, f[p[j].id]);
}
f[p[i].id] = max(f[p[i].id], Query(p[i].y - 1) + 1);
}
for (j--; j >= l; j--) {
Clear(p[j].y);
}
sort(p + mid + 1, p + r + 1, [](V p, V q) { return p.id < q.id; });
Cdq(mid + 1, r);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &p[i].x, &p[i].y);
p[i].id = i;
}
for (int i = 1; i <= n; i++) {
f[i] = 1;
b[i] = p[i].y;
}
sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - b - 1;
for (int i = 1; i <= n; i++) {
p[i].y = lower_bound(b + 1, b + m + 1, p[i].y) - b;
}
Cdq(1, n);
for (int i = 1; i <= n; i++) {
res = max(res, f[i]);
}
printf("%d\n", res);
return 0;
}
斜率升级
初始有一个空集,有
-
x, y
:向点集中加入一个点 。 -
k
:询问一个 ,求当前点集中,最大的 。
CDQ + 斜率即可, 二分查找答案。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <stack>
using namespace std;
const int kmax = 1e5 + 3;
struct V {
int x, y, id;
long long res;
int op;
} v[kmax], stk[kmax];
int n, tp;
// 计算斜率
double Slope(V p, V q) {
return 1.0 * (q.y - p.y) / (q.x - p.x);
}
// 计算结果
long long Calc(int x, int k) {
return 1ll * k * stk[x].x + stk[x].y;
}
bool Check(V p, V q, int k) {
return 1.0 * (q.y - p.y) / (q.x - p.x) >= k;
}
void Cdq(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
Cdq(l, mid), Cdq(mid + 1, r);
tp = 0;
sort(v + l, v + mid + 1, [](V p, V q) { return p.x < q.x || p.x == q.x && p.y < q.y; });
for (int i = l; i <= mid; i++) {
if (v[i].op) continue;
for (; tp >= 2 && Slope(stk[tp], v[i]) >= Slope(stk[tp - 1], stk[tp]); tp--) {
}
stk[++tp] = v[i];
}
if (!tp) return;
for (int i = mid + 1, _l, _r; i <= r; i++) {
if (!v[i].op) continue;
// 二分查找答案
for (_l = 1, _r = tp - 1; _l <= _r;) {
int mid = (_l + _r) >> 1;
if (Calc(mid, v[i].x) >= Calc(mid + 1, v[i].x)) {
_r = mid - 1;
} else {
_l = mid + 1;
}
}
v[i].res = max(v[i].res, Calc(_r + 1, v[i].x));
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &v[i].op, &v[i].x);
v[i].id = i;
if (--v[i].op == 0) {
scanf("%d", &v[i].y);
}
}
// sort(v + 1, v + n + 1, [](V p, V q) { return p.op < q.op || p.op == q.op && p.x < q.x; });
Cdq(1, n);
sort(v + 1, v + n + 1, [](V p, V q) { return p.id < q.id; });
for (int i = 1; i <= n; i++) {
if (!v[i].op) continue;
printf("%lld\n", v[i].res);
}
return 0;
}
完结撒花 \ / \ / \ /
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】