線段樹維護映射

常見的線段樹是將左右兩個區間的標記合併。然而有些時候傳統的標記無法合併(比如不滿足結合律的運算),這時候就需要用特殊的標記。這裏說一種叫映射的標記。

下面有三道例題:
T1:

在這裏插入圖片描述
在這裏插入圖片描述

首先我們要維護兩個個數組,下標i表示第i位,數組的意義是如果第i位上原本是1或者0,經過一段區間後變成了1或者0,這就是一個映射。我們設a[i]表示第i位上的1經過一段區間後變成了0或者1,則合併v的兩個兒子ls,rs的時候,v.a[i]=rs.a[ls.a[i]]。
然後用數組維護複雜度太高,我們用一個int來表示。
我們以v.a1v.a_{1}爲例: v.a1=(ls.a1&rs.a1)((v.a_{1}=(ls.a_{1} \&rs.a_{1})|((~ls.a1)&rs.a0)ls.a_{1})\& rs.a_{0})
詢問的時候,我們得到了一個映射,表示第i位上的1或者0經過指定的區間過後變成了0或者1,然後我們再DP一下。具體實現看代碼。

代碼:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<set>
#include<map>
#include<vector>
#include<ctime>
#define ll long long
#define N 500005

using namespace std;
inline int Get() {
	int x=0;char ch=getchar();
	while(!isdigit(ch))ch=getchar();
	while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
	return x;
}
char Buffer[10<<20],*T=Buffer;
char W[1000][3];
inline void Print(int x){
	if(!x)return;
	if(x>=1000){
		Print(x/1000);
		char*w=W[x%1000];
		*T++=w[0];
		*T++=w[1];
		*T++=w[2];
	}else{
		Print(x/10);
		*T++=x%10+'0';
	}
}
inline void print(int x){
	if(x==0)*T++='0';
	else Print(x);
	*T++='\n';
}

int n,m;
int op[N],w[N];
const int maxx=(1ll<<31)-1;
struct tree {
	int l,r;
	int c0,c1;
}tr[N<<2];

inline void update(int v) {
	int ls=v<<1,rs=v<<1|1;
	tr[v].c0=(tr[ls].c0&tr[rs].c1)|((tr[ls].c0^maxx)&tr[rs].c0);
	tr[v].c1=(tr[ls].c1&tr[rs].c1)|((tr[ls].c1^maxx)&tr[rs].c0);
}

inline void build(int v,int l,int r) {
	tr[v].l=l,tr[v].r=r;
	if(l==r) {
		if(op[l]==0) {
			tr[v].c0=0;
			tr[v].c1=w[l];
		} else if(op[l]==1) {
			tr[v].c0=w[l];
			tr[v].c1=maxx;
		} else {
			tr[v].c0=w[l];
			tr[v].c1=maxx^w[l];
		}
		return ;
	}
	int mid=l+r>>1;
	build(v<<1,l,mid),build(v<<1|1,mid+1,r);
	update(v);
}

inline void Modify(int v,int pos,int op,int w) {
	if(tr[v].l>pos||tr[v].r<pos) return ;
	if(tr[v].l==tr[v].r) {
		if(op==0) {
			tr[v].c0=0;
			tr[v].c1=w;
		} else if(op==1) {
			tr[v].c0=w;
			tr[v].c1=maxx;
		} else {
			tr[v].c0=w;
			tr[v].c1=maxx^w;
		}
		return ;
	}
	Modify(v<<1,pos,op,w),Modify(v<<1|1,pos,op,w);
	update(v);
}

int c0,c1;
inline void query(int v,int l,int r) {
	if(tr[v].l>r||tr[v].r<l) return ;
	if(l<=tr[v].l&&tr[v].r<=r) {
		c0=(c0&tr[v].c1)|((c0^maxx)&tr[v].c0);
		c1=(c1&tr[v].c1)|((c1^maxx)&tr[v].c0);
		return ;
	}
	query(v<<1,l,r),query(v<<1|1,l,r);
}

int lx[31],rx[31];
int g[2];
int f[31][2][2];

inline void work(int x,int y) {
	for(int i=0;i<31;i++,x>>=1) lx[i]=x&1;
	for(int i=0;i<31;i++,y>>=1) rx[i]=y&1;
	g[0]=c0,g[1]=c1;
	memset(f,-1,sizeof(f));
	f[0][0][0]=max(f[0][0][0],g[0]&1);
	f[0][lx[0]<=0][0]=max(f[0][lx[0]<=0][0],g[0]&1);
	f[0][0][0<=rx[0]]=max(f[0][0][0<=rx[0]],g[0]&1);
	f[0][lx[0]<=0][0<=rx[0]]=max(f[0][lx[0]<=0][0<=rx[0]],g[0]&1);
	
	f[0][0][0]=max(f[0][0][0],g[1]&1);
	f[0][lx[0]<=1][0]=max(f[0][lx[0]<=1][0],g[1]&1);
	f[0][0][1<=rx[0]]=max(f[0][0][1<=rx[0]],g[1]&1);
	f[0][lx[0]<=1][1<=rx[0]]=max(f[0][lx[0]<=1][1<=rx[0]],g[1]&1);
	for(int i=1;i<=30;i++) {
		for(int j=0;j<=1;j++) {
			for(int k=0;k<=1;k++) {
				int a=j?lx[i]:0,b=k?rx[i]:1;
				for(;a<=b;a++) {
					f[i][j][k]=max(f[i][j][k],f[i-1][j&&a==lx[i]][k&&a==rx[i]]+(g[a]&(1<<i)));
				}
			}
		}
	}
	cout<<f[30][1][1]<<"\n";
}

int main() {
	for(int i=0;i<10;i++)
		for(int j=0;j<10;j++)
			for(int k=0;k<10;k++)
				W[i*100+j*10+k][0]=i+'0',
				W[i*100+j*10+k][1]=j+'0',
				W[i*100+j*10+k][2]=k+'0';
	n=Get(),m=Get();
	for(int i=1;i<=n;i++) {
		op[i]=Get(),w[i]=Get();
	}
	build(1,1,n);
	int op,x,y,z;
	int l,r,u,d;
	while(m--) {
		op=Get();
		if(op==1) {
			x=Get(),y=Get(),z=Get();
			Modify(1,x,y,z);
		} else {
			l=Get(),r=Get(),u=Get(),d=Get();
			c0=0,c1=maxx;
			query(1,l,r);
			work(u,d);
		}
	}
	return 0;
}

T2:
【問題描述】
跳房子,是一種世界性的兒童遊戲,也是中國民間傳統的體育遊戲之一。
跳房子是在N個格子上進行的,CYJ對遊戲進行了改進,該成了跳棋盤,改進後的遊戲是在一個N行M列的棋盤上進行,並規定從第一行往上可以走到最後一行,第一列往左可以走到最後一列,反之亦然。每個格子上有一個數字。
在這個棋盤左上角(1,1)放置着一枚棋子。每次棋子會走到右、右上和右下三個方向格子中對應上數字最大一個。即任意時刻棋子都只有一種走法,不存在多個格子同時滿足條件。
現在有兩種操作:
move k 將棋子前進k步。
change a b e 將第a行第b列格子上的數字修改爲e。
請對於每一個move操作輸出棋子移動完畢後所處的位置。
【輸入】
第一行包含兩個正整數N,M(3<=N,M<=2000),表示棋盤的大小。
接下來N行,每行M個整數,依次表示每個格子中的數字a[i,j](1<= a[i,j]<=109)。
接下來一行包含一個正整數Q(1<=Q<=5000),表示操作次數。
接下來m行,每行一個操作,其中1<=a<=N,1<=b<=M,1<=k,e<=109。
【輸出】
對於每個move操作,輸出一行兩個正整數x,y,即棋子所處的行號和列號。
【輸入輸出樣例】
jump.in
4 4
1 2 9 3
3 5 4 8
4 3 2 7
5 8 1 6
4
move 1
move 1
change 1 4 100
move 1 4 2

jump.out
4 2
1 3
1 4

