cdq 分治
概念
cdq 分治是一种分治思想,用于处理序列中的点对关系等。
cdq 分治的主要思路是:
对于区间 \([l, r]\) 内的点对,记 \(m = \lfloor \frac{l + r}{2} \rfloor\),考虑:
-
在 \([l, m]\) 内的点对
-
横跨 \([l, m]\) 和 \((r, m]\) 的点对
-
在 \((r, m]\) 内的点对
其中 1.3. 可以递归处理,主要考虑的是维护2。通常情况下使用 树状数组/线段树 等数据结构维护。
例题
维护序列内点对个数
通常用于维护序列内满足特定条件的点对。
例:三维偏序。
对于考虑维护区间 \([l, r]\) 内的点对。
考虑区间 \([l, r]\) 内的点对。
沿用二维偏序的思路,同时考虑第三维的维护。首先将所有元素按 \(a\) 升序排序。如果我们考虑第 2 类点对,发现在区间左右两半的元素互不影响。即我们可以对两边的元素进行任意排序,因此考虑对于两边的元素分别按照 \(b\) 升序排序。
由于区间两半各满足单调性,因此可以考虑对区间的两半分别维护一个指针。具体地,令指针从对应的起点开始向右扫描,记左半部分的指针为 \(j\),右半部分的指针为 \(i\)。当 \(b_j \leq b_i\) 时,由于初始时按 \(a\) 升序排序,所以 \(a_j \leq a_i\)。又有 \(\forall l \leq k \leq j, a_k \leq a_i, b_k \leq b_j \leq b_i\),因此问题转化为求 \(k \in [l, j]\) 且 \(c_k \leq c_i\) 的 \(k\) 的个数。
容易联想到二维偏序。于是维护一个值域树状数组,每次 \(j\) 向右扫时用当前值的个数更新树状数组中 \(c_j\) 处的下标。查询时直接查询 \([1, c_i]\) 的总和即为所求。
有的题目可能需要离散化。
bool cmp2(int x, int y)
{
if (b[x] != b[y]) return b[x] < b[y];
return c[x] < c[y];
}
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, cmp2);
sort(p + mid + 1, p + r + 1, cmp2);
int i = mid + 1, j = l;
while (i <= r)
{
while ((j <= mid) && (b[p[i]] >= b[p[j]])) update(c[p[j]], v[p[j]]), j++;
cnt[p[i]] += query(c[p[i]]), i++;
}
for (i = l; i < j; i++) update(c[p[i]], -v[p[i]]);
}
\(T(n) = T(\lfloor \frac{n}{2} \rfloor) + T(\lceil \frac{n}{2} \rceil) + n \log n = O(n \log^2 n)\)
优化特殊 1D/1D 动态规划
优化形如 \(f[i] = \max_{j = 1}^{i - 1} (f[j] \cdot [a_j \leq a_i] \cdot [b_j \leq b_i])\) 的动态规划。
例:优化最长不升子序列。
动态转静态
将一些数据结构问题从动态修改转化成静态维护。
要求:
-
可以离线。
-
修改之间互相独立,即先前的修改不会对后面修改的贡献造成影响。
对于询问的点 \((x_i, y_i)\),不妨先只考虑在其左下方的点 \((x_j, y_j)\),然后通过旋转平面直角坐标系求出左下、右上、右下的贡献。
容易发现此时点 \(j\) 到点 \(i\) 的距离为 \((x_i + y_i) - (x_j + y_j)\),即我们要维护修改的点中最大的横纵坐标之和。
容易发现修改之间相互独立,并且修改对于询问的贡献次数是 \(O(n^2)\) 的,因此可以考虑用 cdq 分治优化。
具体地,对于第 \(l\) 次操作到第 \(r\) 次操作(含修改和询问),考虑:
-
\([l, m]\) 中的修改 -> \([l, m]\) 中的询问。
-
\([l, m]\) 中的修改 -> \((r, m]\) 中的询问。
-
\((r, m]\) 中的修改 -> \((r, m]\) 中的询问。
对于 2,容易发现此时维护的是满足 \(j \in [l, m], i \in (m, r]\) 且 \(j < i, x_j \leq x_i, y_j \leq y_i\) 的点对关系,于是考虑用三维偏序的方法处理。
时间复杂度 \(O(n \log^2 n)\)
void cdq(int l, int r)
{
if (l == r) return;
int mid = (l + r) >> 1;
cdq(l, mid);
cdq(mid + 1, r);
sort(q + l, q + mid + 1);
sort(q + mid + 1, q + r + 1);
int i = mid + 1, j = l;
while (i <= r)
{
while ((j <= mid) && (q[j].x <= q[i].x))
{
if (q[j].opt == 1) update(q[j].y, q[j].x + q[j].y);
j++;
}
if (q[i].opt == 2)
{
int v = query(q[i].y);
if (v) ans[q[i].idx] = min(ans[q[i].idx], q[i].x + q[i].y - v);
}
i++;
}
for (i = l; i <= mid; i++)
if (q[i].opt == 1) clear(q[i].y);
}
四维偏序
cdq 套 cdq,理论上可以一直套下去,五维偏序或者以上也是可以做的,每套一层 cdq 就会多 \(O(\log n)\) 的复杂度。
例题:【HDU5126 Stars】
考虑套两层 cdq.
这里钦定每个元素的第一维是时间轴,后三维对应点的坐标。
- 将所有元素按照第一维排序。
这里因为第一维是时间轴不用排序。
-
第一层 cdq.
-
考虑先分治左右两半区间,使得子区间的第二维坐标有序。
-
考虑对于右半区间的每一个询问,处理出左半区间中能影响到它的询问,按序 排好。
-
考虑对排好的操作进行第二层 cdq 分治,无有效操作则不操作。
-
按第二维坐标归并合并当前区间。
-
-
第二层 cdq.
-
考虑分治左右两半区间,使得子区间的第三维坐标有序。
-
考虑对于右半区间的每一个询问,用树状数组维护左半区间中可以影响到它的修改的贡献(维护第四维坐标)。
-
使用树状数组维护询问答案。
-
撤销左半区间在树状数组中的影响。
-
按第三维坐标归并合并当前区间。
-
另外有一种写法是考虑到:只有两次都被划分到左半区间的修改 可以影响 两次都被划分到右半区间的修改。于是考虑对于元素进行标记,然后在第二层 cdq 中相应处理。
这种写法常数极大,可以简单卡掉,但时间复杂度也是 \(T(n) = O(n \log n) + 2 T(\frac{n}{2}) = O(n \log n)\).
时间复杂度是 \(T(n) = O(n) + 2 T(\frac{n}{2}) = O(n \log^2 n)\).
#include <cstdio>
#include <algorithm>
using namespace std;
#define il inline
const int maxq = 5e4 + 5;
const int opt_sz = 5e5 + 5;
struct Query
{
int x, y, z, t, coe, idx;
} nd[opt_sz], s1[opt_sz], s2[opt_sz];
int q;
int ans[maxq];
int tmp[opt_sz];
namespace BIT
{
int c[opt_sz];
il int lowbit(int x) { return x & (-x); }
il void update(int p, int w) { for (int i = p; i <= (q << 3); i += lowbit(i)) c[i] += w; }
il int query(int p) { int res = 0; for (int i = p; i; i -= lowbit(i)) res += c[i]; return res; }
}
il int read()
{
int res = 0;
char ch = getchar();
while ((ch < '0') || (ch > '9')) ch = getchar();
while ((ch >= '0') && (ch <= '9')) res = res * 10 + ch - '0', ch = getchar();
return res;
}
il void write(int x)
{
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
il bool cmp_x(const Query& a, const Query& b)
{
if (a.x != b.x) return (a.x < b.x);
if (a.y != b.y) return (a.y < b.y);
if (a.z != b.z) return (a.z < b.z);
return (a.t < b.t);
}
il bool cmp_y(const Query& a, const Query& b)
{
if (a.y != b.y) return (a.y < b.y);
if (a.z != b.z) return (a.z < b.z);
if (a.t != b.t) return (a.t < b.t);
return (a.x < b.x);
}
il bool cmp_z(const Query& a, const Query& b)
{
if (a.z != b.z) return (a.z < b.z);
if (a.t != b.t) return (a.t < b.t);
if (a.x != b.x) return (a.x < b.x);
return (a.y < b.y);
}
il void cdq2(int l, int r)
{
if (l >= r) return;
int mid = (l + r) >> 1;
cdq2(l, mid), cdq2(mid + 1, r);
int i = mid + 1, j = l;
for ( ; i <= r; i++)
{
if (!s1[i].coe) continue;
for ( ; (j <= mid) && (s1[j].y <= s1[i].y); j++)
if (!s1[j].coe) BIT::update(s1[j].z, 1);
ans[s1[i].idx] += s1[i].coe * BIT::query(s1[i].z);
}
for (i = l; i < j; i++)
if (!s1[i].coe) BIT::update(s1[i].z, -1);
int k = mid + 1; j = l;
for (i = l; i <= r; i++)
if ((k > r) || ((j <= mid) && (s1[j].y <= s1[k].y))) s2[i] = s1[j++];
else s2[i] = s1[k++];
for (i = l; i <= r; i++) s1[i] = s2[i];
}
il void cdq1(int l, int r)
{
if (l >= r) return;
int mid = (l + r) >> 1, len = 0;
cdq1(l, mid), cdq1(mid + 1, r);
int i = mid + 1, j = l;
for ( ; i <= r; i++)
{
if (!nd[i].coe) continue;
for ( ; (nd[j].x <= nd[i].x) && (j <= mid); j++)
if (!nd[j].coe) s1[++len] = nd[j];
s1[++len] = nd[i];
}
if (len) cdq2(1, len);
int k = mid + 1; j = l;
for (int i = l; i <= r; i++)
if ((k > r) || ((j <= mid) && nd[j].x <= nd[k].x)) s1[i] = nd[j++];
else s1[i] = nd[k++];
for (int i = l; i <= r; i++) nd[i] = s1[i];
}
int main()
{
q = read();
int len = 0, cnt = 0;
for (int i = 1, opt, x, y, z, X, Y, Z; i <= q; i++)
{
opt = read();
if (opt == 1)
{
ans[i] = -1;
cnt++, x = read(), y = read(), tmp[++cnt] = z = read();
len++, nd[len] = (Query){x, y, z, len, 0, i};
}
else
{
cnt++, x = read() - 1, y = read() - 1, tmp[++cnt] = z = read() - 1;
cnt++, X = read(), Y = read(), tmp[++cnt] = Z = read();
len++, nd[len] = (Query){x, y, z, len, -1, i};
len++, nd[len] = (Query){X, y, z, len, 1, i};
len++, nd[len] = (Query){x, Y, z, len, 1, i};
len++, nd[len] = (Query){x, y, Z, len, 1, i};
len++, nd[len] = (Query){x, Y, Z, len, -1, i};
len++, nd[len] = (Query){X, y, Z, len, -1, i};
len++, nd[len] = (Query){X, Y, z, len, -1, i};
len++, nd[len] = (Query){X, Y, Z, len, 1, i};
}
}
sort(tmp + 1, tmp + cnt + 1);
int cur = unique(tmp + 1, tmp + cnt + 1) - tmp - 1;
for (int i = 1; i <= len; i++) nd[i].z = lower_bound(tmp + 1, tmp + cur + 1, nd[i].z) - tmp;
cdq1(1, len);
for (int i = 1; i <= q; i++)
if (ans[i] != -1) write(ans[i]), putchar('\n');
return 0;
}