CDQ分治
它是离线的一种分治算法
每次计算分治中线左边的对右边的贡献或是范围内顺序在前的对顺序在后面的贡献
注意计算顺序,考虑是先分治后计算还是先计算后分治
一般是前一种,但如果计算后面某部分时要用到先计算的结果,就必须先计算后分治
之所以把它归类到数据结构,是因为其实它分治的函数可以看作树套树的外层树,内层再嵌套 CDQ 或直接用数据结构维护
可以部分替代树套树,优点是空间占用及常数较小,缺点是必须离线
想不清楚递归的时候就相信自己的程序能完成计算需要的数据,只考虑当前或干脆想象成树套树分析
偏序问题
第一维:直接排好序
后面维:CDQ 嵌套,也可以用数据结构维护最后一维
分治的 CDQ:
-
先分治
-
然后将两边按处理的这维的大小顺序归并排序,如果来自左边则打上标记,标记计算时它可以对后面的产生贡献,因为来自左边保证了前面的维满足,归并排序满足了当前维
-
更新数组
-
进入下一层 CDQ
统计答案的 CDQ:
比较类似
-
先分治
-
然后将两边按最终这维的大小顺序归并排序,如果来自左边且有标记则记录标记信息,如果来自右边且无标记则更新答案
-
更新数组
注意每次归并前的数组都满足两边按照当前维大小排序,右边的每个和左边的任意一个在前面维都形成偏序
【模板】三维偏序(陌上花开)
这里一定要去重,因为排序时还是把一样的排出了先后顺序,万一它们跨过了中线会出现右边更新左边的情况,就会错
这里的统计答案的 CDQ 可以用树状数组代替,不过用数据结构一定记得清零
代码:
struct node
{
int a, b, c, cnt, book, id;
bool operator<(const node &d)const
{
if(a != d.a) return a < d.a;
else if(b != d.b) return b < d.b;
return c < d.c;
}
}x[N], y[N], z[N], p[N];
inline void solve(int l, int r)
{
if(l == r) return;
int mid = (l + r) >> 1, num = 0;
solve(l, mid), solve(mid + 1, r);
for(int i = l, j = l, k = mid + 1; i <= r; ++i)
{
if((k > r || y[j].c <= y[k].c) && j <= mid) z[i] = y[j++], num += z[i].book;
else
{
z[i] = y[k++];
if(!z[i].book) ans[z[i].id] += num;
}
}
for(int i = l; i <= r; ++i) y[i] = z[i];
}
inline void merge(int l, int r)
{
if(l == r) return;
int mid = (l + r) >> 1;
merge(l, mid), merge(mid + 1, r);
for(int i = l, j = l, k = mid + 1; i <= r; ++i)
{
if((k > r || x[j].b <= x[k].b) && j <= mid) y[i] = x[j++], y[i].book = y[i].cnt;
else y[i] = x[k++], y[i].book = 0;
}
for(int i = l; i <= r; ++i) x[i] = y[i];
solve(l, r);
}
int main()
{
n = read(), k = read();
for(int i = 1; i <= n; ++i)
p[i].a = read(), p[i].b = read(), p[i].c = read(), p[i].cnt = 1, p[i].id = i;
sort(p + 1, p + n + 1);
for(int i = 1; i <= n; ++i)
if(p[i].a != p[i - 1].a || p[i].b != p[i - 1].b || p[i].c != p[i - 1].c) x[++idx] = p[i];
else ++x[idx].cnt;
merge(1, idx);
for(int i = 1; i <= idx; ++i) sum[ans[x[i].id] + x[i].cnt - 1] += x[i].cnt;
for(int i = 0; i < n; ++i) print(sum[i]), putchar('\n');
return 0;
}
P4093 [HEOI2016/TJOI2016]序列
分析题意,考虑 DP,设 \(f_i\) 表示以 \(i\) 结尾的符合要求的最长序列长度
发现若 \(f_i\) 能转移到 \(f_j\),则 \(max_i<a_j\) 且 \(a_i<min_j\) 且 \(i<j\)
刚好形成三维偏序
此时就可以用 CDQ 分治优化 DP,把模板中统计答案改成当满足条件时进行转移
注意顺序,算右侧时右侧的最左边的 DP 值要被左边更新过才能正确更新后面,所以先计算再递归向右
用了树状数组维护,更方便实现
不用去重,下标肯定不同
代码:
inline void merge(int l, int r)
{
if(l == r) return;
int mid = (l + r) >> 1;
merge(l, mid);
sort(x + l, x + mid + 1, cmp1), sort(x + mid + 1, x + r + 1, cmp2);
for(int i = l, j = l, k = mid + 1; i <= r; ++i)
{
if((k > r || x[j].mx <= x[k].init) && j <= mid) add(x[j].init, f[x[j].id]), ++j;
else f[x[k].id] = max(f[x[k].id], query(x[k].mn) + 1), ++k;
}
for(int i = l; i <= mid; ++i) clear(x[i].init);
sort(x + mid + 1, x + r + 1, cmp3);
merge(mid + 1, r);
}
int main()
{
n = read(), m = read();
for(int i = 1; i <= n; ++i) x[i].init = x[i].mn = x[i].mx = read(), f[i] = 1, x[i].id = i;
for(int i = 1; i <= m; ++i)
{
u = read(), v = read();
x[u].mx = max(x[u].mx, v), x[u].mn = min(x[u].mn, v);
}
merge(1, n);
for(int i = 1; i <= n; ++i) ans = max(ans, f[i]);
printf("%d", ans);
return 0;
}
P3157 [CQOI2011]动态逆序对
考虑删除一个数对答案的减少量
发现是在它前面且未删除且比它大的数个数+在它后面且未删除且比它小的数的个数
思路一:树套树,树状数组套权值线段树,树状数组维护对应下标上的权值线段树,权值线段树维护每个值对应的数的数量
思路二:限制条件构成三维偏序,用 CDQ 分治计算每个数去掉后会产生的影响,这里把不去掉的数的删除时间看作 \(m+1\)
代码:
inline int cmp1(const node &c, const node &d)
{
if(c.tim != d.tim) return c.tim > d.tim;
return c.val < d.val;
}
inline int cmp2(const node &c, const node &d)
{
if(c.tim != d.tim) return c.tim > d.tim;
return c.val > d.val;
}
inline void solve1(ll l, ll r)
{
if(l == r) return;
ll mid = (l + r) >> 1, num = 0;
solve1(l, mid), solve1(mid + 1, r);
for(ll i = l, j = l, k = mid + 1; i <= r; ++i)
{
if((k > r || b[j].pos > b[k].pos) && j <= mid) c[i] = b[j++], num += c[i].book;
else
{
c[i] = b[k++];
if(!c[i].book) sum[c[i].pos] += num;
}
}
for(int i = l; i <= r; ++i) b[i] = c[i];
}
inline void cdq1(ll l, ll r) // 找 tim比它大,val比它小,pos在它后面的数的个数
{
if(l == r) return;
ll mid = (l + r) >> 1;
cdq1(l, mid), cdq1(mid + 1, r);
for(ll i = l, j = l, k = mid + 1; i <= r; ++i)
{
if((k > r || a[j].val < a[k].val) && j <= mid) b[i] = a[j++], b[i].book = 1;
else b[i] = a[k++], b[i].book = 0;
}
for(int i = l; i <= r; ++i) a[i] = b[i];
solve1(l, r);
}
inline void solve2(int l, int r)
{
if(l == r) return;
int mid = (l + r) >> 1, num = 0;
solve2(l, mid), solve2(mid + 1, r);
for(ll i = l, j = l, k = mid + 1; i <= r; ++i)
{
if((k > r || b[j].pos < b[k].pos) && j <= mid) c[i] = b[j++], num += c[i].book;
else
{
c[i] = b[k++];
if(!c[i].book) sum[c[i].pos] += num;
}
}
for(ll i = l; i <= r; ++i) b[i] = c[i];
}
inline void cdq2(ll l, ll r) // 找 tim比它大,val比它大,pos在它前面的数的个数
{
if(l == r) return;
ll mid = (l + r) >> 1;
cdq2(l, mid), cdq2(mid + 1, r);
for(ll i = l, j = l, k = mid + 1; i <= r; ++i)
{
if((k > r || a[j].val > a[k].val) && j <= mid) b[i] = a[j++], b[i].book = 1;
else b[i] = a[k++], b[i].book = 0;
}
for(ll i = l; i <= r; ++i) a[i] = b[i];
solve2(l, r);
}
inline void merge(ll l, ll r) // 求总共逆序对的数量
{
if(l == r) return;
ll mid = (l + r) >> 1;
merge(l, mid), merge(mid + 1, r);
for(ll i = l, j = l, k = mid + 1; i <= r; ++i)
{
if((k > r || a[j].val < a[k].val) && j <= mid) b[i] = a[j++];
else b[i] = a[k++], tot += mid - j + 1;
}
for(int i = l; i <= r; ++i) a[i] = b[i];
}
int main()
{
n = read(), m = read();
for(int i = 1; i <= n; ++i) a[i].val = read(), a[i].pos = p[a[i].val] = i;
for(int i = 1; i <= m; ++i)
{
lin[i] = read();
a[p[lin[i]]].tim = i;
}
for(int i = 1; i <= n; ++i)
if(!a[i].tim) a[i].tim = m + 1;
merge(1, n);
sort(a + 1, a + n + 1, cmp1), cdq1(1, n);
sort(a + 1, a + n + 1, cmp2), cdq2(1, n);
for(int i = 1; i <= m; ++i)
{
print(tot), putchar('\n');
tot -= sum[p[lin[i]]];
}
return 0;
}
整体二分
通常我们知道二分答案,把最优性问题转化为可行性问题
整体二分用于离线回答多组询问,可以看作一次二分所有的答案,询问按时间顺序排列,因此可以加上修改
设当前二分的答案为 \([lval,rval]\),询问区间为 \([st,ed]\)
那么处理询问,看答案是否大于 \(mid\),小于等于 \(mid\) 则按顺序放在左边,大于 \(mid\) 则按顺序放在右边,并且减去左边对它的影响
把左右边的询问合并到原序列上,设左边有 \(ql\) 个询问
然后二分 \([lval,mid],[st,st+ql-1]\),\([mid+1,rval],[st+ql,ed]\)
递归边界是当 \(lval=rval\) 时,所有询问答案为 \(lval\),当 \(st>ed\) 时直接返回
P2617 Dynamic Rankings
显然可以用之前的树套树解决
但是发现如果知道了一个值,可以快速判断它在区间内的名次是否 \(\le k\)
具体的,把小于等于它的数看作 \(1\),其余看作 \(0\),那么若区间和 \(\ge k\),则这个数的名次 \(\le k\)
因此可以整体二分所有答案,判断 \(mid\) 的名次
询问按上面流程处理,修改则看成删除一个数和加入一个数,最开始全部加入,如果值 \(\le mid\) 则放在左边,否则放右边
用树状数组维护区间和,碰到 \(\le mid\) 的修改直接更新,碰到询问则查询,注意把询问放到右边时 \(k\) 要减去区间和,抵消值域 \([lval,mid]\) 的修改产生的影响
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 100010, inf = 1e9;
int n, m, a[N], ans[N], cnt, li, ri, xi, numl, numr, tree[N];
char opt;
struct opr
{
int op, l, r, val, id;
}p[N << 2], pl[N << 2], pr[N << 2];
inline void print(int x)
{
if(x / 10) print(x / 10);
putchar(x % 10 + '0');
}
inline void add(int x, int val)
{
for(int i = x; i <= n; i += i & (-i)) tree[i] += val;
}
inline int query(int x)
{
int res = 0;
for(int i = x; i; i -= i & (-i)) res += tree[i];
return res;
}
inline void solve(int lval, int rval, int st, int ed)
{
if(st > ed) return;
if(lval == rval)
{
for(int i = st; i <= ed; ++i)
if(p[i].op == 2) ans[p[i].id] = lval;
return;
}
int numl = numr = 0;
int mid = (lval + rval) >> 1;
for(int i = st; i <= ed; ++i)
if(p[i].op != 2)
{
if(p[i].val <= mid) add(p[i].l, p[i].op), pl[++numl] = p[i];
else pr[++numr] = p[i];
}
else
{
int sum = query(p[i].r) - query(p[i].l - 1);
if(sum >= p[i].val) pl[++numl] = p[i];
else pr[++numr] = p[i], pr[numr].val -= sum;
}
for(int i = 1; i <= numl; ++i)
if(pl[i].op != 2) add(pl[i].l, -pl[i].op);
for(int i = 1; i <= numl; ++i) p[st + i - 1] = pl[i];
for(int i = 1; i <= numr; ++i) p[st + numl + i - 1] = pr[i];
solve(lval, mid, st, st + numl - 1), solve(mid + 1, rval, st + numl, ed);
}
int main()
{
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; ++i)
{
cin >> a[i];
p[++cnt] = (opr){1, i, i, a[i], i};
}
for(int i = 1; i <= m; ++i)
{
cin >> opt >> li >> ri, ans[i] = -1;
if(opt == 'Q') cin >> xi, p[++cnt] = (opr){2, li, ri, xi, i};
else p[++cnt] = (opr){-1, li, li, a[li], i}, p[++cnt] = (opr){1, li, li, ri, i}, a[li] = ri;
}
solve(0, inf, 1, cnt);
for(int i = 1; i <= m; ++i)
if(ans[i] > -1) print(ans[i]), putchar('\n');
return 0;
}