線段樹(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;} //求x和y最大值的函式
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);
}
}