lp3391 【模板】文艺平衡树(Splay)

虽然说是模板题,但实际上时候在考察Splay一个很巧妙的运用,也就是用Splay模拟区间。
首先我们发现二叉排序树一个很巧妙的性质:对于一个点\(a\)和它的一个孩子\(b\),若令\(b\)是\(a\)的\(D\)方向的孩子,
那么区间\((a,b)\)内的所有数都在以\(b\)的\(D\ xor\ 1\)方向上的孩子为根的子树中。
然后我们考虑翻转操作。我们毫不惊讶地发现,当我们将一棵子树上的所有节点都左右调换以后,这棵子树所代表的区间就完成了翻转。
对于一个节点来说,它的位置就已经表示了它的大小。
但是,如果对于每一次翻转我们都翻转整棵子树的话,复杂度是不可接受的。我们考虑一种类线段树的操作方法,也就是使用\(Lazy Tag\)来维护翻转。
而,除了翻转操作之外,会改变树的结构的只有旋转操作。于是我们只需要在旋转之前下放\(x\)和\(fa\)的延迟标记即可。
另外尤其要注意不要旋转过头。

#include<iostream>
#include<cstdio>
class Splay{
	private:
		class Node{
			public:
				int v;
				int sz;
				int fa;
				int sn[2];
				int lzy;
		};
		Node tr[200005];
		int rt;
		inline void prnt(int X){
			if(!X){
				return;
			}
			pshd(X);
			prnt(tr[X].sn[0]);
			printf("%d ",tr[X].v);
			prnt(tr[X].sn[1]);
		}
		inline void debugPrnt(int X){
			if(!X){
				return;
			}
			pshd(X);
			debugPrnt(tr[X].sn[0]);
			printf("[%d,%d]{%d}:%d ",tr[X].sn[0],tr[X].sn[1],tr[X].fa,tr[X].v);
			debugPrnt(tr[X].sn[1]);
		}
		inline void pshd(int X){
			if(tr[X].lzy){
				tr[X].lzy^=1;
				tr[tr[X].sn[0]].lzy^=1,tr[tr[X].sn[1]].lzy^=1;
				std::swap(tr[X].sn[0],tr[X].sn[1]);
			}
		}
		inline void updt(int X){
			tr[X].sz=tr[tr[X].sn[0]].sz+tr[tr[X].sn[1]].sz+1;
		}
		inline int fndD(int X){
			return tr[tr[X].fa].sn[0]==X?0:1;
		}
		inline void splayOne(int X){
			int D=fndD(X),D2=fndD(tr[X].fa);
			pshd(tr[X].fa),pshd(X);
			tr[tr[X].sn[D^1]].fa=tr[X].fa,tr[tr[X].fa].sn[D]=tr[X].sn[D^1];
			tr[X].sn[D^1]=tr[X].fa,tr[X].fa=tr[tr[X].fa].fa;
			tr[tr[X].fa].sn[D2]=X,tr[tr[X].sn[D^1]].fa=X;
			updt(tr[X].sn[D^1]),updt(X);
		}
		inline void splayRnw(int X){
			while(tr[X].fa){
				splayOne(X);
			}
			rt=X; 
		}
		inline int fnd(int X){
			int P=rt,FP=0;
			while(P){
				pshd(P);
				FP=P;
				if(tr[tr[P].sn[0]].sz+1<X){
					X-=(tr[tr[P].sn[0]].sz+1);
					P=tr[P].sn[1];
				}else if(tr[tr[P].sn[0]].sz<X){
					return P;
				}else{
					P=tr[P].sn[0];
				}
			}
			return FP;
		}
	public:
		inline void build(int N){
			for(int i=1;i<=N;++i){
				tr[i].sn[0]=i-1;
				tr[i].v=i;
				tr[i].fa=i+1;
				tr[i].sz=1;
			}
			tr[N].fa=0;
			tr[1].sn[0]=N+1,tr[N].sn[1]=N+2;
			tr[N+1].v=-2147483647,tr[N+2].v=2147483647;
			tr[N+1].fa=1,tr[N+2].fa=N;
			tr[N+1].sz=1,tr[N+2].sz=1;
			rt=N;
			splayRnw(N+1);
			splayRnw(N+2);
		}
		inline void flp(int L,int R){
			//printf("[%d](%d,%d)\n",rt,fnd(L),fnd(R+2));
			splayRnw(fnd(L));
			splayRnw(fnd(R+2));
			tr[tr[tr[rt].sn[0]].sn[1]].lzy^=1;
		}
		inline void splayPrnt(int N){
			splayRnw(N+1);
			splayRnw(N+2);
			prnt(tr[tr[rt].sn[0]].sn[1]);
			puts("");
		}
		inline void debug(){
			printf("%d ->",rt);
			debugPrnt(rt);
			puts("");
		}
};
int n,q;
Splay T;
void init(){
	int n,q;
	scanf("%d%d",&n,&q);
	int l,r;
	T.build(n);
	for(int i=1;i<=q;++i){
		scanf("%d%d",&l,&r);
		T.flp(l,r);
	}
	T.splayPrnt(n);
}
int main(){
	init();
	return 0;
}

平衡树-Splay

我们将一棵以\(Splay\)方式维护的二叉搜索树封装为一个对象。
对于这个对象,我们定义它的一类私有对象\(Node\),代表二叉搜索树上的一个节点。
对于这个节点,在通常情况下,我们需要维护的信息包括:
\(sn\),这是一个有两个元素的数组。其中第\(0\)项表示左子节点,第\(1\)表示右子节点。
\(fa\),表示它的父节点。
\(v\),表示它的值。
\(nm\),表示值为\(v\)的数的个数。
\(sz\),表示所有子节点中的数的个数的和。

对于一棵二叉搜索树上的一个节点,我们定义它的旋转、单旋和双旋操作。
定义一个函数\(fndD(X)\),表示一个节点相对于它的父节点是\(fndD(X)\)子节点
那么,一个节点\(X\)的旋转操作为:
将它的\(fndD(X)\ xor\ 1\)子节点作为它的父节点的\(fndD(X)\)子节点,并将\(X\)作为它的祖父节点的\(fndD(X_fa)\)节点,然后将它原来的父节点变成它的\(fndD(X)\ xor \ 1\)节点。
定义一个节点的单旋操作为,将这个节点向上旋转两次。
定义一个节点的双旋操作为,将这个节点的父节点向上旋转一次,然后再将这个节点向上旋转一次。
同时,我们定义一个将一个节点一直依据下述方法单旋/双旋到根的操作为一个伸展操作:
对于一个相对于其父亲的位置与其父亲相对于其祖父的位置相同的节点,对它采用双旋操作。
对于一个相对于其父亲的位置与其父亲相对于其祖父的位置不同的节点,对它采用单旋操作。


下面我们用一种被称为势能分析法的复杂度分析方式来分析它的复杂度。

我们定义关于一个节点\(X\)的势能函数\(\phi(X)\),定义为它的子树大小取对数。我们定义关于整棵二叉搜索树\(T\)的一个势能函数\(\Phi (T)\),定义为它的所有节点的\(\phi\)值之和。

对于第\(i\)次操作,我们定义一个函数\(T\)和一个摊还代价辅助函数\(A\),其中\(T(i)\)表示第\(i\)次操作消耗的实际时间。\(A(i)\)由下述式子定义:
$$A_{i}=T_{i}+\Phi_{i}-\Phi_{i-1}$$
那么,我们可以得到公式:
$$A_{sum}=\sum_{i=1}^{q}A_{i}=\sum_{i=1}^{q}T_{i}+\Phi_{q}-\Phi_{0}$$
故而,我们只需要求出对于每一次操作的\(T\)与\(\Delta\Phi\)即可。

考虑到,每一个操作事实上都可以视为一次\(Splay\)操作,所以我们只需要考虑各类\(Splay\)操作造成的影响即可。
故而,我们需要计算的便是旋转、单旋和双旋对的操作复杂度以及它们对\(\Phi(T)\)的影响。

首先我们分析旋转操作。
容易知道,对于一次关于节点\(X\)的旋转操作,会改变\(\phi\)值的有且仅有两个节点:\(X\)和它的父亲\(Y\)。
我们定义\(X\)旋转后的节点为\(X’\),\(Y\)旋转后的节点为\(Y’\),很容易可以得到一个式子:
$$\Delta\Phi = \phi_{X’} – \phi_{X} + \phi_{Y’} – \phi_{Y}$$
考虑到旋转操作后,\(X’\)位于\(Y\)的位置,并且节点的数量没有增减、对父节点也没有影响,所以得到\(\phi_{X’}=\phi_{Y}\)
考虑\(Y’\)是\(X’\)的子节点,我们可以得到
$$ \phi_{X’} > \phi_{Y’}$$
将上述两式代入式中,可得:
$$\Delta\Phi < \phi_{X’} – \phi_{X}$$
于是可以证明,一次旋转的均摊复杂度上界是\(O(\phi_{X’}-\phi_{X})\)

然后我们分析单旋操作。
依然是定义一个关于节点\(X\)的旋转操作,定义它的父亲为\(Y\),祖父为\(Z\),旋转后分别为\(X’,Y’,Z’\)
很容易可以得到一个式子:
$$\Delta\Phi = \phi_{X’} – \phi_{X} + \phi_{Y’} – \phi_{Y} + \phi_{Z’} – \phi_{Z}$$
同理于旋转操作,我们可以得到:
$$\phi_{X’} = \phi_{Z}$$
故而:
$$\Delta\Phi = – \phi_{X} + \phi_{Y’} – \phi_{Y} + \phi_{Z’}$$
同理于旋转操作,我们可以得到:
$$\phi_{Y’}<\phi_{X’},\phi_{X}<\phi_{Y}$$ 故而:$$\Delta\Phi < \phi_{X’} + \phi_{Z’} – 2\phi_{X}$$$$\Delta\Phi < 2(\phi_{X’}-\phi_{X}) – \phi_{X’} – \phi_{Z’}$$又,$$\phi_{X’} + \phi_{Z’} = log_{2}{|X’||Z’|} > 2$$
所以,
$$\Delta\Phi < 2(\phi_{X’}-\phi_{X}) – 2$$

故而单旋操作的复杂度为\(O(\phi_{X’}-\phi_{X})\)

双旋操作的复杂度同理。

令总共进行了\(n\)次旋转操作,对于总体的复杂度上界,我们有:
$$A_{sum}<\sum_{i=1}^{n}\phi_{i}-\phi_{i-1}$$
故而得到最终的摊还复杂度:\(O(\phi_{n})\)


然而,上述的势能分析法仍然不够直观。下面我们考虑一种基于伸展操作的性质的另一种分析法。
引理一: 每一次伸展操作,必然会使得从这个节点到根的一条链的长度折半。
首先我们考虑单旋和双旋操作的使用情况。
我们称应当使用双旋的情况为一字型,应当使用单旋的情况为之字形,很容易可以发现,原来已有的一条链总是会被拆成两个部分。
而,详细地考虑各种情况,我们发现,不存在一种链,使得这种折半失效。
故而,我们得到了引理一。

下面考虑引理一是怎样在实际情况中发挥作用的。
我们说一个节点的深度可接受,指的是它的深度是\(log_2n\)的常数倍。
考虑对某一个节点进行操作。如果这个节点的深度可接受,那么操作它的复杂度也是可接受的。
如果这个节点的深度不可接受,根据引理一,总是可以在\(log_2n\)次操作以内将其的深度变为可接受的。
而每一次操作最多只会令这个链的深度加一,因此\(log_2n\)次操作的复杂度可以均摊到那些令这个链深度加一的操作上,从而使操作的均摊浮渣度仍然是可接受的。

注意:
1.三目运算符优先级比逗号高。
2.函数名不要搞混。
3.注意区分节点大小和子树大小。
4.当寻找前驱的时候,当前值与当前点的值相等应当向左走;当寻找后继时,当前值与当前点的值相等应当向右走。

#include<iostream>
#include<cstdio>
#include<cstring>
#define MAXQ 1000005
inline int Max(int A,int B){
    return A>B?A:B;
}
inline int Min(int A,int B){
    return A<B?A:B;
}
class Splay{
	private:
		class Node{
    		public:
    			int sn[2];
    			int fa;
    			int v;
    			int nm;
    			int sz;
    			inline void set(int FA,int LS,int RS,int V){
    			    fa=FA,v=V,sn[0]=LS,sn[1]=RS,nm=1,sz=1;
                }
                inline void init(){
                	sn[0]=sn[1]=fa=v=nm=sz=0;
				}
		};
		int INF;
		Node tr[MAXQ];
		int st[100005],tp,cnt,rt;
		inline int fndD(int X){
			return tr[tr[X].fa].sn[0]==X?0:1;
		}
		inline void szRnw(int X){
		    tr[X].sz=tr[tr[X].sn[0]].sz+tr[tr[X].sn[1]].sz+tr[X].nm;
        }
		inline void splayOne(int X){
			int D=fndD(X),D2=fndD(tr[X].fa);
			tr[tr[X].sn[D^1]].fa=tr[X].fa;tr[tr[X].fa].sn[D]=tr[X].sn[D^1];
			tr[X].sn[D^1]=tr[X].fa;tr[X].fa=tr[tr[X].sn[D^1]].fa;
			tr[tr[X].sn[D^1]].fa=X;tr[tr[X].fa].sn[D2]=X;
			szRnw(tr[X].sn[D^1]);szRnw(X);
			if(!tr[X].fa){
				rt=X;
			}
		}
		inline void splayTwo(int X){
		    int D=fndD(X),D2=fndD(tr[X].fa);
		    tr[X].fa?
				(tr[tr[X].fa].fa?
					(D==D2?
						(splayOne(tr[X].fa),splayOne(X),0)
						:(splayOne(X),splayOne(X),0))
					:(splayOne(X),0))
				:0;
		}
		inline void splayRnw(int X){
			while(tr[X].fa){
			    splayTwo(X);
            }
		}
		inline int nwlc(int FA,int LS,int RS,int V,int D){
			if(!cnt){
				rt=1;
			}
			int P=tp?st[tp--]:++cnt;
		    tr[FA].sn[D]=P;
		    tr[P].set(FA,LS,RS,V);
		    return P;
        }
        inline int fnd(int V){
            int P=rt,FP=0;
            while(P){
                FP=P;
                if(tr[P].v==V){
                    splayRnw(P);
                    return P;
                }
                P=tr[P].sn[tr[P].v>V?0:1];
            }
            return FP;
        }
        inline int fndMn(int X){
            int P=X,FP=tr[X].fa;
            while(P){
                FP=P;
                P=tr[P].sn[0];
            }
            return FP;
        }
        inline int fndMx(int X){
        	int P=X,FP=tr[X].fa;
        	while(P){
        		FP=P;
        		P=tr[P].sn[1];
			}
			return FP;
		}
		inline void clr(int X,int D){
			if(D==2){
				tr[X].init();
				st[++tp]=X;
				return;
			}
			rt=tr[X].sn[D],tr[tr[X].sn[D]].fa=0;
			tr[X].init();
			st[++tp]=X;
		}
		inline void init(int P){
			if(!tr[P].sn[0]||!tr[P].sn[1]){
				tr[P].sn[0]?(clr(P,0)):(tr[P].sn[1]?clr(P,1):clr(P,2));
			}else{
				int RS=fndMn(tr[P].sn[1]);
				tr[RS].sn[0]=tr[P].sn[0],tr[tr[P].sn[0]].fa=RS;
				clr(P,1);
				splayRnw(RS);
			}
			if(!tr[rt].v&&!tr[rt].sz&&!tr[rt].fa&&!tr[rt].nm){
				splayInit();
			}
		}
		inline void prnt(int X){
			if(!X){
				return;
			}
			prnt(tr[X].sn[0]);
			printf("(%d)%d:[%d,%d][%d] ",X,tr[X].v,tr[X].sn[0],tr[X].sn[1],tr[X].fa);
			prnt(tr[X].sn[1]);
		}
	public:
	    inline void psh(int V){
	        int P=rt,FP=0,D=0;
	        while(P){
	            FP=P;
	            if(tr[P].v==V){
	                ++tr[P].nm;
	                splayRnw(P);
	                return;
                }
                D=tr[P].v>V?0:1;
	            P=tr[P].sn[tr[P].v>V?0:1];
            }
            splayRnw(nwlc(FP,0,0,V,D));
        }
        inline void del(int V){
            int P=fnd(V);
            splayRnw(P);
            tr[P].nm>1?(--tr[P].nm):(init(P),0);
        }
        inline int fndPre(int V){
            int P=rt,RT=-INF;
            while(P){
                RT=tr[P].v<V?Max(RT,tr[P].v):RT;
                P=tr[P].sn[tr[P].v<V?1:0];
            }
            return RT;
        }
        inline int fndNxt(int V){
            int P=rt,RT=INF;
            while(P){
                RT=tr[P].v>V?Min(RT,tr[P].v):RT;
                P=tr[P].sn[tr[P].v>V?0:1];
            }
            return RT;
        }
        inline int rnk(int V){
            int P=rt,RT=0;
            while(P){
                if(tr[P].v>V){
                    P=tr[P].sn[0];
                }else if(tr[P].v==V){
                    RT+=tr[tr[P].sn[0]].sz;
                    splayRnw(P);
                    return RT+1;
                }else{
                    RT+=tr[tr[P].sn[0]].sz+tr[P].nm;
                    P=tr[P].sn[1]; 
                }
            }
            return RT+1;
        }
        inline int aRnk(int V){
            int P=rt;
            while(P){
                if(tr[tr[P].sn[0]].sz+tr[P].nm<V){
                    V-=(tr[tr[P].sn[0]].sz+tr[P].nm);
                    P=tr[P].sn[1];
                }else if(tr[tr[P].sn[0]].sz<V){
                    return tr[P].v;
                }else{
                    P=tr[P].sn[0];
                }
            }
            return P;
        }
        inline void splayInit(){
			INF=2147483647,tp=0,cnt=0,rt=0;
			memset(tr,0,sizeof(tr));
		}
		inline void splayPrnt(){
			prnt(rt);
			puts("");
		}
}; 
Splay T;
void init(){
	int n,x,op;
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		//T.splayPrnt();
		scanf("%d%d",&op,&x);
		switch(op){
			case 1:{
				T.psh(x);
				break;
			}
			case 2:{
				T.del(x);
				break;
			}
			case 3:{
				printf("%d\n",T.rnk(x));
				break;
			}
			case 4:{
				printf("%d\n",T.aRnk(x));
				break;
			}
			case 5:{
				printf("%d\n",T.fndPre(x));
				break;
			}
			case 6:{
				printf("%d\n",T.fndNxt(x));
				break;
			}
		}
	}
}
int main(){
//	freopen("Splay.out","w",stdout);
	T.splayInit();
	init();
	return 0;
}

二叉搜索树

二叉搜索树。
二叉搜索树是一棵二叉树。对于这棵树上的任意一个节点,右子树中的所有节点都大于它;左子树中的所有节点都小于它。
基于这个性质,我们可以以\(O(h)\)的时间的复杂度,来完成下述操作:
1.插入一个元素。
2.删除其中任意一个元素。
3.查找其中的一个元素。
4.查找第\(k\)大数。
5.询问某数的排名。
6.以常数更小的方式来查找一个数的前驱/后继。
7.以\(O(n)\)输出有序的整棵树。
8.查找将一个数插入树后的前驱/后继。
9.查找一个节点的子树大小。
下面我们一一讨论下述操作的实现。
操作6可能是最容易理解的一种操作。对于一个节点,如果要寻找它的后继,那么它的后继一定在如下两个位置之一:它的右子树,或者它的父亲。
如果它有右子树,那么它的后继一定在它的右子树中。这是因为,对于其他所有点,要么比它和它的右子树中的所有节点更大,要么比它们更小。这由二叉搜索树的基本性质可以轻松证明。
然后,它的后继一定是它的右子树中的最小点,这是因为右子树中的所有点都大于它。显然,这个最小点是右子树中最左的节点。我们称其为「后继右左定理」。
而上述的内容对于求一个有左子树的节点的前驱显然仍然是成立的。
如果它没有右子树,那么对于它的所有祖先,存在两种情况。
一是不存在任意一个祖先,使得这个祖先的父亲在这个祖先的右边,这种情况下,因为它没有右子树,所以它一定是整棵树中最大的节点,这样它就没有后继了;
另外一种是至少存在一个祖先,使得这个祖先的父亲在这个祖先的左边。
即,以这个祖先为根的整棵子树都是这个祖先的父亲的左子树,则此时很显然该节点是整棵子树中最右的节点。那么它显然是这个祖先的父亲的前驱。这可以根据「前驱左右定理」得出。
这样,我们就可以对任意的一个节点找到它的(如果存在)前驱和/或后继。
在有了操作6之后,我们就可以考虑操作1。如果插入的数比已有的任何数都大/都小,那么就一定可以插入到最右/左节点的右/左儿子。
如果不是这样的话,那么,它的大小一定在原来的一对大小相邻的数之间。
而综合「前驱左右定理」和「后继右左定理」,可以得出,一对大小相邻的节点之中,有且仅有一方的可以通过在子树中寻找来找到另一方。
这也就意味着,较大者的左子树和较小者的右子树中有且仅有一个是空的。那么,我们就一定可以把一个新的节点插入到一个空节点。
然后我们考虑操作3。它可以通过递归的二分操作来实现。假设我们当前在以\(x\)为根节点的子树中查找指定节点,
那么我们就可以根据二叉搜索树的性质,判断这个节点是\(x\),或位于\(x\)的某一棵子树中。
于是我们就可以递归查找任意一个数。
同理,操作8也是可以用相似的方法来实现。当这个数递归查找到一个无法继续查找的节点\(x\),
那么\(x\)一定是需要查找的数的前驱/后继,然后我们就可以通过操作6来查找这个数的后继/前驱。
接下来我们考虑操作4。操作4本质上也是一个树上二分的过程。我们首先计算出所有子树的大小,然后依据子树大小的单调性来递归查找第\(k\)大数。
于是我们考虑操作5,操作5可以在实现操作3的过程中实现。当每一次我们递归查找,我们同样也可以确定目标排名所在的一个区间,并且不断缩小这个区间。
事实上,每一次向下递归查找的过程,都确定了一部分的数一定大于或者小于目标数。当找到目标节点的时候,目标节点的区间就可以根据已有信息和它的左右子树(如果存在)来确定。
操作7就显得很显然了。通过中序遍历,我们可以以小-中-大的顺序来遍历整棵二叉树,也就将二叉树展开为一个有序序列。
它体现了二叉搜索树的性质的一个有趣的性质——对于任意一棵子树,它总是对应着有序序列上的一个连续区间。
这是因为,这棵子树之外的所有点,要么大于这棵子树中的所有点,要么小于这棵子树中的所有点。
另一方面,我们也可以从顶向下来理解这个过程。
对于根节点,它的左子树中的所有节点(如果存在)在序列中一定位于它左侧,而右子树(如果存在)中的所有节点在序列中一定位于它的右侧。
那么,递归地考虑每个子树,根据二叉搜索树的基本性质,它们都对应着一个有序的序列。所以它们总是对应着有序序列上的一个连续区间。
通过这种理解方式,我们也可以相应的理解(乃至证明)上诉的所有操作和性质。
下面是所有操作中最复杂的操作2,事实上操作2是唯一一个可能改变已有结构的操作。
对于需要删除的节点,存在三种情况。
情况一:它是一个叶节点。对于这种情况,我们只需要单纯地删除它,然后自底向顶更新子树大小。
情况二:它只有一个子节点。对于这种情况,我们只需要单纯地删除它,然后将它的子树接在它的父节点,然后自顶向上更新子树大小。
情况三:它既有左节点又有右节点。这种情况可能会比较复杂。首先我们知道,这个节点必然有前驱/后继。不妨将它的其他部分放在它的后继上。(前驱同理。)
这时候分两类讨论。
如果它的后继不是它的右子节点,那么可以将它的后继接在它的父节点,然后将它的右子树的其他部分作为它的后继的右子树。然后将它的左子树作为它的后继的左子树。
否则,我们将它的左节点放在它的后继的左节点处,然后将它的后继接在它的父节点。
对于操作9,我们只需要在每次更新的时候自顶向上计算贡献,存储好即可直接写出。
二叉搜索树是平衡树的基础,由于没有相关模板题,所以我也不太确定我的二叉搜索树有没有写挂。请各位多多指教。
 

#include<iostream>
#include<cstdio>
int st[100005],tp=0;
struct data{
    int v;int fa;int ls;int rs;int sz;int cnt;
}tr[100005];
int rot=0,trt=0;
inline int nwlc(){
    return tp?st[tp--]:++trt;
}
inline void init(int X){
    tr[X].cnt=tr[X].fa=tr[X].ls=tr[X].rs=tr[X].sz=tr[X].v=0;
    st[++tp]=X;
}
inline void rnw(int X){
    int P=X;
    while(P){
        tr[P].sz=tr[tr[P].ls].sz+tr[tr[P].rs].sz+tr[P].cnt;
        P=tr[P].fa;
    }
}
inline int nwpt(int V,int FA,int LS,int RS,int SZ){
    if(!rot){
        int P=1;trt=1;tp=0;rot=1;
        tr[P]=(data){V,0,0,0,1,1};
        return P;
    }
    int P=nwlc();
    tr[FA].v>V?tr[FA].ls=P:tr[FA].rs=P;
    tr[P]=(data){V,FA,LS,RS,SZ,1};
    return P;
}
inline int fndNm(int X){
    int P=rot,FP=0;
    while(P){
        FP=P;
        if(tr[P].v==X){
            FP=P;
            break;
        }
        P=X<tr[P].v?tr[P].ls:tr[P].rs;
    }
    return FP;
}
inline int fndMx(int X){
    int P=X,FP;
    while(P){
        FP=P;
        P=tr[P].rs;
    }
    return FP;
}
inline int fndMn(int X){
    int P=X,FP;
    while(P){
        FP=P;
        P=tr[P].ls;
    }
    return FP;
}
inline int fndPre(int X){
    if(tr[X].ls){
        return fndMx(tr[X].ls);
    }else{
        int P=X,NP;
        while(P){
            NP=P;
            P=tr[P].fa;
            if(tr[P].rs==NP){
                break;
            }
        }
        return P;
    }
}
inline int fndNxt(int X){
    if(tr[X].rs){
        return fndMn(tr[X].rs);
    }else{
        int P=X,NP;
        while(P){
            NP=P;
            P=tr[P].fa;
            if(tr[P].ls==NP){
                break;
            }
        }
        return P;
    }
}
inline int fndPP(int X){
    int MX=fndMx(rot);
    if(X<tr[MX].v){
        return MX;
    }
	int P=rot,FP=0;
	while(P){
		FP=P;
		if(tr[P].v==X){
			FP=P;
			break;
		}else{
			if(tr[P].v>X&&tr[tr[P].ls].v<X){
				FP=P;
				break;
			}else if(tr[P].v<X&&tr[tr[P].rs].v>X){
				FP=tr[P].rs;
				break;
			}
		}
		P=X<tr[P].v?tr[P].ls:tr[P].rs;
	}
	return fndPre(FP);
}
inline int fndNN(int X){
    int MN=fndMn(rot);
    if(X<tr[MN].v){
        return MN;
    }
	int P=rot,FP=0;
	while(P){
		FP=P;
		if(tr[P].v==X){
			FP=P;
			break;
		}else{
			if(tr[P].v>X&&tr[tr[P].ls].v<X){
				FP=tr[P].ls;
				break;
			}else if(tr[P].v<X&&tr[tr[P].rs].v>X){
				FP=P;
				break;
			}
		}
		P=X<tr[P].v?tr[P].ls:tr[P].rs;
	}
	return fndNxt(FP);
}
inline void trIns(int X){
    int P=rot,FP;
    while(P){
        FP=P;
        if(tr[P].v==X){
            ++tr[P].cnt;
            rnw(P);
            return;
        }
        P=(tr[P].v>X)?tr[P].ls:tr[P].rs;
    }
    rnw(nwpt(X,FP,0,0,1));
}
inline void cnnctP(int FA,int X,int SN){
    if(!SN){
        return;
    }
    tr[SN].fa=FA;
    tr[FA].ls==X?(tr[FA].ls=SN):(tr[FA].rs=SN);
}
inline void delP(int FA,int X,int SN){
    (tr[X].cnt>1)?--tr[X].cnt,cnnctP(FA,X,SN):cnnctP(FA,X,SN),init(X);
    rnw(SN?SN:FA);
}
inline void trDel(int X){
    if(!tr[X].ls&&!tr[X].rs){
        delP(tr[X].fa,X,0);
    }else if(tr[X].ls&&!tr[X].rs){
        delP(tr[X].fa,X,tr[X].ls);
    }else if(tr[X].rs&&!tr[X].ls){
        delP(tr[X].fa,X,tr[X].rs);
    }else{
        if(tr[X].rs==fndNxt(X)){
            tr[tr[X].ls].fa=tr[X].rs;
            tr[tr[X].rs].ls=tr[X].ls;
            delP(tr[X].fa,X,tr[X].rs);
        }else{
            int XN=fndNxt(X),LW;
            LW=tr[XN].fa;
            cnnctP(tr[XN].fa,XN,tr[XN].rs);
            tr[XN].rs=tr[X].rs;
            tr[tr[X].rs].fa=XN;
            tr[XN].ls=tr[X].ls;
            tr[tr[X].ls].fa=XN;
            tr[X].rs=XN;
            tr[XN].fa=X;
            delP(tr[X].fa,X,XN);
            rnw(LW);
        }
    }
}
inline int fndRnkN(int X){
    int P=rot;
    while(P){
        if(X<=tr[tr[P].ls].sz){
            X-=tr[tr[P].ls].sz;
            P=tr[P].ls;
        }else if(X<=tr[tr[P].ls].sz+tr[P].sz){
            return P;
        }else{
            X-=(tr[tr[P].ls].sz+tr[P].cnt);
            P=tr[P].rs;
        }
    }
    return -1;
}

