常见的线段树是将左右两个区间的标记合并。然而有些时候传统的标记无法合并(比如不满足结合律的运算),这时候就需要用特殊的标记。这里说一种叫映射的标记。

下面有三道例题:
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;
}


总结

线段树维护映射对于我来说算是一个比较新的知识。遇到难以用一般标记合并的线段树问题要想起这种高级的方法。

相关文章: