整体二分 & CDQ 分治
整体二分
所需性质
能解决的问题需要有以下性质:
- 可以离线。
- 可以二分。
- 修改对判定答案的贡献互相独立,修改之间互不影响效果。
- 修改如果对判定答案有贡献,则贡献为一确定的与判定标准无关的值。
- 贡献满足交换律,结合律,具有可加性。
——许昊然《浅谈数据结构题几个非经典解法》
流程
先给询问 / 修改按时间顺序打上编号,定义函数 \(\operatorname{solve}(l, r, L, R)\) 表示当前操作区间为 \([l, r]\) 时,答案值域在 \([L, R]\),考虑取 \(mid = \lfloor\dfrac{L + R}{2}\rfloor\),将 \([l, r]\) 统计出的答案与 \(mid\) 的关系分为两类(\(\leq mid\) 和 \(> mid\) 两类),并分别递归处理,当 \(L=R\) 时 \([l, r]\) 的答案均为 \(L\) 并结束递归。
时间复杂度为 \(\mathcal O(T\log A)\),其中 \(A\) 为答案范围,\(T\) 为单次处理的时间复杂度。
题目链接
// 大概框架
inline void solve(int l, int r, int L, int R)
{
if (l > r || L > R) return;
if (L == R)
{
// L = R 表示 [l, r] 区间的询问答案已经得出
for (int i = l; i <= r; ++ i)
...
return;
}
int cnt1 = 0, cnt2 = 0;
int mid = (L + R) / 2;
for (int i = l; i <= r; ++ i)
{
// 划分为两类
...
}
for (int i = l; i <= r; ++ i)
{
...
// 删去记录
}
// 分为两类
for (int i = 1; i <= cnt1; ++ i)
que[l + i - 1] = leq[i];
for (int i = 1; i <= cnt2; ++ i)
que[l + cnt1 + i - 1] = big[i];
// 递归处理
solve(l, l + cnt1 - 1, L, mid);
solve(l + cnt1, r, mid + 1, R);
}
CDQ 分治
解决的问题大致分为三类:
- 解决和点对有关的问题。
- 1D 动态规划的优化与转移。
- 通过 CDQ 分治,将一些动态问题转化为静态问题。
CDQ 分治的思想最早由 IOI2008 金牌得主陈丹琦在高中时整理并总结,它也因此得名。
———— oi-wiki
问题类似于:给定一个长度为 \(n\) 的区间,统计一些特殊的点对 \((i, j)\) 或找到一对有某种意义的 \((i, j)\)。
流程如下:
- 找到这个序列的中点 \(mid\)。
- 将点对 \((i, j)\) 分为三类
a. \(1 \leq i \leq mid, 1 \leq j \leq mid\)
b. \(mid < i \leq n, mid < j \leq n\)
c. \(1 \leq i \leq mid, mid < j \leq n\) - 将序列 \([1, n]\) 拆成 \([1, mid]\) 和 \([mid + 1, n]\) 发现 a.b. 的点对在这两个区间之内。
- 递归处理。
- 处理 c. 的点对。
我们发现重点就是在第 5. 步,只要维护好了 5.,也就维护好了整个序列的点对。
具体来说,我们设 \(\operatorname {solve}(l, r)\) 表示处理 \([l, r]\) 之间的点对,每次不断递归处理 \(\operatorname{solve}(l, mid)\) 和 \(\operatorname{solve}(mid + 1, r)\),最后将 c. 的点对处理好即可。
// 大概框架如下
inline void cdq(int l, int r)
{
if (l == r) return;
int mid = (l + r) >> 1;
cdq(l, mid), cdq(mid + 1, r);
...
}