cdq分治和整体二分

来源:互联网 发布:java 进阶书籍 编辑:程序博客网 时间:2024/04/30 10:47

cdq分治

ps:先膜拜陈丹琦大神,Orz%%%。

作用

很多动态的题目都需要高级数据结构,代码量很大,这时候cdq分治就展现了它的强大。只要不强制在线,cdq分治就可以将动态转化为静态处理,而且代码复杂度低,易调试,是很不错的方法!

实现

cdq分治是将整个操作序列进行分治,分治步骤如下:
1.将序列拆分为两半,[L,mid]和[mid+1,R]。
2.考虑[L,mid]所有操作对[mid+1,R]的影响,并计算。
3.由于[L,mid]和[mid+1,R]是和[L,R]相同的子问题,于是递归处理。

在cdq分治之前,通常要让操作序列满足一定的顺序(比如按照时间排序,使[L,mid]所有操作在[mid+1,R]之前)。而在cdq分治时,也可能需要让操作序列再次满足一定的顺序。

cdq分治典型的应用是三维偏序,然而我并不知道三维偏序准确定义是什么,考虑这样一个题目:给出n个三维点(x,y,z),(xi,yi,zi)>=(xj,yj,zj)定义为xi>=xj且yi>=yj且zi>=zj。求每个点>=多少个其他点。

如果点是二维的,那么就可以先按照x第一关键字,y第二关键字排序,然后利用树状数组就可以计算每个点>=多少个其他点。然而点是三维的,不过我们可以用同样的想法。先按照x第一关键字,y第二关键字,z第三关键字排序。x不用管,y和z用数据结构来维护,但这样代码量就比较大了。

如果我们把每个点看做操作,排序过后一个点会影响前面的点吗?不会,只会影响后面的点,所以我们想到了cdq分治。将操作序列分为[L,mid]和[mid+1,R],然后按照y第一关键字,区间(左小右大)第二关键字排序,这样就可以不用管y了,而z和二维问题一样,用树状数组即可。最后再处理一下完全相同的节点就可以得到答案。

模板

以HDU5618为例。
这就是上面说的问题,不多解释了。

#include<cstdio>#include<cstring>#include<algorithm>using namespace std;const int maxn=100000,maxm=100000;int te,n,MAX,ans[maxn+5];struct Point3D{    int x,y,z,id;    bool operator == (const Point3D &a) {return x==a.x&&y==a.y&&z==a.z;}};Point3D p[maxn+5],tem[maxn+5];struct FenwickTree{    int s[maxm+5];    int lowbit(int x) {return x&(-x);}    void Insert(int x,int tem) {while (x<=MAX) s[x]+=tem,x+=lowbit(x);}    int Ask(int L,int R)    {        int x,sum=0;L--;        x=L;while (x) sum-=s[x],x-=lowbit(x);        x=R;while (x) sum+=s[x],x-=lowbit(x);        return sum;    }};FenwickTree tr;bool Eoln(char ch) {return ch==10||ch==13||ch==EOF;}int readi(int &x){    int tot=0,f=1;char ch=getchar(),lst='+';    while ('9'<ch||ch<'0') {if (ch==EOF) return EOF;lst=ch;ch=getchar();}    if (lst=='-') f=-f;    while ('0'<=ch&&ch<='9') tot=tot*10+ch-48,ch=getchar();    x=tot*f;    return Eoln(ch);}bool mycmp1(const Point3D &a,const Point3D &b){    if (a.y<b.y) return true;    if (a.y==b.y&&a.id<b.id) return true;    return false;}void cdqBinary(int L,int R){    if (L==R) return;int mid=L+(R-L>>1);    for (int i=L;i<=mid;i++) tem[i]=p[i],tem[i].id=0;    for (int i=mid+1;i<=R;i++) tem[i]=p[i];    sort(tem+L,tem+1+R,mycmp1);    for (int i=L;i<=R;i++)        if (!tem[i].id) tr.Insert(tem[i].z,1); else        ans[tem[i].id]+=tr.Ask(1,tem[i].z);    for (int i=L;i<=R;i++) if (!tem[i].id) tr.Insert(tem[i].z,-1);    cdqBinary(L,mid);cdqBinary(mid+1,R);}bool mycmp2(const Point3D &a,const Point3D &b){    if (a.x<b.x) return true;    if (a.x==b.x&&a.y<b.y) return true;    if (a.x==b.x&&a.y==b.y&&a.z<b.z) return true;    if (a.x==b.x&&a.y==b.y&&a.z==b.z&&a.id<b.id) return true;    return false;}int main(){    freopen("cdqBinary.in","r",stdin);    freopen("cdqBinary.out","w",stdout);    readi(te);    while (te--)    {        readi(n);MAX=0;        for (int i=1;i<=n;i++)        {            readi(p[i].x);readi(p[i].y);readi(p[i].z);            p[i].id=i;MAX=max(MAX,p[i].z);        }        sort(p+1,p+1+n,mycmp2);        memset(ans,0,sizeof(ans));        for (int i=n-1,tot=0;i>=1;i--)        {            if (p[i]==p[i+1]) tot++; else tot=0;            ans[p[i].id]=tot;            //cdq分治处理时只往前统计相同三维点,所以要再向后统计一下相同三维点        }        cdqBinary(1,n);        for (int i=1;i<=n;i++) printf("%d\n",ans[i]);    }    return 0;}

整体二分

ps:先膜拜发明整体二分的我不知道是谁的人,Orz%%%

作用

和cdq分治一样,也是将动态转化为静态。

实现

整体二分也是对操作序列进行处理。但和cdq分治稍微有些不同,因为整体二分需要二分处理答案范围[L,R]:
1.如果L==R,说明答案已经确定了,将目前序列中所有询问的答案定为L(R)。
2.二分答案mid。
3.考虑序列中靠前的操作对靠后的询问的影响。
4.对序列进行分块,序列中<=mid的操作放到左边,>mid的放到右边。
5.递归处理相同子问题[L,mid]和[mid+1,R]。

整体二分经典应用是区间第K大。二分答案mid之后,按顺序遍历序列,如果是插入删除修改等操作,且数值<=mid,说明这些操作和[mid+1,R]无关,所以放到左边,如果数值>mid,说明这些操作和[L,mid]无关,所以放到右边,同时累加>mid的个数(比如使用树状数组)。如果是查询,先询问查询区间中>mid的个数,记为now,如果查询数值<now,说明该查询的答案<=mid,和[mid+1,R]无关,所以放到左边,但是查询数值要减去now(因为放到左边后[mid+1,R]的就没了),如果查询数值>=now,说明该查询的答案>mid,和[L,mid]无关,所以放到右边。

模板

以BZOJ3110为例。
这就是上面说的问题,不多解释了。下面的代码用到了树状数组区间增减,比线段树方便很多。

#include<cstdio>#include<cstring>#include<algorithm>using namespace std;typedef long long LL;const int maxn=50000,maxm=50000,MAXINT=((1<<30)-1)*2+1;int n,m,ans[maxm+5];struct Work{    int id,td,L,R,k;};Work wk[maxm+5],tem[2][maxm+5];struct FenwickTree{    LL s[2][maxn+5];    void clear() {memset(s,0,sizeof(s));}    int lowbit(int x) {return x&(-x);}    void Insert(int L,int R,LL tem)    {        int x;R++;        x=L;while (x<=n) s[0][x]+=tem,s[1][x]+=tem*(L-1),x+=lowbit(x);        x=R;while (x<=n) s[0][x]-=tem,s[1][x]-=tem*(R-1),x+=lowbit(x);    }    LL Ask(int L,int R)    {        int x;LL sum=0;L--;        x=L;while (x) sum-=s[0][x]*L-s[1][x],x-=lowbit(x);        x=R;while (x) sum+=s[0][x]*R-s[1][x],x-=lowbit(x);        return sum;    }};FenwickTree tr;void AllBinary(int L,int R,int l,int r){    if (l>r) return;    if (L==R)    {        for (int i=l;i<=r;i++) if (wk[i].td==2) ans[wk[i].id]=L;        return;    }    int num[2],mid=L+(R-L>>1);num[0]=num[1]=0;    for (int i=l;i<=r;i++)        if (wk[i].td==1)        {            if (wk[i].k<=mid) tem[0][++num[0]]=wk[i]; else            tem[1][++num[1]]=wk[i],tr.Insert(wk[i].L,wk[i].R,1);        } else        {            LL now=tr.Ask(wk[i].L,wk[i].R);            if (now<wk[i].k) wk[i].k-=now,tem[0][++num[0]]=wk[i]; else            tem[1][++num[1]]=wk[i];        }    for (int i=l;i<=r;i++) if (wk[i].td==1&&wk[i].k>mid) tr.Insert(wk[i].L,wk[i].R,-1);    for (int i=1;i<=num[0];i++) wk[l+i-1]=tem[0][i];    for (int i=1;i<=num[1];i++) wk[l+num[0]+i-1]=tem[1][i];    AllBinary(L,mid,l,l+num[0]-1);AllBinary(mid+1,R,l+num[0],r);}int main(){    freopen("AllBinary.in","r",stdin);    freopen("AllBinary.out","w",stdout);    scanf("%d%d",&n,&m);int L=MAXINT,R=-MAXINT;    for (int i=1;i<=m;i++)    {        scanf("%d%d%d%d",&wk[i].td,&wk[i].L,&wk[i].R,&wk[i].k);wk[i].id=i;        if (wk[i].td==1) L=min(L,wk[i].k),R=max(R,wk[i].k);    }    memset(ans,63,sizeof(ans));    AllBinary(L,R,1,m);    for (int i=1;i<=m;i++) if (ans[i]!=ans[0]) printf("%d\n",ans[i]);    return 0;}

效率

假设cdq分治和整体二分中的操作复杂度为T,则cdq分治和整体二分的效率均为O(Tlog2(n))。非常不错。

然而我这个蒟蒻要熟练掌握cdq分治和整体二分看来还需要很久……

0 0
原创粉丝点击