0%

树链剖分(洛谷 P3384)

前置芝士

LCA、线段树、DFS序

解决的问题

将树从x到y结点最短路径上所有节点的值都加上z
求树从x到y结点最短路径上所有节点的值之和
将以x为根节点的子树内所有节点值都加上z
求以x为根节点的子树内所有节点值之和

概念

重儿子:对于每一个非叶子节点,它的儿子中 儿子数量最多的那一个儿子 为该节点的重儿子
轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子
叶子节点没有重儿子也没有轻儿子(因为它没有儿子。。)
重边:连接任意两个重儿子的边叫做重边
轻边:剩下的即为轻边
重链:相邻重边连起来的 连接一条重儿子 的链叫重链
对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链
每一条重链以轻儿子为起点

dfs1

这个dfs要处理几件事情:

标记每个点的深度dep[]
标记每个点的父亲fa[]
标记每个非叶子节点的子树大小(含它自己)
标记每个非叶子节点的重儿子编号son[]

inline void dfs1(int x,int f,int deep){//x当前节点,f父亲,deep深度 
    dep[x]=deep;//标记每个点的深度 
    fa[x]=f;//标记每个点的父亲 
    siz[x]=1;//标记每个非叶子节点的子树大小 
    int maxson=-1;//记录重儿子的儿子数 
    for(Rint i=beg[x];i;i=nex[i]){
        int y=to[i];
        if(y==f)continue;//若为父亲则continue 
        dfs1(y,x,deep+1);//dfs其儿子 
        siz[x]+=siz[y];//把它的儿子数加到它身上 
        if(siz[y]>maxson)son[x]=y,maxson=siz[y];//标记每个非叶子节点的重儿子编号 
    }
}

dfs2

这个dfs2也要预处理几件事情

标记每个点的新编号
赋值每个点的初始值到新编号上
处理每个点所在链的顶端
处理每条链

顺序:先处理重儿子再处理轻儿子,理由后面说

inline void dfs2(int x,int topf){//x当前节点,topf当前链的最顶端的节点 
    id[x]=++cnt;//标记每个点的新编号 
    wt[cnt]=w[x];//把每个点的初始值赋到新编号上来 
    top[x]=topf;//这个点所在链的顶端 
    if(!son[x])return;//如果没有儿子则返回 
    dfs2(son[x],topf);//按先处理重儿子,再处理轻儿子的顺序递归处理 
    for(Rint i=beg[x];i;i=nex[i]){
        int y=to[i];
        if(y==fa[x]||y==son[x])continue;
        dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链 
    }
}

处理问题

Attention 重要的来了!!! 前面说到dfs2的顺序是先处理重儿子再处理轻儿子 我们来模拟一下:

  • 因为顺序是先重再轻,所以每一条重链的新编号是连续的
  • 因为是dfs,所以每一个子树的新编号也是连续的

现在回顾一下我们要处理的问题

  • 处理任意两点间路径上的点权和
  • 处理一点及其子树的点权和
  • 修改任意两点间路径上的点权
  • 修改一点及其子树的点权

1、当我们要处理任意两点间路径时: 设所在链顶端的深度更深的那个点为x点

  • ans加上x点到x所在链顶端 这一段区间的点权和
  • 把x跳到x所在链顶端的那个点的上面一个点

不停执行这两个步骤,直到两个点处于一条链上,这时再加上此时两个点的区间和即可

这时我们注意到,我们所要处理的所有区间均为连续编号(新编号),于是想到线段树,用线段树处理连续编号区间和每次查询时间复杂度为O(log^2n)

