我们尝试用序列之王spaly来解决线段树经典两个问题:
最大值和最大值2。
在N(1<=N<=100000)个数A1…An组成的序列上进行M(1<=M<=100000)次操作,操作有两种:
1 x y:表示修改A[x]为y;
2 x y:询问x到y之间的最大值。
这是线段树很容易解决的问题。
我们现在考虑用splay解决。
我们设key[i]表示结点i的值。
那么初始情况下key[i]=a[i]。
设num[i]表示以结点i为根的子树中的最大值。
我们将n个结点弄成一条链。
然后旋最小的至根进行平衡调整。
接下来我们可以打出所有splay基本操作:
pd(x):判断x是否为其父亲的右子树。
updata(x):更新结点x的值(例如num就需要更新)。
rotate(x):将结点x往上旋(需要用到函数pd)。
splay(x,y):将结点x旋至结点y下方(如果y=0相当于旋到顶)。
kth(x,y):在以结点x为根的子树中查找第y小的结点。
split(x,y,l,r):在以结点x为根的子树中(注意father[x]为0),从第y小的元素后分开。让前y个元素变为一棵splay,其余变为一棵splay,其中前者根节点赋值给l,后者根节点赋值给r。
merge(l,r,x):将以结点l为根的splay和以结点r为根的splay合并成一棵splay(father[l]=0,father[r]=0),得到的新splay根节点赋值给x。
为了支持kth,merge操作,我们需要维护size[i]表示以i为根的子树中有多少结点。
为了更好更优美的执行split和merge操作[即使分裂后两棵splay均非空],我们在第一个元素添加虚拟元素[虚拟结点]n+1,在最后一个元素后添加虚拟元素[虚拟结点]n+2。
这些操作的实现不讲,不懂的可以去学习然后看一下参考程序的实现。
注意的是虚拟结点和空结点的num值和key值都得赋值为-inf。
#include
#include
#include
#define fo(i,a,b) for(i=a;i<=b;i++)
#define maxn (100000+5)
#define inf (INT_MAX)
using namespace std;
int tree[maxn][3],father[maxn],key[maxn],num[maxn],size[maxn];
int i,j,k,l,r,mid,t,n,m,tot,root,x,y;
int pd(int x){
if (tree[father[x]][0]==x) return 0;else return 1;
}
void updata(int x){
num[x]=max(key[x],max(num[tree[x][0]],num[tree[x][1]]));
size[x]=size[tree[x][0]]+size[tree[x][1]]+1;
}
void rotate(int x){
int y=father[x],z=pd(x);
tree[y][z]=tree[x][1-z];
if (tree[x][1-z]) father[tree[y][z]]=y;
father[x]=father[y];
if (father[y]) tree[father[y]][pd(y)]=x;
father[y]=x;
tree[x][1-z]=y;
updata(y);
updata(x);
}
void splay(int x,int y){
while (father[x]!=y){
if (father[father[x]]!=y)
if (pd(x)==pd(father[x])) rotate(x);else rotate(father[x]);
rotate(x);
}
}
int kth(int x,int y){
if (size[tree[x][0]]+1==y) return x;
else if (size[tree[x][0]]+1>y) return kth(tree[x][0],y);
else return kth(tree[x][1],y-size[tree[x][0]]-1);
}
void split(int x,int y,int &l,int &r){
int j=kth(x,y);
splay(j,0);
int k=tree[j][1];
tree[j][1]=0;
father[k]=0;
l=j;
r=k;
updata(j);
}
void merge(int l,int r,int &x){
int j=kth(l,size[l]);
splay(j,0);
tree[j][1]=r;
father[r]=j;
x=j;
updata(j);
}
int main(){
scanf("%d",&n);
key[0]=key[n+2]=key[n+1]=-inf;
num[n+1]=num[0]=-inf;
size[0]=0;
size[n+1]=size[n+2]=1;
fo(i,1,n){
scanf("%d",&key[i]);
if (i>1){
tree[i][0]=i-1;
father[i-1]=i;
updata(i);
}
else{
tree[1][0]=n+1;
father[n+1]=1;
updata(1);
}
}
tree[n+2][0]=n;
father[n]=n+2;
updata(n+2);
root=n+2;
splay(n+1,0);
root=n+1;
scanf("%d",&m);
fo(i,1,m){
scanf("%d%d%d",&t,&x,&y);
if (t==1){
splay(x,0);
root=x;
key[x]=y;
updata(x);
}
else{
split(root,y+1,l,r);
split(l,x,l,mid);
printf("%d\n",num[mid]);
merge(l,mid,l);
merge(l,r,root);
}
}
}
这次要求我们支持区间修改(区间都加上一个数,可以是负数),怎么办?
我们学习线段树,可以打懒标记。
因此add[i]表示以i结点为根的splay全部加上add[i]。
当一个j需要进行splay操作时,沿途的结点就进行懒标记下传。
那么我们只需要在第一题的基础上加两个操作:
remove(x,y):在进行splay(x,y)时,对沿途的结点进行懒标记下传。
clear(x):对结点x进行懒标记下传。
然后我们发现并不能对,在答案为负数的情况下会输出0。
?
其实,是因为虚拟结点它是真实不存在的,因此你不能改变它的key值。而在虚拟结点没有左右子树时,也不能改变它的num值。
特殊处理一下即可。具体看程序实现。
为什么第一题不会出现这种情况?因为第一题不需要区间修改,就没有懒标记下传。虚拟结点的key永远不会被修改。
#include
#include
#include
#include
#define fo(i,a,b) for(i=a;i<=b;i++)
#define maxn (100000+5)
#define inf (INT_MAX)
using namespace std;
stack <int> s;
int tree[maxn][3],father[maxn],key[maxn],num[maxn],size[maxn],add[maxn];
int i,j,k,l,r,mid,t,n,m,tot,root,x,y;
int pd(int x){
if (tree[father[x]][0]==x) return 0;else return 1;
}
void updata(int x){
num[x]=max(key[x],max(num[tree[x][0]],num[tree[x][1]]));
size[x]=size[tree[x][0]]+size[tree[x][1]]+1;
}
void rotate(int x){
int y=father[x],z=pd(x);
tree[y][z]=tree[x][1-z];
if (tree[x][1-z]) father[tree[y][z]]=y;
father[x]=father[y];
if (father[y]) tree[father[y]][pd(y)]=x;
father[y]=x;
tree[x][1-z]=y;
updata(y);
updata(x);
}
void clear(int x){
if (add[x]){
if (tree[x][0]){
if (tree[x][0]<=n||(tree[x][0]>n&&size[tree[x][0]]>1))
num[tree[x][0]]+=add[x];
if (tree[x][0]<=n)
key[tree[x][0]]+=add[x];
add[tree[x][0]]+=add[x];
}
if (tree[x][1]){
if (tree[x][1]<=n||(tree[x][1]>n&&size[tree[x][1]]>1))
num[tree[x][1]]+=add[x];
if (tree[x][1]<=n)
key[tree[x][1]]+=add[x];
add[tree[x][1]]+=add[x];
}
add[x]=0;
}
}
void remove(int x,int y){
do{
s.push(x);
x=father[x];
}while (x!=y);
while (!s.empty()){
x=s.top();
clear(x);
s.pop();
}
}
void splay(int x,int y){
remove(x,y);
while (father[x]!=y){
if (father[father[x]]!=y)
if (pd(x)==pd(father[x])) rotate(x);else rotate(father[x]);
rotate(x);
}
}
int kth(int x,int y){
if (size[tree[x][0]]+1==y) return x;
else if (size[tree[x][0]]+1>y) return kth(tree[x][0],y);
else return kth(tree[x][1],y-size[tree[x][0]]-1);
}
void split(int x,int y,int &l,int &r){
int j=kth(x,y);
splay(j,0);
int k=tree[j][1];
tree[j][1]=0;
father[k]=0;
l=j;
r=k;
updata(j);
}
void merge(int l,int r,int &x){
int j=kth(l,size[l]);
splay(j,0);
tree[j][1]=r;
father[r]=j;
x=j;
updata(j);
}
int main(){
scanf("%d",&n);
key[0]=key[n+2]=key[n+1]=-inf;
num[n+1]=num[0]=-inf;
size[0]=0;
size[n+1]=size[n+2]=1;
fo(i,1,n){
scanf("%d",&key[i]);
if (i>1){
tree[i][0]=i-1;
father[i-1]=i;
updata(i);
}
else{
tree[1][0]=n+1;
father[n+1]=1;
updata(1);
}
}
tree[n+2][0]=n;
father[n]=n+2;
updata(n+2);
root=n+2;
splay(n+1,0);
root=n+1;
scanf("%d",&m);
fo(i,1,m){
scanf("%d%d%d",&t,&x,&y);
if (t==1){
scanf("%d",&t);
split(root,y+1,l,r);
split(l,x,l,mid);
num[mid]+=t;
key[mid]+=t;
add[mid]+=t;
merge(l,mid,l);
merge(l,r,root);
}
else{
split(root,y+1,l,r);
split(l,x,l,mid);
printf("%d\n",num[mid]);
merge(l,mid,l);
merge(l,r,root);
}
}
}
实际上splay还可以做很多线段树做不了的东西。
例如翻转操作(排序机械臂)。
可以维护连通性(网络通信)。
序列之王,维护序列(维护序列是一道题目)。
大家可以去做。