跳房子的過程就是一個映射,映射是可以直接合並的,並且映射是可以快速冪的。所以我們對列開一個線段樹,沒個節點中有一個數組,數組第i位表示第i行初始時就在原位置,經過了區間 [l,r][l,r]後第i行在哪個位置。與上道題類似 v.a[i]=rs.a[ls.a[i]]v.a[i]=rs.a[ls.a[i]]
然後我們處理詢問的時候從第i行第j列出發先走完m列(這樣一定會回到第j列),這樣我們又得到了一個映射,表示從第i行第j列出發,走回第j列時會到達哪一行。然後在對這個映射進行快速冪,最後不足m列的地方我們在挨着走就是了。

代碼:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<set>
#include<map>
#include<vector>
#include<ctime>
#define ll long long
#define N 2005

using namespace std;
inline int Get() {int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}while('0'<=ch&&ch<='9') {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*f;}

int n,m,Q;
int w[N][N];
int id[N];
struct change {
	int g[N];
	void Init() {for(int i=1;i<=n;i++) g[i]=i;}
}tem,h;
change operator *(const change &a,const change &b) {
	for(int i=1;i<=n;i++) tem.g[i]=b.g[a.g[i]];
	return tem;
}

struct tree {
	int l,r;
	change x;
}tr[N<<2];
void update(int v) {tr[v].x=tr[v<<1].x*tr[v<<1|1].x;}

void Get(int x,int y,int *g) {
	int pos=0,nxt=y%m+1;
	for(int i=-1;i<=1;i++) {
		int now=x+i;
		if(now<1) now=n;
		else if(now==n+1) now=1;
		if(w[pos][nxt]<w[now][nxt]) pos=now;
	}
	g[x]=pos;
}

void build(int v,int l,int r) {
	tr[v].l=l,tr[v].r=r;
	if(l==r) {
		id[l]=v;
		for(int i=1;i<=n;i++) Get(i,l,tr[v].x.g);
		return ;
	}
	int mid=l+r>>1;
	build(v<<1,l,mid),build(v<<1|1,mid+1,r);
	update(v);
}

void Modify(int v,int y) {
	if(tr[v].l>y||tr[v].r<y) return ;
	if(tr[v].l==tr[v].r) {
		for(int i=1;i<=n;i++) Get(i,y,tr[v].x.g);
		return ;
	}
	Modify(v<<1,y),Modify(v<<1|1,y);
	update(v);
}

change query(int v,int l,int r) {
	if(l<=tr[v].l&&tr[v].r<=r) return tr[v].x;
	int mid=tr[v].l+tr[v].r>>1;
	if(mid<l) return query(v<<1|1,l,r);
	else if(r<=mid) return query(v<<1,l,r);
	else return query(v<<1,l,r)*query(v<<1|1,l,r); 
}

change ksm(change h,int x) {
	change ans;
	ans.Init();
	for(;x;x>>=1,h=h*h) {
		if(x&1) ans=ans*h;
	}
	return ans;
}

int sx,sy;

void Move(int k) {
	for(int i=1;i<=k;i++) {
		sx=tr[id[sy]].x.g[sx];
		sy=sy%m+1;
	}
}

int main() {
	n=Get(),m=Get();
	sx=1,sy=1;
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=m;j++) {
			w[i][j]=Get();
		}
	}
	build(1,1,m);
	Q=Get();
	char op[10];
	int x,y,z,k;
	for(int i=1;i<=Q;i++) {
		scanf("%s",op);
		if(op[0]=='m') {
			k=Get();
			x=k/m,y=k%m;
			h=query(1,sy,m);
			if(sy>1) h=h*query(1,1,sy-1);
			h=ksm(h,x);
			sx=h.g[sx];
			Move(y);
			cout<<sx<<" "<<sy<<'\n';
		} else {
			x=Get(),y=Get(),z=Get();
			w[x][y]=z;
			y--;
			if(y==0) y=m;
			Modify(1,y);
		}
	}
	return 0;
}


在這裏插入圖片描述
在這裏插入圖片描述

題目大意:一段區間每個區間有一個運算:+x,或者*x,或者^x(次方),運算優先級永遠從左到右。問給出一個初始值,經過了所有的運算之後模29393的值。
我們發現,如果沒有次方那就可以維護一個ax+ba*x+b,但顯然有了次方過後就不能合併了。我們有一個很暴力的想法,開一個數組,維護0~29392的每個數經過該段區間後會變成什麼。但顯然時間和空間複雜度太高。
然後就是一般的套路:29393=71317*19。所以我們對每個質因數開一個線段樹,最後的到了答案過後再用CRT合併。