inline int qRange(int x,int y){
    int ans=0;
    while(top[x]!=top[y]){//当两个点不在同一条链上 
        if(dep[top[x]]<dep[top[y]])swap(x,y);//把x点改为所在链顶端的深度更深的那个点
        res=0;
        query(1,1,n,id[top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
        ans+=res;
        ans%=mod;//按题意取模 
        x=fa[top[x]];//把x跳到x所在链顶端的那个点的上面一个点
    }
    //直到两个点处于一条链上
    if(dep[x]>dep[y])swap(x,y);//把x点深度更深的那个点
    res=0;
    query(1,1,n,id[x],id[y]);//这时再加上此时两个点的区间和即可
    ans+=res;
    return ans%mod;
}

2、处理一点及其子树的点权和:

想到记录了每个非叶子节点的子树大小(含它自己),并且每个子树的新编号都是连续的

于是直接线段树区间查询即可

时间复杂度为O(logn)

inline int qSon(int x){
    res=0;
    query(1,1,n,id[x],id[x]+siz[x]-1);//子树区间右端点为id[x]+siz[x]-1 
    return res;
}

当然,区间修改就和区间查询一样的啦~~

inline void updRange(int x,int y,int k){
    k%=mod;
    while(top[x]!=top[y]){
        if(dep[top[x]]<dep[top[y]])swap(x,y);
        update(1,1,n,id[top[x]],id[x],k);
        x=fa[top[x]];
    }
    if(dep[x]>dep[y])swap(x,y);
    update(1,1,n,id[x],id[y],k);
}

inline void updSon(int x,int k){
    update(1,1,n,id[x],id[x]+siz[x]-1,k);
}

完整代码

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdio>
#define Rint register int
#define mem(a,b) memset(a,(b),sizeof(a))
#define Temp template<typename T>
using namespace std;
typedef long long LL;
Temp inline void read(T &x){
    x=0;T w=1,ch=getchar();
    while(!isdigit(ch)&&ch!='-')ch=getchar();
    if(ch=='-')w=-1,ch=getchar();
    while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^'0'),ch=getchar();
    x=x*w;
}

#define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
#define len (r-l+1)

const int maxn=200000+10;
int n,m,r,mod;
//见题意 
int e,beg[maxn],nex[maxn],to[maxn],w[maxn],wt[maxn];
//链式前向星数组,w[]、wt[]初始点权数组 
int a[maxn<<2],laz[maxn<<2];
//线段树数组、lazy操作 
int son[maxn],id[maxn],fa[maxn],cnt,dep[maxn],siz[maxn],top[maxn]; 
//son[]重儿子编号,id[]新编号,fa[]父亲节点,cnt dfs_clock/dfs序,dep[]深度,siz[]子树大        小,top[]当前链顶端节点 
int res=0;
//查询答案 

inline void add(int x,int y){//链式前向星加边 
    to[++e]=y;
    nex[e]=beg[x];
    beg[x]=e;
}
//-------------------------------------- 以下为线段树 
inline void pushdown(int rt,int lenn){
    laz[rt<<1]+=laz[rt];
    laz[rt<<1|1]+=laz[rt];
    a[rt<<1]+=laz[rt]*(lenn-(lenn>>1));
    a[rt<<1|1]+=laz[rt]*(lenn>>1);
    a[rt<<1]%=mod;
    a[rt<<1|1]%=mod;
    laz[rt]=0;
}

inline void build(int rt,int l,int r){
    if(l==r){
        a[rt]=wt[l];
        if(a[rt]>mod)a[rt]%=mod;
        return;
    }
    build(lson);
    build(rson);
    a[rt]=(a[rt<<1]+a[rt<<1|1])%mod;
}

inline void query(int rt,int l,int r,int L,int R){
    if(L<=l&&r<=R){res+=a[rt];res%=mod;return;}
    else{
        if(laz[rt])pushdown(rt,len);
        if(L<=mid)query(lson,L,R);
        if(R>mid)query(rson,L,R);
    }
}

inline void update(int rt,int l,int r,int L,int R,int k){
    if(L<=l&&r<=R){
        laz[rt]+=k;
        a[rt]+=k*len;
    }
    else{
        if(laz[rt])pushdown(rt,len);
        if(L<=mid)update(lson,L,R,k);
        if(R>mid)update(rson,L,R,k);
        a[rt]=(a[rt<<1]+a[rt<<1|1])%mod;
    }
}
//---------------------------------以上为线段树 
inline int qRange(int x,int y){
    int ans=0;
    while(top[x]!=top[y]){//当两个点不在同一条链上 
        if(dep[top[x]]<dep[top[y]])swap(x,y);//把x点改为所在链顶端的深度更深的那个点
        res=0;
        query(1,1,n,id[top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
        ans+=res;
        ans%=mod;//按题意取模 
        x=fa[top[x]];//把x跳到x所在链顶端的那个点的上面一个点
    }
    //直到两个点处于一条链上
    if(dep[x]>dep[y])swap(x,y);//把x点深度更深的那个点
    res=0;
    query(1,1,n,id[x],id[y]);//这时再加上此时两个点的区间和即可
    ans+=res;
    return ans%mod;
}

inline void updRange(int x,int y,int k){//同上 
    k%=mod;
    while(top[x]!=top[y]){
        if(dep[top[x]]<dep[top[y]])swap(x,y);
        update(1,1,n,id[top[x]],id[x],k);
        x=fa[top[x]];
    }
    if(dep[x]>dep[y])swap(x,y);
    update(1,1,n,id[x],id[y],k);
}

inline int qSon(int x){
    res=0;
    query(1,1,n,id[x],id[x]+siz[x]-1);//子树区间右端点为id[x]+siz[x]-1 
    return res;
}

inline void updSon(int x,int k){//同上 
    update(1,1,n,id[x],id[x]+siz[x]-1,k);
}

inline void dfs1(int x,int f,int deep){//x当前节点,f父亲,deep深度 
    dep[x]=deep;//标记每个点的深度 
    fa[x]=f;//标记每个点的父亲 
    siz[x]=1;//标记每个非叶子节点的子树大小 
    int maxson=-1;//记录重儿子的儿子数 
    for(Rint i=beg[x];i;i=nex[i]){
        int y=to[i];
        if(y==f)continue;//若为父亲则continue 
        dfs1(y,x,deep+1);//dfs其儿子 
        siz[x]+=siz[y];//把它的儿子数加到它身上 
        if(siz[y]>maxson)son[x]=y,maxson=siz[y];//标记每个非叶子节点的重儿子编号 
    }
}

inline void dfs2(int x,int topf){//x当前节点,topf当前链的最顶端的节点 
    id[x]=++cnt;//标记每个点的新编号 
    wt[cnt]=w[x];//把每个点的初始值赋到新编号上来 
    top[x]=topf;//这个点所在链的顶端 
    if(!son[x])return;//如果没有儿子则返回 
    dfs2(son[x],topf);//按先处理重儿子,再处理轻儿子的顺序递归处理 
    for(Rint i=beg[x];i;i=nex[i]){
        int y=to[i];
        if(y==fa[x]||y==son[x])continue;
        dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链 
    }
}

int main(){
    read(n);read(m);read(r);read(mod);
    for(Rint i=1;i<=n;i++)read(w[i]);
    for(Rint i=1;i<n;i++){
        int a,b;
        read(a);read(b);
        add(a,b);add(b,a);
    }
    dfs1(r,0,1);
    dfs2(r,r);
    build(1,1,n);
    while(m--){
        int k,x,y,z;
        read(k);
        if(k==1){
            read(x);read(y);read(z);
            updRange(x,y,z);
        }
        else if(k==2){
            read(x);read(y);
            printf("%d\n",qRange(x,y));
        }
        else if(k==3){
            read(x);read(y);
            updSon(x,y);
        }
        else{
            read(x);
            printf("%d\n",qSon(x));
        }
    }
}

代码很长,但思路简单,写的时候千万仔细,变量命名最好贴切,否则写挂的话debug会花费不少时间,(永无乡Splay写挂了查了两天愣是一直90分,最后还是去学了线段树合并水过去了-_-)

转自https://www.cnblogs.com/chinhhh/p/7965433.html

乘法逆元

基础概念

在数论中, 如果ab ≡ 1(mod p), 即 p | (ab-1)(|是整除的意思)我们就说 a 和 b 在模 p 意义下互为乘法逆元, 记做 a = inv(b).
逆元有什么用呢?我们常常遇到一些题目要求结果对一个大质数 p 取模,这是因为答案很大,出题人为了不麻烦大家写高精,就采取这样的方法。加减法和乘法对取模运算都是封闭的,所以你可以处处取模来避免溢出。但遇到除法时,就麻烦了。

(12 / 3) % 4 ≠ (12%4 / 3%4) % 4

为了解决模意义下的除法问题,我们引入了逆元。 inv(a) 其实可以看做模 p 意义下的 $$a^{-1}$$ ,那么在模 p 意义下, $$\frac{a}{b}$$ 就可以变形为 a*inv(b) % p 。
实际上在模 10 意义下 inv(3)=7 ,所以上面的式子可以这样计算:

(12 / 3) % 4 = (12inv(3)) % 4 = (127) % 4 = 4

显然结果是正确的

求解方法

1.扩展欧几里得求逆

如果你还不会扩欧,那可以看下这篇博客。 因为ax ≡ 1(mod p)等价于ax+by = 1, 那么就可以通过扩欧来求解x,复杂度O(logp)由此我们还可以得出逆元存在的冲要条件是gcd(a,p) = 1。

2.费马小定理求逆

注意,这种求逆元的方法仅使用在 p 为质数的时候

费马小定理:若 p 为质数,且整数 a 满足 gcd(a,p) = 1 ,则有 $$a^{p-1}$$ ≡ 1 (mod p), 又因为ab ≡ c (mod p) 等价于 ((a%p)b) ≡ c (mod p),(这个可以自己写下式子模拟下就清楚了),所以a在模p意义下的逆元即为$$a^{p-2}$$%p,用快速幂求解即可。
一道例题有理数取余, 这道题可以加深你对分数取余的 理解,请仔细体会其与除法取余的区别。

3.线性求逆

基于拓展欧几里得算法,我们虽然可以在 O(nlogp) 时间内,求出 [1,n] (1 <= n < p) 中所有整数在模质数 [公式] 下的乘法逆元,但在面对更大的数据范围时,我们就需要更快的方法。
注意,这种求逆元的方法仅使用在 p 为质数的时候。因为难免出现[1,n]内存在数x使gcd(x,p) ≠ 1, 那么它自然就不存在模p意义下的逆元,而后面的逆元都是根据前面求出的逆元得来的,这将导致后面一连串的逆元都是错的。
我们设p = ki + r,(r < i, 1 < i < p), 将其转换为同余方程得到:

ki + r ≡ 0 (mod p)

两边同乘以$$i^{-1}$$和$$r^{-1}$$,得:

k$$r^{-1}$$ + $$i^{-1}$$ = 0 (mod p)

移项得:

$$i^{-1}$$ = -k$$r^{-1}$$ (mod p)

将 k = $$\lfloor \frac{p}{i} \rfloor$$, r = p%i代入得:

$$i^{-1}$$ = -$$\lfloor \frac{p}{i} \rfloor$$*$$(p%i)^{-1}$$ (mod p)

注意-$$\lfloor \frac{p}{i} \rfloor$$为负数,在模p意义下,其等价于p-$$\lfloor \frac{p}{i} \rfloor$$, 故有递推式:

$$i^{-1}$$ = (p-$\lfloor \frac{p}{i} \rfloor$)*$(p%i)^{-1}$ (mod p)

复杂度O(n),模板题

4.离线逆元

其用于求解给定n个数在模p下的乘法逆元,这时递推式不再适用,时间空间都会爆炸。因为该算法并不常用,因此在此不再赘述,有兴趣的可以参考这道题,题解讲的十分清楚。

注:本篇内容借鉴于洛谷日报

关于C++未定义行为(undefined behavior)

代码出现bug是再正常不过的事情,然而找bug也是一件令人十分头疼的苦差事。曾一段时间日常debug三四小时的蒟蒻飘过QWQ如果是符合逻辑的错误那还好说,但如果出现了一些执行情况不确定的错误,而你又天真地认为它们“应该”会执行出那样的效果,那这毫无疑问会让你之后的debug旅途变得丰富多彩。所以学习UB是必要的,由于我又菜又懒,这里推荐一篇博客,讲的十分清晰,希望能帮到你。

二元一次不定方程(exged)

目录:

题目概要
拓展欧几里得算法(exgcd)
求二元一次方程/线性同余方程特解
求二元一次方程/线性同余方程通解
解决问题

题目概要

给定二元一次不定方程ax+by=c
若该方程无整数解,输出 -1
若该方程有整数解,且有正整数解,则输出其正整数解的数量,所有正整数解中 x 的最小值,所有正整数解中 y 的最小值,所有正整数解中 x 的最大值,以及所有正整数解中 y 的最大值
若方程有整数解,但没有正整数解,你需要输出所有整数解中 x 的最小正整数值, y 的最小正整数值

正整数解即为 x,y 均为正整数的解
整数解即为 x,y 均为整数的解
x 的最小正整数值即所有 x 为正整数的整数解中 x 的最小值,y 同理

数据范围:1<=a,b,c<=1e9 , 输入含有多组数据,数据组数不超过2*10^5

样例输入:
2
2 11 100
3 18 6

样例输出:
4 6 2 39 8
2 1

拓展欧几里得算法

扩展欧几里得算法:用于求解形如ax+by=gcd(a,b)的不定方程特解。
求解方法&证明: 当b=0时,可以看出gcd(a,b)=a,而x=1,y=0是方程的一组特解
当b!=0时,根据欧几里得算法递归求解exgcd(b,a%b,x,y),设a’=b,b’=a%b,可以求得a’x’+b’y’=gcd(a,b)的一组特解,即x’,y’。
而ax+by=gcd(a,b)=a’x’+b’y’,将a’=b,b’=a%b代入,得x=y’,y=x-a/b*y’。

Code

int exgcd(int a,int b,int &x,int &y)
    {
        if(!b)
        {
            x=1;
            y=0;
            return a;
        }
        int k=exgcd(b,a%b,x,y);
        int p=x;
        x=y;
        y=p-(a/b)*y;
        return k;
    }

求二元一次方程组的特解

二元一次方程: 对于形如ax+by=c的二元一次方程,裴蜀定理指出,当且仅当gcd(a,b)|c(|为整除)时,存在整数解。由裴蜀定理我们可以得到一个推论:a,b互质的充要条件是存在整数x,y使ax+by=1.
设g=gcd(a,b),a’=a/g,b’=b/g,c’=c/g,则ax+by=c⇔a’x+b’y=c’,
此时gcd(a’,b’)=1,可以利用exgcd求出a’x’+b’y’=1的一组特解,继而得出x=c’y’, y=c’x’,从而得到ax+by=c的一组特解

线性同余方程: 对于形如ax\equiv≡c(mod b)的线性同余方程,根据模运算的定义,在方程左侧添加一个by不会对结果造成影响,其实质就等价于ax+by=c的不定方程,利用上述方法求解便可。

求二元一次方程/线性同余方程通解

设g=gcd(a,b), a’=b/g, b’=a/g, x0,y0为方程的一组特解, 则通解为x=x0+a’t, y=y0-b’t(t为整数)

证明
Q:为什么它是通解?
A:因为将该解代入方程恒成立
Q:但还有其他形式的解能满足,例如x=x0+bt, y=y0-at,为什么选他呢?
A:因为他能无遗漏地表示所有正整数解,因为lcm(a,b)=ab/g, x,y的系数分别为a,b,所以x,y的在满足方程的前提下的最小变化量为b/g,a/g,证毕。
线性同余方程同理

解决问题

至此我们具备了解决问题所需要的所有知识
无整数解: 要求输出-1。根据扩展欧几里得算法的定义可得,当c%gcd(a,b)≠0时方程无整数解,输出-1即可。
判断有无正整数解: 根据二元一次方程的通解可得,x,y的增减性相反,所以若存在正整数解,则当x取得最小正整数解时,y取得最大正整数解,若此时y≤0,则说明该方程无正整数解。最小整数解可以通过取余快速得到,如x的最小整数解为(x0%a’+a’)%a’,这里需要特判下,当x=0时另x=a’,y同理。
解组个数: 易得正整数解的组数为(Xmax-Xmin)/a’+1或(Ymax-Ymin)/b’+1.

Code:

#include<cstdio>
#define ll long long    //注意开long long

ll read(){
    char ch;
    ll num=0;
    ch=getchar();
    while(ch<'0'||ch>'9')
        ch=getchar();
    while(ch>='0'&&ch<='9'){
        num=num*10+ch-'0';
        ch=getchar();
    }
    return num;
}

ll ex_gcd(ll a,ll b,ll &x,ll &y){
    if(b==0){
        x=1, y=0;
        return a;
    }
    ll k=ex_gcd(b,a%b,x,y);
    ll temp=x;
    x=y;
    y=temp-a/b*y;
    return k;
}

int main(){
    ll n,x,y,a,b,c,d;
    n=read();
    while(n--){
        a=read(), b=read(), c=read();
        d=ex_gcd(a,b,x,y);
        if(c%d!=0){
            printf("-1\n");
            continue;
        }
        x*=c/d, y*=c/d;
        ll temp=(x%(b/d)+b/d)%(b/d);    //temp为x的最小正整数解
        if(temp==0)                //特判
            temp=b/d;
        if((c-a*temp)/b<=0){
            int q=(y%(a/d)+a/d)%(a/d);    //q为y的最小正整数解
            if(!q)
                q=a/d;
            printf("%lld %lld\n",temp,q);
            continue;
        }
        ll temp2=(y%(a/d)+a/d)%(a/d);    //temp2为y的最小正整数解
        if(!temp2)
            temp2=a/d;
        int maxx=(c-temp2*b)/a;        //maxx为x的最大正整数解
        printf("%lld %lld %lld %lld %lld\n",(maxx-temp)/(b/d)+1,temp,temp2,maxx,(c-temp*a)/b);
    }
    return 0;
}

此题为洛谷P5656,建议去洛谷切下这道题来巩固,洛谷CF7C Line为弱化版,只用找出一组整数解即可

普通平衡树(splay)

前言

本篇讲解内容转自rtldl的博客,其中添加了些个人理解,希望能帮助你更好的了解Splay。个人认为Splay并没有那么难,跟树剖一样属于虽然码量不小但只要理解了思路就不难实现的一类数据结构,所以当你看到下面的长篇大论时请一定要坚持看下去,你会发现其实每一步都是很好理解的,最后祝你食用愉快!

基本思想

数据结构

对于Splay,我定义了一个class类(当成struct就行啦。。。个人习惯不同啦),定义名称为“Splay”。

之后在类中,我定义了Splay的主体,即数组e。

e的类型是node类型,包含节点值(v)、父级节点(father)、左孩子(ch[0])、右孩子(ch[1])、包含自己在内的下面共有多少元素(sum)(注意是元素啊!!!不是节点!!!)、该节点所表示的元素出现的次数(recy)。

之后,还在类中定义了n代表当前已经使用的数组编号。points代表整个树总共有多少元素(注意是元素啊!!!不是节点!!!)。

另外,整棵树中,有一个超级根e[0],其右孩子即为树的根。

宏定义了e[0].ch[1]为root,方便访问、理解。并在类的末尾取消定义root,确保外部再定义root变量时不会出现问题,维持其模块性质。

class Splay//存储规则:小左大右,重复节点记录 
{
    #define root e[0].ch[1]   //该树的根节点
    private:
        class node
        {
            public:
                int v,father;//节点值,父级节点 
                int ch[2];//左孩子=0,右孩子=1
                int sum;//自己+自己下级有多少节点。在根节点为1。
                int recy;//记录自己被重复了几次
        };        
node e[MAXL];//Splay树主体
        int n,points;//使用存储数,元素数
    ……
    #undef root
};

功能全解

更新当前节点sum值(update)

就是在进行了连接、插入、删除等操作以后使用的一个维护性质的函数。用来确定被update的节点的sum值。

void update(int x)
{
    e[x].sum=e[e[x].ch[0]].sum+e[e[x].ch[1]].sum+e[x].recy;
}

获取父子关系(identify)

用来确定当前节点到底是父亲的左孩子(0)还是右孩子(1)。

int identify(int x)
{
    return e[e[x].father].ch[0]==x?0:1;
}

建立父子关系(connect)

用来连接两个点,其中一个点为另一个点的孩子。

注意,这个操作并不能将其他的父子关系一并断开。因为他们与被操作的两个点没有直接的数据联系。例如下图:

12

图表明尽管B的父亲已经不是x,但是x的右孩子依旧是B,没有被更新。因此使用过程中应当有更巧妙的设计来避免这样导致的错误发生。

void connect(int x,int f,int son)//连接函数。用法:connect(son,father,左儿子(0)或右儿子(1))
{
    e[x].father=f;
    e[f].ch[son]=x;
}//作用:将x连接在f的下方。连接方向由son的值决定。

旋转节点(rotate)

着重注意的一个函数。这个函数同时实现了左旋和右旋。

所谓的旋转,其实就是指将被指定的节点向上移动一级,并将原有的父级节点作为自己的儿子。如下图:

12

我们可以通过下图原理论证来确定只需要三次connect即可完成旋转。

12

上图代表了右旋。

在图中,A、B、C代表三个子树(可以是空的),x和y代表被旋转的节点。R为y的上级节点,与旋转没有直接关系,但是它的右孩子要进行相应的改变。

在进行完connect函数后,再进行update函数即可完成旋转。

但是旋转总共有两种类型的操作(即左旋和右旋)。在这里,我们需要配合位运算直接达到自动判断和旋转方向决断的目的。

我们知道,对于任意一个自然数,与1进行逻辑异或运算,会得到这样的结果:

0^1=1 1^1=0 2^1=3 3^1=2 4^1=5 5^1=4 ……

也就是说,0对应1,2对应3,4对应5,向后依次推。

既然这样,那么我们的左右儿子节点所代表的编号分别是0和1。也就是说对其中一个取逻辑异或,会得到另一个儿子的标号(即对0取逻辑异或得1,对1取逻辑异或得0)。

通过左旋右旋的性质可以知道,实际改变了父子关系的节点是上图的:x、y、B节点。因为实际上,A、C节点的父子关系并没有发生任何改变。

并且我们能够注意到,x与y节点的连接方向一定是与x和B的连接方向不同的。

那么,我们只需要先通过“identify”函数确定x与y的父子关系,确定到底要向那一边旋转(如果x是y的左孩子,那么就向右旋转。如果x是y的右孩子,那么就向左旋转),然后通过逻辑异或来确定子树“B”究竟应当被连接在y的哪一侧。

void rotate(int x)
{
    int y=e[x].father;
    int mroot=e[y].father;
    int mrootson=identify(y);
    int yson=identify(x);
    int B=e[x].ch[yson^1];
    connect(B,y,yson);connect(y,x,(yson^1));connect(x,mroot,mrootson);
    update(y);update(x);
}

伸展操作(splay)

其实就是考虑上旋节点的方式。

在这里,一开始我使用了一种较为偷懒的旋转方式,即能向上旋转就向上旋转。并不考虑上面的状况到底怎样。

其实,标准的写法中,需要考虑两种情况。如下图:

12

为了防止造成误导,我将不再介绍直接上旋的操作。但事实上,无论是直接上旋还是先判断再上旋,都会有可能进化或者退化原本的树形结构。

我也曾举出过两种操作模式各自进化或者退化树的例子。但是根据交题情况,在洛谷的模板题中,直接上旋的速度更快。然而在湖南的一道省选题中,使用直接上旋的模式却直接导致超时(大概慢了10倍)。所以说在面对大数据的不确定因素下,还是应当选择考虑更多种情况,而不能图方便。

在这里,我的函数实现的操作是:将at节点旋转到to节点所在的位置。

void splay(int at,int to)
{
    to=e[to].father;
    while(e[at].father!=to)
    {
        int up=e[at].father;
        if(e[up].father==to) rotate(at);
        else if(identify(up)==identify(at))
        {//对应图中case1
            rotate(up);
            rotate(at);
        }
        else
        {//对应图中case2
            rotate(at);
            rotate(at);
        }
    }
}

添加节点(crepoint)和摧毁节点(destroy)

这两个操作是在插入新元素和删除元素过程中使用的函数。

crepoint的作用是获得一个新的树存储位置,然后为这个存储空间写入基本的信息,并返回使用的存储位置编号。

destroy的作用则是使得一个节点完全失效,完全抹除节点信息,防止其他意外的发生。并且添加了一个小小的优化:如果被抹除的节点恰好是存储数组的当前最后一个元素,那么就对存储空间的使用数减1。

实际上,也可以通过一个队列来确定那些节点在中间被挖空了。但这样的操作不仅要牺牲一个O(log N)的时间复杂度,而且事实上并没有太大的用处,因为你开的数组大小一定能够满足极端情况(比如说所有操作都是插入)。

int crepoint(int v,int father)
{
    n++;
    e[n].v=v;
    e[n].father=father;
    e[n].sum=e[n].recy=1;
    return n;
}
void destroy(int x) 
{
    e[x].v=e[x].ch[0]=e[x].ch[1]=e[x].sum=e[x].father=e[x].recy=0;
    if(x==n) n--;
}

查找元素(find)

要实现的功能是找特定值是否在树中以及对应的节点编号。

很简单的实现方式。从根开始向下移动,如果要找的元素比当前节点小,那么就转到自己的左孩子。否则,就转向自己的右孩子,直到节点值等于要找的值。

如果在找到目标值之前,需要走的路已经无法再走(比如说现在到了5,要找的是3,应该往左走,但是5已经没有左孩子了),那么则查找失败,返回失败值(0)。如果查找成功,则返回节点对应的编号。

查找结束后,将被查找的节点旋转到根,以保证树的结构随机性。

int find(int v) 
{
    int now=root;
    while(true)
    {
        if(e[now].v==v)
        {
            splay(now,root);
            return now;
        }
        int next=v<e[now].v?0:1;
        if(!e[now].ch[next]) return 0;
        now=e[now].ch[next];
    }
}

建树(build)

建树的功能我并没有看懂大佬们的操作到底是什么意思。。。(我觉得应该是将Splay用作线段树的时候使用的功能)所以我写了一个没有上旋操作的insert函数。

首先,从根开始,向下寻找。如果要插入的元素已经在树中,那么将这个节点的recy加1即可。如果没有出现过,那么找一个合适的空的位置。找到位置后,调用crepoint函数,在数组中申请一个新的下标存储元素。

同时注意,在向下寻找的过程中,对被经过的点的sum值加1,因为如果经过这个点,代表要加的点肯定在自己下面,所以自己下面的元素个数加1。

int build(int v)//内部调用的插入函数,没有splay 
{
    points++;
    if(n==0)//特判无点状态 
    {
        root=1;
        crepoint(v,0);
    }
    else
    {
        int now=root;
        while(true)//向下找到一个空节点 
        {
            e[now].sum++;//自己的下级肯定增加了一个节点 
            if(v==e[now].v)
            {
                e[now].recy++;
                return now;
            }
            int next=v<e[now].v?0:1;
            if(!e[now].ch[next])
            {
                crepoint(v,now);
                e[now].ch[next]=n;
                return n;
            }
            now=e[now].ch[next];
        }
    }
    return 0;
}

插入节点(push)

就是在进行完build操作以后,执行一次上旋操作,确保树的结构随机性。

void push(int v)
{
    int add=build(v);
    splay(add,root);
}

删除节点(pop)

将输入值对应的节点在树中移除。

进行这样的操作时,我一开始考虑的是通过逐层的rotate操作将要被删除的节点转到最下方,然后再删除,最后逐层向上改变路径上的sum值。但是考虑到这样的操作可能会一方面导致树的大幅度退化,另一方面相当于要进行两次O(log N)的时间复杂度操作,常数略大,可能会成为一颗定时炸弹。所以为了稳定,还是用了常规的方法:

首先将要删除的节点旋转到根节点的位置。

然后,判断情况:如果要被删除的节点(注意现在它在根的位置)没有左孩子,那么直接摧毁这个节点,并将它的右孩子变成根。

如果自己有左孩子,那么就先把左子树中值最大的元素旋转到根的左孩子位置,然后将根节点的右孩子变成根节点的左孩子的右孩子,然后摧毁节点,并将左孩子变成根。

原理还请读者自己考虑吧,根据二叉排序树的性质。。。

void pop(int v)//删除节点 
{
    int deal=find(v);
    if(!deal) return;
    points--;
    if(e[deal].recy>1)
    {
        e[deal].recy--;
        e[deal].sum--;
        return;
    }
    if(!e[deal].ch[0])
    {
        root=e[deal].ch[1];
        e[root].father=0;
    }
    else
    {
        int lef=e[deal].ch[0];
        while(e[lef].ch[1]) lef=e[lef].ch[1];
        splay(lef,e[deal].ch[0]);
        int rig=e[deal].ch[1];
        connect(rig,lef,1);connect(lef,0,1);
        update(lef);
    }
    destroy(deal);
}

获取元素的排名(rank)&获取该排名对应的元素值(atrank)

两个函数是互逆的函数。

rank的实现根find差不多,只是在向下走的时候,对于当前已经记录的rank值进行更新(每次调用rank时都初始化为0)。规则是:向左走时,rank值不发生任何改变。向右走之前,要先给rank加上当前节点的左孩子的sum值和recy值。找到对应元素时,再对rank+1。如下图:

12

atrank函数根rank实现完全相反。在向下走的过程中,如果要找的排名大于当前点左子树的sum值,并且小于等于当前点的左子树的sum加上本节点的recy的值,那么当前的点就是要找的点。如果小于上述范围,就往左走,反之向右。注意向右走的过程中,将要查询的排名值减少上述范围的最大值。

两个操作结束后,都要将被操作的节点旋转到根。

int rank(int v)//获取值为v的元素在这棵树里是第几小 
{
    int ans=0,now=root;
    while(true)
    {
        if(e[now].v==v) return ans+e[e[now].ch[0]].sum+1;
        if(now==0) return 0;
        if(v<e[now].v) now=e[now].ch[0];
        else
        {
            ans=ans+e[e[now].ch[0]].sum+e[now].recy;
            now=e[now].ch[1];
        }
    }
    if(now) splay(now,root);
    return 0;
}
int atrank(int x)//获取第x小的元素的值 
{
    if(x>points) return -INF;
    int now=root;
    while(true)
    {
        int minused=e[now].sum-e[e[now].ch[1]].sum;
        if(x>e[e[now].ch[0]].sum&&x<=minused) break;
        if(x<minused) now=e[now].ch[0];
        else
        {
            x=x-minused;
            now=e[now].ch[1];
        }
    }
    splay(now,root);
    return e[now].v;
}

查找前驱(lower)和后继(upper)

两种操作是类似的操作。

前驱是指在树中,小于这个值并且最接近这个值的元素值。

后继则是大于这个值并且最接近这个值的元素值。

对于这两种函数的实现方式,就是先初始化一个最值,然后在向下走的过程中,如果发现了符合要求且更优的值,就用更优值替换当前的值。最后不能走的时候输出这个值即可。

int upper(int v) 
{
    int now=root;
    int result=INF;
    while(now)
    {
        if(e[now].v>v&&e[now].v<result) result=e[now].v;
        if(v<e[now].v) now=e[now].ch[0];
        else now=e[now].ch[1];
    }
    return result;
}
int lower(int v) 
{
    int now=root;
    int result=-INF;
    while(now)
    {
        if(e[now].v<v&&e[now].v>result) result=e[now].v;
        if(v>e[now].v) now=e[now].ch[1];
        else now=e[now].ch[0];
    }
    return result;

完结撒花~ 恭喜你坚持了下来,至此你应该对Splay有了大致的了解,其中的思路大致如此,具体的实现会因人而异,下面我将贴上另一种Splay实现,思路都是一样的,细节实现稍有不同
,你可以与上面的代码作对比,并加深理解。

#include<cstdio>
#define maxn 200500

int n,opt,cnt,root;
int val[maxn],son[maxn][2],fa[maxn],size[maxn],sum[maxn];

inline void destroy(int u){
    val[u] = son[u][0] = son[u][1] = fa[u] = sum[u] = size[u] = 0;
}

inline int identify(int u){
    return son[fa[u]][1] == u;
}

inline void update(int u){
    if(u)
        sum[u] = sum[son[u][0]]+sum[son[u][1]]+size[u];
}

void rotate(int u){
    int f = fa[u],gf = fa[f],sta = identify(u),sta_f = identify(f);
    son[f][sta] = son[u][sta^1];
    fa[son[f][sta]] = f;
    son[u][sta^1] = f,fa[f] = u,fa[u] = gf;
    son[gf][sta_f] = u;
    update(f),update(u);
}

void splay(int u){
    for(int f;f = fa[u];rotate(u))
        if(fa[f])
            rotate(identify(u)==identify(f) ? f:u);
    root = u;
}

void insert(int u){
    if(!root){
        val[++cnt] = u;
        size[cnt] = sum[cnt] = 1;
        root = cnt;
        return ;
    }
    int now = root,f = 0;
    while(true){
        if(u == val[now]){
            ++size[now];
            update(now),update(f);
            splay(now);
            return ;
        }
        f = now,now = son[now][val[now]<u];
        if(!now){
            val[++cnt] = u;
            size[cnt] = sum[cnt] = 1;
            fa[cnt] = f,son[f][val[f]<u] = cnt;
            ++sum[f];
            splay(cnt);
               return ;
        }
    }
}

int find_num(int u){
    int now = root;
    while(true){
        if(son[now][0]&&u <= sum[son[now][0]])
            now = son[now][0];
        else{
            int temp = sum[son[now][0]]+size[now];
            if(u <= temp)
                return val[now];
            now = son[now][1],u -= temp;
        }
    }
}

int find_rank(int u){
    int now = root,rank = 0;
    while(true){
        if(u < val[now])
            now = son[now][0];
        else{
            rank += sum[son[now][0]];
            if(u == val[now]){
                splay(now);
                return rank+1;
            }
            rank += size[now],now = son[now][1];
        }
    }
}

int find_pre(){     //注意这里查找前驱后继的函数和上面不一样,实现思路见下面主函数里的注释    
    int now = son[root][0];
    while(son[now][1])
        now = son[now][1];
    return now;
}

int find_suffix(){
    int now = son[root][1];
    while(son[now][0])
        now = son[now][0];
        return now;
}

void delete_val(int u){        //按照二叉搜索树(BST)的思路删点
    find_rank(u);
    if(size[root] > 1){
        --size[root],--sum[root];
        return ;
    }
    if(!son[root][0] && !son[root][1]){
        destroy(root),root = 0;
        return ;
    }
    int old_root = root;
    if(!son[root][0]){
        root = son[root][1];
        fa[root] = 0;
        destroy(old_root);
        return ;
    }
    if(!son[root][1]){
        root = son[root][0];    
        fa[root] = 0;
        destroy(old_root);
        return ;
    }
    int left_max = find_pre();
    splay(left_max);
    son[root][1] = son[old_root][1];
    fa[son[old_root][1]] = root;
    destroy(old_root);
    update(root);
}    

int main(){
    int x;
    scanf("%d",&n);
    while(n--){
        scanf("%d%d",&opt,&x);
        if(opt == 1)
            insert(x);
        else if(opt == 2)
            delete_val(x);
        else if(opt == 3)
            printf("%d\n",find_rank(x));
        else if(opt == 4)
            printf("%d\n",find_num(x));
        else if(opt == 5){  //与上面不同,5和6操作是先插入要删除的值,目的是将储存该值的节点上旋到跟节点,这样左子树的最大值就是答案。
            insert(x);
            printf("%d\n",val[find_pre()]);
            delete_val(x);
        }
        else{
            insert(x);
            printf("%d\n",val[find_suffix()]);
            delete_val(x);
        }
    }
    return 0;
}

最后再奉上一道练习题,这道题也可以当做线段树合并的入门题。

洛谷p3224永无乡

可持久化线段树(主席树)

前置芝士 : 线段树,离散化(部分题目需要)

如果还不了解线段树可以戳这里

主席树是一种以线段树为基础的可以快速查询区间第k小(大)的数据结构,其中可持久化的意思就是对于每次修改,我们都将结果保存下来,然后你可以随时查询修改之前任意一次操作时所对应的线段树,类似于git,可以查看历史版本。为了实现这一功能,容易想到的是暴力建树,即对于每次操作都重新建立一颗线段树,但这样做时间空间肯定会炸的稀里哗啦。于是我们换个角度想,考虑到对于每次修改,实际变动的节点只是少数,如下图:
233

假如现在正在进行第x次操作,我们对原序列第四个点进行修改,其中橙色节点是第x-1次操作对应的线段树的节点,而其右边是第x次操作新建立的线段树节点。这里若修改第4个点,则只有橙色的节点维护的信息发生了变动,而其他节点都保持不变,考虑到这一特点,在每次修改后,我们只需要创建与受到影响的节点(橙色节点)相对应的节点(橙色节点右边的节点),然后让他们共用那些没有被影响的节点,这样一来,便可以很快的建好第x次操作所对应的线段树,并且极大节约了空间。(与普通线段树不同的是,节点的左右儿子编号并不能被计算出来,需要另行记录)

下面我们通过一道例题具体实现下,此题需进行离散化 可参考这篇博客的方法二

例题 洛谷p3834

题意
给定 n 个整数构成的序列,将对于指定的闭区间查询其区间内的第 k 小值。

输入格式
第一行包含两个正整数 n,mn,m,分别表示序列的长度和查询的个数。

第二行包含 nn 个整数,表示这个序列各项的数字。

接下来 mm 行每行包含三个整数 l, r, kl,r,k , 表示查询区间 [l, r][l,r] 内的第 kk 小值。

输出格式
输出包含 mm 行,每行一个整数,依次表示每一次查询的结果

数据范围
1 <= n,m <= 2*10^5 数列中的所有数绝对值都不超过10^9

这是一道板子题,我将结合代码用注释解释

Code

#include<cstdio>
#include<algorithm>
#define maxn 200005
using std::sort;
using std::unique;
using std::lower_bound;

int n,m,cnt,p,k,l,r;
int cl[maxn<<5],cr[maxn<<5],sum[maxn<<5];
//cl[i],cr[i],sun[i]分别为i号节点的左孩子序号,右孩子序号,i号节点管理的区间和
int rt[maxn],a[maxn],b[maxn];
//rt[i]是第i颗线段树的根节点号码,a,b分别是原数组和其副本
int change(int u,int l,int r){    //u是与现在访问的节点相对应的上一颗数的节点号码,l,r是该号码所管理的区间
    int t = ++cnt;    //t是新建立的节点号码
    cl[t] = cl[u],cr[t] = cr[u],sum[t] = sum[u]+1; //先让t号节点继承上一颗线段树与之对应的节点的信息
    if(l == r)        //该节点没有孩子了,结束更新
        return t;
    int mid = (l+r)>>1;
    if(p <= mid)    //变动的节点在左子树,递归进入左子树创立新节点
        cl[t] = change(cl[u],l,mid);
    else
        cr[t] = change(cr[u],mid+1,r);
    return t;    //返回这次创立的节点序号
}

int query(int fu,int u,int l,int r,int k){    //返回第k小的数离散化后的rank
    if(l == r)
        return l;
    int mid = (l+r)>>1,num = sum[cl[u]]-sum[cl[fu]];    //主席树是可减的,这里实际模拟下就很清楚了
    if(num >= k)
        return query(cl[fu],cl[u],l,mid,k);
    else
        return query(cr[fu],cr[u],mid+1,r,k-num);
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++){
        scanf("%d",&a[i]);
        b[i] = a[i];    //因为要进行离散化,而之后还要用该数组的值,所以创建副本
    }
    //   下面是离散化操作
    sort(b+1,b+1+n);
    int len = unique(b+1,b+1+n)-b-1;
    for(int i = 1;i <= n;i++){        //对原数组下标1-i的数建一颗线段树(也就是一个版本),一共会建立n颗线段树
        p = lower_bound(b+1,b+1+len,a[i])-b;
        rt[i] = change(rt[i-1],1,len);    //change函数会创建新节点,同时返回新创建的节点标号
    }
    while(m--){
        scanf("%d%d%d",&l,&r,&k);
        int t = query(rt[l-1],rt[r],1,len,k);
        printf("%d\n",b[t]);
    }
    return 0;
}

下面放两道主席树的其他类型题目:
洛谷 p3919
洛谷 p3402

以网络流24题为例

匹配

二分图匹配
1.匈牙利算法
一般图最大匹配
1.带花树算法

前置知识:线段树 离散化

123

456

  1. 456
  2. 789

hello people

  • 欢迎光临*
    • 123
    • 456