1. 程式人生 > >線段樹(lazy演算法+離散化)

線段樹(lazy演算法+離散化)

畢竟是寫給自己看的
還是寫好看一點吧

一、

最簡單的

題意~
給出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演算法

題目大概是這樣的:
有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演算法,下面來講講優化空間的

離散化

所謂離散化就是
原陣列為A[]={3,100,9845587},
這個陣列那麼大,但是卻只用到了很少一部分
而 {1~2,4~99,…}都沒有用到
我們怎樣能高效地利用這些空間呢

那麼我們就要用到離散化 了
如上述的A離散化為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的離散值,而且一一對應。

程式碼:

#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;
//update是否有更新兒子(lazy),真需要更新假已更新};
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);
    }
}