cdq分治和整体二分学习笔记
终于学完了cdq分治和整体二分,写篇文章总结下吧qwq
cdq分治
cdq分治是用来解决经典的偏序问题
其实归并排序求逆序对就是求偏序问题的做法,当合并左右两个有序的序列时,两个指针分别移动,当右边比左边小时就更新答案
那么我们回到三维偏序问题,也就是求所有满足\(a_i<a_j,b_i<b_j,c_i<c_j\)的个数
首先我们可以对\(a\)这一维排序,保证第一维有序
然后对\(b\)这一维归并,归并的时候将\(c\)这维加入树状数组,归并完之后更新答案查树状数组就好了
复杂度\(O(nlog^2n)\)
洛谷板子题的代码,因为有重复元素,并且还有\(=\),所以还要去重
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cmath>
#define N 100000
using namespace std;
struct node
{
int a,b,c,w,f;
}d[N + 5],t[N + 5];
int n,K,cnt,c[N * 2 + 5],ans[N + 5];
int cmp(node x,node y)
{
if (x.a == y.a)
if (x.b == y.b)
return x.c < y.c;
else
return x.b < y.b;
return x.a < y.a;
}
int lowbit(int x)
{
return x & (-x);
}
void add(int k,int x)
{
for (int i = k;i <= K;i += lowbit(i))
c[i] += x;
}
int query(int k)
{
int ans = 0;
for (int i = k;i;i -= lowbit(i))
ans += c[i];
return ans;
}
void cdq_(int l,int r)
{
if (l == r)
return;
int mid = l + r >> 1;
cdq_(l,mid);
cdq_(mid + 1,r);
int ll = l,it = l;
for (int j = mid + 1;j <= r;j++)
{
while (ll <= mid && d[ll].b <= d[j].b)
{
add(d[ll].c,d[ll].w);
t[it++] = d[ll];
ll++;
}
d[j].f += query(d[j].c);
t[it++] = d[j];
}
for (int i = ll;i <= mid;i++)
t[it++] = d[i];
for (int i = l;i < ll;i++)
add(d[i].c,-d[i].w);
for (int i = l;i < it;i++)
d[i] = t[i];
}
int main()
{
scanf("%d%d",&n,&K);
for (int i = 1;i <= n;i++)
{
scanf("%d%d%d",&d[i].a,&d[i].b,&d[i].c);
d[i].w = 1;
}
sort(d + 1,d + n + 1,cmp);
cnt = 1;
for (int i = 2;i <= n;i++)
if (d[cnt].a == d[i].a && d[cnt].b == d[i].b && d[cnt].c == d[i].c)
d[cnt].w++;
else
d[++cnt] = d[i];
cdq_(1,cnt);
for (int i = 1;i <= cnt;i++)
ans[d[i].f + d[i].w - 1] += d[i].w;
for (int i = 0;i < n;i++)
printf("%d\n",ans[i]);
return 0;
}
然后cdq分治的另一重大用处就是优化dp
当我们求出转移方程之后,如果转移条件满足偏序的性质,那么就可以用cdq分治来优化
拿道题来说
[USACO15FEB]牛跳房子(金)Cow Hopscotch (Gold)
就像人类喜欢跳格子游戏一样,FJ的奶牛们发明了一种新的跳格子游戏。虽然这种接近一吨的笨拙的动物玩跳格子游戏几乎总是不愉快地结束,但是这并没有阻止奶牛们在每天下午参加跳格子游戏
游戏在一个R*C的网格上进行,每个格子有一个取值在1-k之间的整数标号,奶牛开始在左上角的格子,目的是通过若干次跳跃后到达右下角的格子,当且仅当格子A和格子B满足如下条件时能从格子A跳到格子B:
1.B格子在A格子的严格右方(B的列号严格大于A的列号)
2.B格子在A格子的严格下方(B的行号严格大于A的行号)
3.B格子的标号和A格子的标号不同
请你帮助奶牛计算出从左上角的格子到右下角的格子一共有多少种不同的方案
首先我们可以设\(dp_{ij}\)表示走到坐标为\((i,j)\)这个格子的方案数
然后很快的写出状态转移方程:\(dp_{ij} = \sum_{k=1}^{i-1}\sum_{h=1}^{j-1} dp_{kh} (a_{ij}=a_{kh})\)
但是这样是\(O(n^4)\)的,于是考虑优化
可以对列分治然后按行循环,先更新左区间再更新右区间,每次更新答案的时候减掉重复的标号数就可以了
复杂度\(O(n^2log^2n)\)
Code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
const int N = 750;
const int p = 1e9 + 7;
using namespace std;
int n,m,k,a[N + 5][N + 5],f[N + 5][N + 5],s[N * N + 5],t[N * N + 5],ti;
void cdq_(int l,int r)
{
if (l == r)
return;
int mid = l + r >> 1;
cdq_(l,mid);
int tot = 0;
ti++;
for (int j = 1;j <= m;j++)
{
for (int i = r;i >= mid + 1;i--)
{
if (t[a[i][j]] < ti)
t[a[i][j]] = ti,s[a[i][j]] = 0;
f[i][j] = ((f[i][j] + tot - s[a[i][j]]) % p + p) % p;
}
for (int i = l;i <= mid;i++)
{
if (t[a[i][j]] < ti)
t[a[i][j]] = ti,s[a[i][j]] = 0;
s[a[i][j]] = (s[a[i][j]] + f[i][j]) % p;
tot = (tot + f[i][j]) % p;
}
}
cdq_(mid + 1,r);
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for (int i = 1;i <= n;i++)
for (int j = 1;j <= m;j++)
scanf("%d",&a[i][j]);
f[1][1] = 1;
cdq_(1,n);
printf("%d\n",f[n][m]);
return 0;
}
整体二分
整体二分是一种多次询问且对于答案可二分的离线做法
主要思想是将操作离线下来,全部对其二分
一般复杂度为\(O(nlog^2n)\),加一些奇技淫巧可以到\((nlogn)\)
这个做法支持所有答案可以二分,不强制在线且没有区间加的数据结构题
我们拿区间第k大来说,用\(solve(l,r,L,R)\)表示\(L~R\)的询问答案在\([l,r]\)范围内
那么取\(mid=l+r>>1\),如果要加的数比\(mid\)小,那么就加进去,遇到询问直接查前缀和排名(用树状数组维护就可以)
这样一直二分直到\(l=r\)就是\(L~R\)询问的答案了
我是把询问和点揉在一起二分的,常数可能大点qwq
Code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#define N 100000
#define M 5000
const int INF = 1e9;
using namespace std;
struct Q
{
int l,r,typ,id,k;
}q[N + M + 5],q1[N + M + 5],q2[N + M + 5];
int n,m,cnt,a[N + 5],ans[N + 5],c[N + 5];
int lowbit(int x)
{
return x & (-x);
}
void add(int x,int z)
{
for (;x <= n;x += lowbit(x))
c[x] += z;
}
int query(int x)
{
int ans = 0;
for (;x;x -= lowbit(x))
ans += c[x];
return ans;
}
void solve(int l,int r,int L,int R)
{
if (L > R)
return;
if (l == r)
{
for (int i = L;i <= R;i++)
if (q[i].typ == 2)
ans[q[i].id] = l;
return;
}
int mid = l + r >> 1,cnt1 = 0,cnt2 = 0;
for (int i = L;i <= R;i++)
if (q[i].typ == 1)
{
if (q[i].l <= mid)
{
q1[++cnt1] = q[i];
add(q[i].id,1);
}
else
q2[++cnt2] = q[i];
}
else
{
int x = query(q[i].r) - query(q[i].l - 1);
if (q[i].k <= x)
q2[++cnt2] = q[i];
else
{
q[i].k -= x;
q1[++cnt1] = q[i];
}
}
for (int i = 1;i <= cnt1;i++)
if (q1[i].typ == 1)
add(q1[i].id,-1);
for (int i = L;i <= L + cnt1 - 1;i++)
q[i] = q1[i - L + 1];
for (int i = L + cnt1;i <= R;i++)
q[i] = q2[i - L - cnt1 + 1];
solve(l,mid,L,L + cnt1 - 1);
solve(mid + 1,r,L + cnt1,R);
}
int main()
{
scanf("%d%d",&n,&m);
for (int i = 1;i <= n;i++)
{
scanf("%d",&a[i]);
q[++cnt] = (Q){a[i],1,1,i,0};
}
int l,r,k;
for (int i = 1;i <= m;i++)
{
scanf("%d%d%d",&l,&r,&k);
q[++cnt] = (Q){l,r,2,i,k};
}
solve(-INF,INF,1,cnt);
for (int i = 1;i <= m;i++)
printf("%d\n",ans[i]);
return 0;
}
当然如果有单点修改也是很容易的,我们直接把这个修改操作当成这个位置原来的数\(-1\),改成的新数\(+1\)就好了