线段树(lazy算法+离散化)

来源:互联网 发布:襄阳华为云计算招聘 编辑:程序博客网 时间:2024/05/16 00:36

最近陪着小六的小学生们补了补线段树
发现我的线段树真是弱鸡到不能在弱了
怪我咯,以前老师赶进度赶得太紧了,我又缺课(我终于明白我为什么那么蒟蒻了)
这里写一写我这几天的学习的一些算(题)法(目)

一、
最简单的
HDOJ 1754
相信很多人开始学习线段树都是从这一题开始的

题目罗罗嗦嗦了一大堆也不就一句话:
给出N个数,两种操作:
1、U x y:修改第x个数的值为y;
2、Q x y:求第x到第y个的最大值,注:x未必比y小

标准的线段树对不对
这里写图片描述

我们可以理解成总裁管左右两个总经理,总经理管左右两个副总经理,副总经理管左右两个经理……每个管理者都只知道自己的值,不知道他们手下的值,所以他们每次都要问自己的手下……这样就很好理解了

#include<cstring>#include<cstdio>using namespace std;int a[210000];//员工,只负责一开始表达自己的值struct node //管理者(假设它是经理),管理者绝对不是员工,一定要分开来//最底层的管理者只管一个员工,绝对不能想着:反正一个员工,就自己管自己算了。//理解:有N个员工,就配有2N-1个管理者,用空间换时间{    int l,r,lc,rc,c;// 管理的员工编号是第l个到第r个(连续),lc表示左副经理的编号,rc表示右副经理是谁    //c表示第l个员工至第r个员工的特征值:可以是和、最大值、或者最小值}tr[410000]; int len;//tr数组就是管理者数组,len表示当前申请到第几个管理者//到此,员工有自己的编号,管理者也有自己的编号int mymax(int x,int y){  return x>y?x:y;} //求xy最大值的函数void  bt(int l,int r) // build tree建立线段树,申请一个管理者,管理第l个员工至第r个员工{    len++; int now=len;// now记录当前管理者的编号    tr[now].l=l; tr[now].r=r; tr[now].lc=tr[now].rc=-1;tr[now].c=0;//一开始当前管理者now的左右副总经理都是没人-1,管理者管理范围最大值为0,这里五个元素都要赋值    if(l<r) //如果管的人大于1人,就有权申请两个副总经理帮忙管人    {        int mid=(l+r)/2; // mid为l和r的中间值,从中间分为两段[l,mid]和[mid+1,r]        tr[now].lc=len+1; bt(    l  , mid  );  // [l,mid]给左副总管,让他先去管好[l,mid]        tr[now].rc=len+1; bt( mid+1 ,  r   );  // [mid+1,r]给右副总管,让他先去管好[mid+1,r]    }}void change(int now,int x,int k)//change的功能:在当前管理者now的管理范围内,把管第x个员工的管理者(不知道该管理者的编号)的值改为k//理解:为什么第x个一定在now的管理范围内呢?//注意:修改,改的是管理者,不是改员工,员工已经没用了。{    if( tr[now].l==tr[now].r) { tr[now].c=k;return ;}//如果now只管一人,那么这个人的编号一定是x,为什么?    int lc= tr[now].lc, rc=tr[now].rc;//找出now的左右副总分别是谁    int mid=( tr[now].l+ tr[now].r)/2;//找到now管理范围的中间位置    if( x<=mid)          change(lc,x,k);  //如果x在now的左副总的管理范围,那么修改这件事就交给左副总去做    else if( mid+1<=x)   change(rc,x,k);  //如果x在now的右副总的管理范围,那么修改这件事就交给右副总去做    tr[now].c= mymax( tr[ lc ] .c  ,  tr[ rc ].c );//修改完后,注意要维护,有可能最大值发生变化了}int findmax(int now,int l,int r)//findmax的功能:在当前管理者now的管理范围内,找出第l个员工至第r个员工的最大值{    if( l== tr[now].l &&  tr[now].r== r) return tr[now].c;//如果now的管理范围刚好是[l,r],就不用问左右副总了    int lc= tr[now].lc, rc=tr[now].rc;    int mid=( tr[now].l+ tr[now].r)/2;    if( r<=mid)         return  findmax(lc,l,r); //[l,r]在左副总的管理范围内    else if( mid+1<=l)  return  findmax(rc,l,r); //[l,r]在右副总的管理范围内    else  return mymax(  findmax(lc,l,mid)  ,  findmax(rc,mid+1,r)  );//其他情况就是[l,r]一部分在左副总,一部分在右副总}int main(){    int n,m,i,x,y; char ss[10];    while( scanf("%d%d",&n,&m)!=EOF)    {        for(i=1;i<=n;i++) scanf("%d",&a[i]);        len=0; bt(1,n);tr[1].c=0; //初始化len为0,一开始没有一个管理者for(i=1;i<=n;i++) change(1,i,a[i]);//初始化,把第i个位置改为a[i]        for(i=1;i<=m;i++)        {            scanf("%s%d%d",ss,&x,&y);            if(ss[0]=='Q')  printf("%d\n",  findmax(1, x,y)   );            else change( 1, x, y);        }    }    return 0;}

