二叉搜索树

二叉搜索树。
二叉搜索树是一棵二叉树。对于这棵树上的任意一个节点,右子树中的所有节点都大于它;左子树中的所有节点都小于它。
基于这个性质,我们可以以\(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;
}

 

lp2679 NOIP2015 子串

首先观察数据范围和题意,我们猜测这是一道DP题。
我们首先维护\(f_{i,j,k}\)表示,对于\(B\)中的前\(i\)个字符,在\(A\)中的前\(k\)个字符中,可以用\(j\)个子串来完成匹配。
那么,对于一个当前的字符\(i\),它如果需要匹配,我们有两种匹配方法:一是加到已有的串,二是新开一个串。
加到已有的串是很好转移的,直接\(f_{i+1,j,k+1}+=(A_{k+1}==B_{i+1})*f_{i,j,k}\);
而如果是新开一个串,朴素的考虑肯定是从\(k\)往后扫,然后把状态转移到所有之后的可行的位置:\(f_{i+1,j+1,S}+=f{i,j,k}\)。
但将一个状态转移到多个状态的加速转移是很难实现的,所以我们考虑使用填表法。
很容易可以发现,对于一个状态\(f_{i,j,k}\),它可以从上述的两种状态转移到。
故而我们可以得到一个朴素的转移方程:
\(f_{i,j,k}=(A_{k}==B_{i})*f_{i-1,j,k-1}+\sum_{S=i}^{k}f_{i-1,j-1,S}\)
观察可以发现,这个转移方程的复杂度瓶颈在于后面那个求和式——这使得整个转移的复杂度是\(O(n^{2}*m*k)\)的。
我们考虑优化转移。区间求和的转移优化已经可以说是套路了,我们只需要简单地维护一个前缀和即可。
用\(sm_{i,j,k}\)表示,要用\(1~k\)中的串\(j\)个串填充\(1~i\)的位置的方案数。
同时,\(40000000\)的空间复杂度对于\(128MB\)的内存限制来说是非常危险的。所以我们考虑滚动数组。
这样就做完了。

#include<iostream>
#include<cstdio>
/* 

*/
const long long MOD=1000000007;

int n,m,K;
long long f[2][205][1005],sm[2][205][1005];
char A[1005],B[205];
void init(){
    scanf("%d%d%d",&n,&m,&K);
    std::cin>>(A+1)>>(B+1);
    int nw=1;sm[0][0][0]=1;
    for(int i=1;i<=n;++i){
    	sm[nw][0][0]=1;
    	for(int j=1;j<=m;++j){
    		for(int k=1;k<=K;++k){
    			f[nw][j][k]=(A[i]==B[j])?((f[nw^1][j-1][k]+sm[nw^1][j-1][k-1])%MOD):0;
    			sm[nw][j][k]=(sm[nw^1][j][k]+f[nw][j][k])%MOD;
			}
		}
		nw^=1;
	}
    printf("%lld\n",sm[nw^1][m][K]);
}

int main(){
    init();
    return 0;
}