inline int fndRnkS(int X){
    int P=rot,RT=0;
    while(P){
        P=(X<tr[P].v)?tr[P].ls:tr[P].rs,RT+=tr[tr[P].ls].sz;
    }
    return RT;
}
inline void trPrnt(int X){
    if(tr[X].ls){
        trPrnt(tr[X].ls);
    }
    printf("%d ",tr[X].v);
    if(tr[X].rs){
        trPrnt(tr[X].rs);
    }
}
int q;
void init(){
    scanf("%d",&q);
    int op,x;
    for(int i=1;i<=q;++i){
        scanf("%d%d",&op,&x);
        switch(op){
            case 1: {
                trIns(x);
                break;
            }
            case 2: {
                trDel(fndNm(x));
                break;
            }
            case 3: {
                printf("%d\n",fndRnkS(x));
                break;
            }
            case 4:{
                printf("%d\n",tr[fndRnkN(x)].v);
                break;
            }
            case 5: {
                printf("%d\n",tr[fndPP(x)].v);
                break;
            }
            case 6: {
                printf("%d\n",tr[fndNN(x)].v);
                break;
            }
        }
    }
} 
int main(){
    init();
    return 0;
}

 

lp1486 NOI2004 郁闷的出纳员

(似乎很少看见权值线段树的做法的样子。但是权值线段树如果动态开点、大胆卡常的话,是可以跑得飞快的。)
首先看一下题面,发现值域很小,很容易就可以想到用权值线段树维护。
支持:单点加,区间清零,查询第k大的权值线段树。
当然这题有一些细节。
一:如果初始工资低于工资下限,那么这个人就不存在。
二:维护的是第\(k\)大的而非第\(k\)小的。
三:由于是严格小于工资下限,要注意边界的开闭性。最好将公式写出来。

#include<iostream>
#include<cstdio>
using namespace std;
#define LS (X<<1)
#define RS (X<<1|1)
#define LEN (R-L+1)
#define MID ((L+R)>>1)
#define LLEN (MID-L+1)
#define RLEN (R-MID) 
//支持:单点加,区间清零,查询第k大的权值线段树。
int tr[2000005],cnt=0; 
inline void updt(int X){
	tr[X]=tr[LS]+tr[RS];
}
inline void pshd(int X,int L,int R){
	if(!tr[X]){
		tr[LS]=tr[RS]=0;
	}
}
inline void add(int X,int L,int R,int A){
	if(L==R){
		++tr[X];
		return;
	}
	pshd(X,L,R);
	if(A<=MID){
		add(LS,L,MID,A);
	}else{
		add(RS,MID+1,R,A);
	}
	updt(X);
}
inline void clr(int X,int L,int R,int A,int B){
	if(A<=L&&R<=B){
		cnt+=tr[X];
		tr[X]=0;
		return;
	}
	pshd(X,L,R);
	if(A<=MID){
		clr(LS,L,MID,A,B);
	}
	if(B>MID){
		clr(RS,MID+1,R,A,B);
	}
	updt(X);
}
inline int srch(int X,int L,int R,int K){
	if(L==R){
		return L;
	}
	pshd(X,L,R);
	if(tr[RS]<K){
		return srch(LS,L,MID,K-tr[RS]);
	}else{
		return srch(RS,MID+1,R,K);
	}
}
inline int qry(int X,int L,int R,int A,int B){
	if(A<=L&&R<=B){
		return tr[X];
	}
	pshd(X,L,R);
	int rt=0;
	if(A<=MID){
		rt+=qry(LS,L,MID,A,B);
	}
	if(B>MID){
		rt+=qry(RS,MID+1,R,A,B);
	}
	updt(X);
	return rt;
}
//1 k 插入大小为k的点
//2 k 全部数加上k
//3 k 全部数减去k
//4 k 查询第k大的值 
int n,m,tg=0,k;
char op[4];
void init(){
	scanf("%d%d",&n,&m);
	--m;
	for(int i=1;i<=n;++i){
		cin>>op;
		scanf("%d",&k);
		if(op[0]=='I'){
			//注意大于与大等于。 
			if(k>m){
				add(1,1,300001,k+100001+tg);
			}
		}else if(op[0]=='A'){
			tg-=k; 
		}else if(op[0]=='S'){
			tg+=k;
			clr(1,1,300001,1,m+100001+tg);
		}else if(op[0]=='F'){
			if(qry(1,1,300001,1,300001)<k){
				puts("-1");
			}else{
				printf("%d\n",srch(1,1,300001,k)-100001-tg);
			}
		}
	}
	printf("%d",cnt);
}
int main(){
	init();
	return 0;
}

 

lp2572 SCOI2010 序列操作