虽然用线段树容易空间(时间)超限,值得庆幸的是这道题数据比较弱啊,不会超限

二、lazy算法
poj2777

(现在的题目都那么罗嗦吗)
题目大概是这样的:
有L段线段(编号为1~L) ,一开始 全部是颜色1。有两种操作
1、C A B tt :A~B染第tt种颜色
2、P A B :询问A~B有多少种不一样的颜色。
还是要注意A有可能比B大。

这道题每次更改的是一段数值,并且是求出一段距离的颜色数量,所以这道题体现线段树更彻底。而更新次数就比上一道题大很多了,所以我们在更新时极有可能会更新超时。所以我们这里会用到lazy算法。

所谓lazy算法就是在更新时,只更新要求更新的这一段,而不更新他的手(儿)下(子)们。当我们要访问他的这个手(儿)下(子)时,我们才去更新他的值。这样我们就能减少很多更新的次数,从而减少时间。

然而这道题我有两个不同版本的代码
先看看第一个:

#include<cstring>#include<cstdio>using namespace std;struct node{    int l,r,lc,rc,c;}tr[210000]; int len;bool v[35];void  bt(int l,int r) // build tree{    len++; int now=len;    tr[now].l=l; tr[now].r=r; tr[now].lc=tr[now].rc=-1;    if(l<r)     {        int mid=(l+r)/2;        tr[now].lc=len+1;bt(l,mid);        tr[now].rc=len+1; bt(mid+1,r);    }}void wen(int now,int l,int r)//wen(问)函数的功能:把now所管理范围中第l个员工至第r个员工的颜色都在v数组里面体现为true{    if( tr[now].c>0) { v[tr[now].c]=true;  return ;}// tr[now].c>0表示now管理范围的颜色是统一的,那么就不用麻烦左右副总了    int lc= tr[now].lc, rc=tr[now].rc;    int mid=( tr[now].l+ tr[now].r)/2;    if( r<=mid)       wen(lc,l,r); //[l,r]在左副总的管理范围,这件事情就交给左副总去做    else if( mid+1<=l)  wen(rc,l,r); //[l,r]在右副总的管理范围,这件事情就交给右副总去做    else    //来到了这个else就是表示[l,r]有一部分在左副总,有一部分在右副总    {        wen(lc,  l    , mid );        wen(rc, mid+1 ,  r  );    }}void change(int now,int l,int r,int k)//change(改)函数的功能 :在now的管理范围内,把[l,r]改为第k种颜色{    if( tr[now].c==k) return ;//如果now管理的范围颜色统一,并且本来就是k,那么什么都不要做    if( tr[now].l==l&& r==tr[now].r) { tr[now].c=k;return ;} //如果刚好now的管理范围就是[l,r],那么不管now管理范围原来是什么颜色统一改就行    int lc= tr[now].lc, rc=tr[now].rc;    int mid=( tr[now].l+ tr[now].r)/2;    if(tr[now].c>0)//如果原来now的管理范围颜色统一,那么现在要改now的管理范围中的部分范围的颜色了                   //此时now就想:我的把我原来的颜色先传给我的左右副总,因为他们还不知道他们现在的颜色(在这次改之前的颜色)    {              //这个步骤我们称为:继承        tr[lc].c= tr[now].c;        tr[rc].c= tr[now].c;    }    if( r<=mid)         change(lc,l,r,k); //如果[l,r]在左副总的管理范围中,那么这件事情就交给左副总    else if( mid+1<=l)   change(rc,l,r,k); //如果[l,r]在右副总的管理范围中,那么这件事情就交给右副总    else    {       change(lc,  l    , mid , k );       change(rc, mid+1 ,  r  , k );    }    //注意:有 修改 就配带有 维护    if( tr[lc].c==tr[rc].c&& tr[lc].c>0  )tr[now].c= tr[ lc ] .c ;    else tr[now].c=-1;}int main(){    int k,j,i,x,y,L,t,M,ans; char ss[10];    while( scanf("%d%d%d",&L,&t,&M)!=EOF)    {        len=0; bt(1,L);tr[1].c=1;//初始化所有颜色都为1        for(i=1;i<=M;i++)        {            scanf("%s",ss);            if(ss[0]=='C')            {                scanf("%d%d%d",&x,&y,&k);                if( x>y) { int tt=x;x=y;y=tt;}                change(1,x,y,k);            }            else            {                scanf("%d%d",&x,&y); if(x>y) { int tt=x;x=y;y=tt; } //这是个坑                memset(v,false,sizeof(v)); //一开始所有颜色都没有出现过                wen(1,x,y);                ans=0;  for(j=1;j<=t;j++) if( v[j]==true) ans++;                printf("%d\n",ans);            }        }    }    return 0;}

相信大多数人接触的都是以上这种版本。
据说这个版本在遇到一些数据时会有bug

下面推荐另外一个版本:
通过二进制的方式来记录有多少种颜色
每次更新时只要左移c-1位就可以了
在统计时就只用查找有多少个1就可以了
那!么!
如果我们用了二进制,在更新他的上(父)司(亲)时有多少种颜色是就不能用max了
我们怎么写呢???

我们知道在最后时是统计他有多少个1
并且如果他任意一个儿子有这种颜色,即使他的另外一个儿子没有,他也有这种颜色
而或(|)运算正好可以满足!

推荐并提供我的代码:

#include <cstdio>#include <cstdio>using namespace std;int len,a[100010];struct node{    int lc,rc,l,r,c;bool update;//update:false为已更新了他的儿子,true为还未更新他的儿子//c:是颜色的状态压缩值,用二进制位表示是否选用了某种颜色}tr[200010];int br(int l,int r){    len++;int now=len;    tr[now].l=l;tr[now].r=r;tr[now].c=1;//与上题不一样的:由于开始整条都是颜色1,所以状态值处置设置为1,参考二进制的表示    tr[now].update=false;    if (l<r)    {        int mid=(l+r)>>1;        tr[now].lc=br(l,mid);        tr[now].rc=br(mid+1,r);    }    return now;}void swap(int &a,int &b){    int t=a;a=b;b=t;}int col(int x)//统计x状态二进制位1的个数{    int ans=0;    while (x>0)    {        ans+=x%2;        x=x/2;    }    return ans;}void update(int x)//更新他的儿子。注意这一步一定要在访问他的儿子之前做好{    tr[x].update=false;    tr[tr[x].lc].update=true;    tr[tr[x].lc].c=tr[x].c;    tr[tr[x].rc].update=true;    tr[tr[x].rc].c=tr[x].c;}void change(int x,int l,int r,int c)//返回值为该线段的编号{    if (tr[x].l==l&&tr[x].r==r)     {        tr[x].c=1<<(c-1);        //标记状态值二进制右起第c位为1,其他为0//若整段匹配,则更新该线段update标记        return ;    }    int mid=(tr[x].l+tr[x].r)>>1,lc=tr[x].lc,rc=tr[x].rc;    if (tr[x].update) update(x);//在访问儿子之前必须更新儿子,切记!!也是该算法的重点。    if (r<=mid) change(lc,l,r,c);    else if (l>=mid+1) change(rc,l,r,c);    else {change(lc,l,mid,c);change(rc,mid+1,r,c);}    tr[x].c=(tr[lc].c|tr[rc].c);//合并左右儿子的状态。使用“或”操作计算}int findmax(int x,int l,int r){    if (tr[x].l==l&&tr[x].r==r) return tr[x].c;    int mid=(tr[x].l+tr[x].r)>>1,lc=tr[x].lc,rc=tr[x].rc;    if (tr[x].update) update(x); //在访问儿子之前必须更新儿子    if (l>=mid+1) return findmax(rc,l,r);    else if (r<=mid) return  findmax(lc,l,r);    else return (findmax(lc,l,mid)|findmax(rc,mid+1,r));//返回左右儿子合并值}int main(){    int n,m,t,x,y,z;    char c;    scanf("%d%d%d",&n,&t,&m);    br(1,n);len=0;    for (int i=1;i<=m;i++)    {        getchar();        scanf("%c",&c);        if (c=='C')        {            scanf("%d%d%d",&x,&y,&z);            if (x>y) swap(x,y);            change(1,x,y,z);        }        if (c=='P')        {            scanf("%d%d",&x,&y);            if (x>y) swap(x,y);//本题坑点之一,必须注意大小关系            printf("%d\n",col(findmax(1,x,y)));        }    }    return 0;}