代碼:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<ctime>
#include<queue>
#include<iomanip>
#define ll long long
#define N 50005
#define mod 29393

using namespace std;
inline int Get() {int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}while('0'<=ch&&ch<='9') {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*f;}

int n,m;
int op[N],w[N];
ll ksm(ll t,ll x,ll p) {
	ll ans=1;
	for(;x;x>>=1,t=t*t%p)
		if(x&1) ans=ans*t%p;
	return ans;
}
struct segment {
	int p; 
	struct tree {
		int l,r;
		int to[20];
	}tr[N<<2];
	void update(int v) {
		for(int i=0;i<p;i++)
			tr[v].to[i]=tr[v<<1|1].to[tr[v<<1].to[i]];
	}
	void build(int v,int l,int r) {
		tr[v].l=l,tr[v].r=r;
		if(l==r) {
			for(int i=0;i<p;i++) {
				if(op[l]==1) tr[v].to[i]=(i+w[l])%p;
				else if(op[l]==2) tr[v].to[i]=i*w[l]%p;
				else tr[v].to[i]=ksm(i,w[l],p);
			}
			return ;
		}
		int mid=l+r>>1;
		build(v<<1,l,mid),build(v<<1|1,mid+1,r);
		update(v);
	}
	void Modify(int v,int pos) {
		if(tr[v].l>pos||tr[v].r<pos) return ;
		if(tr[v].l==tr[v].r) {
			int l=tr[v].l;
			for(int i=0;i<p;i++) {
				if(op[l]==1) tr[v].to[i]=(i+w[l])%p;
				else if(op[l]==2) tr[v].to[i]=i*w[l]%p;
				else tr[v].to[i]=ksm(i,w[l],p);
			}
			return ;
		}
		Modify(v<<1,pos),Modify(v<<1|1,pos);
		update(v);
	}
}T[5];
void exgcd(ll a,ll b,ll &x,ll &y) {
	if(!b) {
		x=1,y=0;
		return ;
	}
	exgcd(b,a%b,y,x);
	y=y-a/b*x;
}
int CRT(int p,int k) {
	ll x,y;
	exgcd(mod/p,p,x,y);
	x*=k;
	x=(x%p+p)%p;
	return x*mod/p;
}
int main() {
	n=Get(),m=Get();
	char cm;
	for(int i=1;i<=n;i++) {
		while(cm=getchar(),cm!='^'&&cm!='*'&&cm!='+');
		if(cm=='+') op[i]=1;
		else if(cm=='*') op[i]=2;
		else op[i]=3;
		w[i]=Get();
	}
	T[1].p=7;
	T[2].p=13;
	T[3].p=17;
	T[4].p=19;
	for(int i=1;i<=4;i++) T[i].build(1,1,n);
	int x,x1,x2,x3,x4;
	int q;
	while(m--) {
		q=Get();
		if(q==1) {
			x=Get();
			x1=T[1].tr[1].to[x%T[1].p];
			x2=T[2].tr[1].to[x%T[2].p];
			x3=T[3].tr[1].to[x%T[3].p];
			x4=T[4].tr[1].to[x%T[4].p];
			ll ans=0;
			ans+=CRT(T[1].p,x1);
			ans+=CRT(T[2].p,x2);
			ans+=CRT(T[3].p,x3);
			ans+=CRT(T[4].p,x4);
			cout<<ans%mod<<"\n";
		} else {
			x=Get();
			while(cm=getchar(),cm!='^'&&cm!='*'&&cm!='+');
			if(cm=='+') op[x]=1;
			else if(cm=='*') op[x]=2;
			else op[x]=3;
			w[x]=Get();
			for(int i=1;i<=4;i++) T[i].Modify(1,x);
		}
	}
	return 0;
}


總結

線段樹維護映射對於我來說算是一個比較新的知識。遇到難以用一般標記合併的線段樹問題要想起這種高級的方法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章