操作要求:
0 a b 把[a, b]区间内的所有数全变成0
1 a b 把[a, b]区间内的所有数全变成1
2 a b 把[a,b]区间内的所有数全部取反,也就是说把所有的0变成1,把所有的1变成0
3 a b 询问[a, b]区间内总共有多少个1
4 a b 询问[a, b]区间内最多有多少个连续的1
我们考虑维护线段树来处理这些询问。
考虑线段树的基本性质。线段树的特性决定了它能够\(O(nlogn)\)来处理各种能够\(O(1)\)合并两个子区间信息的信息。
上述询问中,总共有多少个1是很容易可以维护的。最大的问题在于维护“最多有多少个连续的1”
实际上,对于这里的每一种信息,我们都需要对称地维护它们——即,在维护1的信息的同时也要维护0的信息。这是操作\(2\)导致的。
我们考虑对于两种询问分别考虑。
首先是\(3\),这个很好维护,直接维护\(1\)的个数即可。合并的时候简单相加即可。
对于\(4\),第一个想到的肯定是可以维护区间的最长连续1。
但是我们发现,仅仅维护这个信息还是不够的。因为这样是无法把子区间的信息合并的。
我们反过来考虑,对于一个区间,它的最长连续1有可能怎样从两个子区间转移过来。
首先,可能是完全被包含在两个区间之一。这种情况的话,只需要维护区间的最长连续1,然后取Max即可。
但是,还有可能这个区间是跨越了两个区间的。这时候我们需要维护,子区间左起最长连续1与右起最长连续1。
然后,在转移区间信息的时候,我们只需要再考虑两个子区间的左起最长连续1与右起最长连续1即可。
接着我们考虑对于端点起最长连续1的转移——这种转移存在一种特殊情况,也就是,这个区间的端点起最长连续1的长度超过了中点。
这时候需要特殊判定。
这样就做完了。
写了5k,的确是一道麻题。

另,我莫名其妙RE了,估计是不知道哪里边界判挂了。考试的时候要注意在不MLE的情况下多开几倍的数组保险。

#include<iostream>
#include<cstdio>

#define LS (X<<1)
#define RS (X<<1|1)
#define MID ((L+R)>>1)
#define LEN (R-L+1)
#define LLEN (MID-L+1)
#define RLEN (R-MID)
int n,m,a[100005],A,B,op;
inline int Max(int AA,int BB){
    return AA>BB?AA:BB;
}
struct data{ 
    int sm0;int sm1;
    int mx0;int mx1;
    int lmx0;int lmx1;
    int rmx0;int rmx1;
    //有翻转标记为1,否则为0;没有赋值为-1,有为0或1; 
    int lzy;int st;
    data(int sm0=0,int sm1=0,int mx0=0,int mx1=0,int lmx0=0,int lmx1=0,int rmx0=0,int rmx1=0,int lzy=0,int st=-1):
        sm0(sm0),sm1(sm1),mx0(mx0),mx1(mx1),lmx0(lmx0),lmx1(lmx1),rmx0(rmx0),rmx1(rmx1),lzy(lzy),st(st){}
}tr[300005];//262144
inline data mrg(const data &X,const data &Y,const data BS){
    data RT;
    RT.mx0=Max(X.mx0,Y.mx0);
    RT.mx0=Max(RT.mx0,X.rmx0+Y.lmx0);
    RT.sm0=X.sm0+Y.sm0;
    //一个巧妙的技巧,如果存在1,那么肯定不是全0,反之亦然。 
    RT.lmx0=X.sm1?X.lmx0:X.lmx0+Y.lmx0;
    RT.rmx0=Y.sm1?Y.rmx0:Y.rmx0+X.rmx0;
    
    RT.mx1=Max(X.mx1,Y.mx1);
    RT.mx1=Max(RT.mx1,X.rmx1+Y.lmx1);
    RT.sm1=X.sm1+Y.sm1;
    RT.lmx1=X.sm0?X.lmx1:X.lmx1+Y.lmx1;
    RT.rmx1=Y.sm0?Y.rmx1:Y.rmx1+X.rmx1;
    RT.lzy=BS.lzy;RT.st=BS.st;
    return RT;
}
inline void updt(int X){
    tr[X]=mrg(tr[LS],tr[RS],tr[X]);
}
inline void rnw1(int X,int L,int R){
    std::swap(tr[X].lmx0,tr[X].lmx1);
    std::swap(tr[X].rmx0,tr[X].rmx1);
    std::swap(tr[X].mx0,tr[X].mx1);
    std::swap(tr[X].sm0,tr[X].sm1);
}
inline void rnw20(int X,int L,int R){
    tr[X].lmx0=LEN;tr[X].lmx1=0;
    tr[X].rmx0=LEN;tr[X].rmx1=0;
    tr[X].mx0=LEN;tr[X].mx1=0;
    tr[X].sm0=LEN;tr[X].sm1=0;
}
inline void rnw21(int X,int L,int R){
    tr[X].lmx0=0;tr[X].lmx1=LEN;
    tr[X].rmx0=0;tr[X].rmx1=LEN;
    tr[X].mx0=0;tr[X].mx1=LEN;
    tr[X].sm0=0;tr[X].sm1=LEN;
}
inline void rnw(int X,int L,int R,int TYP){
    if(TYP==0){
        tr[X].lzy=0;tr[X].st=0;
        rnw20(X,L,R);
    }else if(TYP==1){
        tr[X].lzy=0;tr[X].st=1;
        rnw21(X,L,R);
    }else{
        tr[X].lzy^=1;
        rnw1(X,L,R);
    }
}
inline void pshd(int X,int L,int R){
    if(tr[X].st>-1){
        rnw(LS,L,MID,tr[X].st);rnw(RS,MID+1,R,tr[X].st);
        tr[X].st=-1;
    }
    if(tr[X].lzy){
        rnw(LS,L,MID,2);rnw(RS,MID+1,R,2);
        tr[X].lzy=0;
    }
}
inline void chg(int X,int L,int R,int TYP){
    if(A<=L&&R<=B){
        //如果完全被包含则直接更新。 
        rnw(X,L,R,TYP);
        return;
    }
    pshd(X,L,R);
    if(A<=MID){
        chg(LS,L,MID,TYP);
    }
    if(B>MID){
        chg(RS,MID+1,R,TYP);
    }
    updt(X);
}
inline data qry(int X,int L,int R){
    if(A<=L&&R<=B){
        pshd(X,L,R);
        return tr[X];
    }
    pshd(X,L,R);
    if(A<=MID){
        if(B>MID){
            return mrg(qry(LS,L,MID),qry(RS,MID+1,R),data());
        }else{
            return qry(LS,L,MID);
        }
    }else{
        if(B>MID){
            return qry(RS,MID+1,R);
        }else{
            return data();
        }
    }
}