上面讲了优化时间的lazy算法,下面来讲讲优化空间的离散化。

poj2528
这里写图片描述

所谓离散化就是
原数组为A[]={3,100,9845587},
这个数组那么大,但是却只用到了很少一部分
而 {1~2,4~99,…}都没有用到
我们怎样能高效地利用这些空间呢
那么我们就要用到离散化 了
离散化为S[]={1,2,3}
再比如:
原数组为A[]={ 3 , 100 , 9845587 , 6 , 6 , 9 , 11 , 2 },
离散化为S[]={ 2 , 6 , 7 , 3 , 3 , 4 , 5 , 1 }

离散化的过程:
1:原数组A每个数还得多带点东西:头x(原来的值),胸口p(原来的位置),肚子z(离散化的值)
2:把A复制一份为B,然后B数组根据B中x的值进行排序,然后根据B排序后的位置赋予每个B的离散值
3:让B中的每个元素带着自己的离散值和位置值去赋值A数组中的z值。A[ B[i].p ] = B[i].z ;
4:到此离散化结束,A.z就是A.x的离散值,而且一一对应。

值得要注意到是:
原数组:{1,2,4,5,7}

代码:

#include<cstdio>#include<cstdlib>#include<cstring>struct node{int x,y,p;};//离散化用node a[20010],b[20010];int n,mm;struct tree{int l,r,lc,rc,c;bool update;};tree tr[40010];int len=0;bool col[20010];void qsort(int l,int r)//快排{    int i=l,j=r;    node mid,t;    mid=b[(l+r)>>1];    while (i<=j)    {        while (b[i].x<mid.x) i++;        while (b[j].x>mid.x) j--;        if (i<=j)        {            t=b[i];b[i]=b[j];b[j]=t;            i++;j--;        }    }    if (l<j) qsort(l,j);    if (i<r) qsort(i,r);}int bt(int l,int r){    len++;    int now=len;    tr[now].l=l;    tr[now].r=r;    tr[now].c=0;    tr[now].lc=-1;    tr[now].rc=-1;    tr[now].update=false;    if (l+1!=r)    {        int mid;        mid=(l+r)>>1;        tr[now].lc=bt(l,mid);        tr[now].rc=bt(mid,r);    }    return now;}void update(int x)//lazy算法{    tr[x].update=false;    if (tr[x].lc!=-1 && tr[x].rc!=-1)     {        tr[tr[x].lc].update=tr[tr[x].rc].update=true;        tr[tr[x].lc].c=tr[tr[x].rc].c=tr[x].c;    }}void insert(int x,int l,int r,int p)//change{    if (tr[x].l==l && tr[x].r==r)    {        tr[x].c=p;        tr[x].update=true;        return;    }    if (tr[x].update) update(x);    int mid;    mid=(tr[x].l+tr[x].r)>>1;    if (r<=mid) insert(tr[x].lc,l,r,p);    else if (l>=mid) insert(tr[x].rc,l,r,p);    else {insert(tr[x].lc,l,mid,p);insert(tr[x].rc,mid,r,p);};}int main(){    int m;    scanf("%d",&m);    for (int u=1;u<=m;u++)    {        scanf("%d",&n);n=n<<1;        for (int i=1;i<=n;i+=2)        {            scanf("%d %d",&a[i].x,&a[i+1].x);            a[i+1].x++;            a[i].p=i;            a[i+1].p=i+1;        }        for (int i=1;i<=n;i++)            b[i]=a[i];        qsort(1,n);        b[1].y=1;        for (int i=2;i<=n;i++)            if (b[i].x==b[i-1].x) b[i].y=b[i-1].y;            else b[i].y=b[i-1].y+1;        for (int i=1;i<=n;i++)        {            a[b[i].p].y=b[i].y;        }        len=0;        bt(1,b[n].y);        for (int i=1;i<=n;i+=2)        {            insert(1,a[i].y,a[i+1].y,i);        }        memset(col,false,sizeof(col));        for (int i=1;i<=len;i++)        {            if (tr[i].update) update(i);            if (tr[i].lc==-1) col[tr[i].c]=true;        }        int ans=0;        for (int i=1;i<=n;i++)            if (col[i]) ans++;        printf("%d\n",ans);    }}

今天测量一套题……结果气到我爆炸
明天将会分享

0 0