inline void build(int X,int L,int R){
    if(L==R){
        tr[X]=(data){a[L]^1,a[L],a[L]^1,a[L],a[L]^1,a[L],a[L]^1,a[L],0,-1};
        return;
    }
    build(LS,L,MID);
    build(RS,MID+1,R);
    updt(X);
}
void init(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){
        scanf("%d",&a[i]);
    }
    build(1,1,n);
    data ans;
    for(int i=1;i<=m;++i){
        scanf("%d%d%d",&op,&A,&B);
        ++A,++B;
        if(op<3){
            chg(1,1,n,op);
        }else if(op==3||op==4){
            ans=qry(1,1,n);
            if(op==3){
                printf("%d\n",ans.sm1);
            }else if(op==4){
                printf("%d\n",ans.mx1);
            }
        }
    }
}
int main(){
    init();
    return 0;
}

 

lp1471 方差

大力上一个线段树。
将方差展开为由区间平方的和与区间和两项构成的多项式,然后维护区间平方的和与区间和。
具体来说,通过将二项式展开可以得到公式:
$$ \sum_{i=l}^{r}(a_{i}+k)^2=\sum_{i=l}^{r}a_{i}^2+\sum_{i=l}^{r}*k*2+(r-l+1)*k^2 $$
套用这个公式,就可以用与维护普通的线段树相似的方法来维护这个数据结构。

#include<iostream>
#include<cstdio>
using namespace std;
#define LEN (R-L+1)
#define MID ((L+R)>>1)
#define LLEN (MID-L+1)
#define RLEN (R-MID)
#define LS (X<<1)
#define RS (X<<1|1)
struct data{
	double sm;
	double sm2;
	double lzy;
}tr[270005];//262141
inline void rnw(int X,int Len,double K){
	tr[X].sm2+=(tr[X].sm*2*K+K*K*Len);
	tr[X].sm+=(K*Len);
	tr[X].lzy+=K;
}
inline void pshd(int X,int L,int R){
	rnw(LS,LLEN,tr[X].lzy);rnw(RS,RLEN,tr[X].lzy);
	tr[X].lzy=0;
}
inline void updt(int X){
	tr[X].sm2=tr[LS].sm2+tr[RS].sm2;
	tr[X].sm=tr[LS].sm+tr[RS].sm;
}
inline void add(int X,int L,int R,int A,int B,double K){
	if(L>=A&&R<=B){
		rnw(X,LEN,K);
		return;
	}
	pshd(X,L,R);
	if(A<=MID){
		add(LS,L,MID,A,B,K);
	}
	if(B>MID){
		add(RS,MID+1,R,A,B,K);
	}
	updt(X);
}
inline double qrySm(int X,int L,int R,int A,int B){
	if(L>=A&&R<=B){
		return tr[X].sm;
	}
	pshd(X,L,R);
	double rt=0;
	if(A<=MID){
		rt+=qrySm(LS,L,MID,A,B);
	}
	if(B>MID){
		rt+=qrySm(RS,MID+1,R,A,B);
	}
	return rt;
}
inline double qrySm2(int X,int L,int R,int A,int B){
	if(L>=A&&R<=B){
		return tr[X].sm2;
	}
	pshd(X,L,R);
	double rt=0;
	if(A<=MID){
		rt+=qrySm2(LS,L,MID,A,B);
	}
	if(B>MID){
		rt+=qrySm2(RS,MID+1,R,A,B);
	}
	return rt;
}
int n,m;
double a[100005];
inline void build(int X,int L,int R){
	if(L==R){
		tr[X].sm=a[L];
		tr[X].sm2=a[L]*a[R];
		return;
	}
	build(LS,L,MID);build(RS,MID+1,R);
	updt(X);
}
void init(){
	scanf("%d%d",&n,&m);
	double x;
	for(int i=1;i<=n;++i){
		scanf("%lf",a+i);
	}
	build(1,1,n);
	int op,l,r;
	while(m--){
		scanf("%d%d%d",&op,&l,&r);
		if(op==1){
			scanf("%lf",&x);
			add(1,1,n,l,r,x);
		}else if(op==2){
			printf("%.4lf\n",qrySm(1,1,n,l,r)/(r-l+1));
		}else{
			x=qrySm(1,1,n,l,r);
			printf("%.4lf\n",(qrySm2(1,1,n,l,r)-x*x/(r-l+1))/(r-l+1));
		}
	}
}
int main(){
	init();
	return 0;
}

 

lp3605 USACO Promotion Counting晋升者计数

题意是求树上逆序对。
统计方式基本和序列逆序对相同。但是需要注意的是,由于树上逆序对需要求的是,一个节点的「子孙节点」中权值比它大的个数,故而可以求差:
具体地来说,在每一次搜索到一个节点之前,把已有的比它大的个数先记录下来,然后往下搜索。在搜索完毕之后将比它大的个数的结果减去原有的比它大的数量,这就是答案。
很容易可以知道这个增量一定是在搜索它的子孙节点的时候增加的。
记住填写的变量到底是\(X\)还是\(b_{X}\).