欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

谈谈主席树那些事

程序员文章站 2022-05-05 17:39:02
...

转载自:http://blog.csdn.net/xgc_woker/article/details/78018297

【主席树】第K小的数Ⅰ(caioj1441)

主席树为什么叫主席树呢?
因为发明它的fotile被我们叫做fotile主席,所以就叫主席树。
首先就先来讲一下它的两个主要函数插入和合并。
这里的插入使用的是动态开点。
因为很多时候线段树维护的区间很大,而能定义的空间是有限的。
所以我们就只给那些有用的点一个编号就可以了(只有在修改时被访问过的点才是有用的)。虽然这样的线段树是残缺不全的,但也还是线段树。
然后就是合并
先讲一下线段树的定义。
当确定了元素个数n,或者key的范围[1,U],建出的线段树形态是唯一的。
对两棵key的上界相同的线段树进行参数相同的单点更新/区间询问时,所访问到的节点也是一致的。—— 来自杭州二中的黄嘉泰大神
所以当两棵线段树的下标和维护的范围大小都一样就可以说这两棵线段树可以合并。
如图:
这里写图片描述
有了这两个函数接下来该怎么做呢?
对于每个位置都建一颗线段树,其实是一条链。
然后再按照顺序把线段树都合并起来。第i棵线段树维护的就是1~i区间的信息。
通过合并可以知道线段树满足可加性,那么得到的信息肯定可以相减。
对于每一次询问通过第r棵线段树和第(l-1)棵线段树作差,即可得到该区间的信息。
经过对左孩子作差,设左边的数字个数为c。
如果k<=c,那么左孩子查找第k大的。
如果k>c,那么在右孩子查找第(k-c)大的。
【参考程序】

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
struct node
{
    int lc,rc,c;//c维护的是这个区间维护了多少个节点
}t[2100000];int cnt;
int n,m;
int wr[110000],s[110000],rt[110000];
int Pos(int num)
{
    int mid,ans,l,r;
    l=1;
    r=n;
    while(l<=r)
    {
        mid=(l+r)/2;
        if(s[mid]<=num)
        {
            ans=mid;
            l=mid+1;
        }
        else r=mid-1;
    }
    return ans;
}

void Link(int &u,int l,int r,int p)
{
    if(!u)u=++cnt;
    t[u].c++;
    if(l==r)return ;
    int mid =(l+r)/2;
    if(p<=mid)Link(t[u].lc,l,mid,p);
    else Link(t[u].rc,mid+1,r,p);
}

void Merge(int &u1,int u2)
{
    if(!u1){u1=u2;return ;}
    if(!u2)return ;
    t[u1].c+=t[u2].c;
    Merge(t[u1].lc,t[u2].lc);
    Merge(t[u1].rc,t[u2].rc);
}

int Calc(int u1,int u2,int l,int r,int k)
{
    if(l==r)return s[l];
    int c=t[t[u1].lc].c-t[t[u2].lc].c;//用u1这棵线段树减u2这棵线段树就可以得到u1到u2这段区间的信息
    int mid=(l+r)/2;
    if(k<=c)return Calc(t[u1].lc,t[u2].lc,l,mid,k);
    else return Calc(t[u1].rc,t[u2].rc,mid+1,r,k-c);
}
char ss[10];
int main()
{
    cnt=0;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&wr[i]);
        s[i]=wr[i];
    }
    sort(s+1,s+n+1);
    for(int i=1;i<=n;i++)
    {
        Link(rt[i],1,n,Pos(wr[i]));//每次插入以rt[i]为根一条链这条链维护i这个点的信息
        Merge(rt[i],rt[i-1]);//将其与前i-1条链合并,这样rt[i]为根的这条链维护的就是1~i的信息
    }
    while(m--)
    {
        int x,y,k;
        scanf("%d%d%d",&x,&y,&k);
        printf("%d\n",Calc(rt[y],rt[x-1],1,n,k));
    }
    return 0;
}

2 [视频] 【主席树】第K小的数Ⅱ(caioj1442)

因为我们求l到r这个区间的信息使用类似前缀和的方法求的,那么带修改的前缀和的问题可以用线段树或者树状数组来维护。
于是这里我用的就是树状数组套主席树,在树状数组的每个节点下都建一棵权值线段树,维护的就是第i个节点的修改情况。
修改每次先减去之前值的影响,再加上这个新的值影响。
询问就用树状数组各自求1~l和1~r的和,再相减,再加上原来的信息。
至于树状数组求和该怎么在线段树跳,我们就先预处理出来就OK。
【参考程序】

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std; 

struct node
{
    int lc,rc,c; 
}t[5100000];
int rt[210000],ust[210000],a[110000];
int cnt,n,m;
char opt[20];

int lowbit(int x){return x&-x;}

void Link(int &u,int l,int r,int p,int c)
{
    if(!u)u=++cnt;
    t[u].c+=c;
    if(l==r)return ;
    int mid=(l+r)/2;
    if(p<=mid)Link(t[u].lc,l,mid,p,c);
    else Link(t[u].rc,mid+1,r,p,c);
}

void Merge(int &u1,int u2)
{
    if(!u1){u1=u2;return ;}
    if(!u2)return ;
    t[u1].c+=t[u2].c;
    Merge(t[u1].lc,t[u2].lc);
    Merge(t[u1].rc,t[u2].rc);
}

void Turn(int u,int c)
{
    while(u>=n+1)
    {
        if(c==-1)ust[u]=rt[u];//从根跳
        else if(c==0)ust[u]=t[ust[u]].lc;//往左跳
        else if(c==1)ust[u]=t[ust[u]].rc;//往右跳
        u-=lowbit(u);
    }
}

void Modify(int u,int p,int c)
{
    while(u<=2*n)
    {
        Link(rt[u],0,1000000000,p,c);
        u+=lowbit(u);
    }
}

int Getsum(int u)
{
    int ret=0;
    while(u>=n+1)
    {
        ret+=t[t[ust[u]].lc].c;
        u-=lowbit(u);
    }
    return ret;
}

void Calc(int u1,int u2,int p1,int p2,int l,int r,int k)
{
    if(l==r)
    {
        printf("%d\n",l);
        return ;
    }
    int c=t[t[u2].lc].c-t[t[u1].lc].c+Getsum(p2+n)-Getsum(p1+n);
    //用树状数组求出p1和p2区间的信息和再加上原来的信息就可以得到新的信息
    int mid=(l+r)/2;
    if(k<=c)
    {
        Turn(p1+n,0);
        Turn(p2+n,0);
        Calc(t[u1].lc,t[u2].lc,p1,p2,l,mid,k);
    }
    else
    {
        Turn(p1+n,1);
        Turn(p2+n,1);
        Calc(t[u1].rc,t[u2].rc,p1,p2,mid+1,r,k-c);
    }
}

int main()
{
    cnt=0;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        Link(rt[i],0,1000000000,a[i],1);
        Merge(rt[i],rt[i-1]);
    }
    //rt[1]~rt[n]就是普通的主席树,维护原来的信息
    //rt[n+1]~rt[2*n]维护的就是第i-n棵线段树修改的情况
    while(m--)
    {
        scanf("%s", opt+1);
        if(opt[1]=='C')
        {
            int p, c;
            scanf("%d%d",&p,&c);
            Modify(p+n,a[p],-1);
            //先减去之前的值的影响
            a[p]=c;
            Modify(p+n,a[p],1);
            //先加上要修改的值的影响
        }
        else
        {
            int l, r, k;
            scanf("%d%d%d",&l,&r,&k);
            Turn(l-1+n,-1);
            Turn(r+n,-1);
            //先预处理出跳的方向。
            Ask(rt[l-1],rt[r],l-1,r,0,1000000000,k);
        }
    }
    return 0;
}

3 [视频] 【主席树】第K小的数Ⅲ(caioj1443)

对于i这条链它的定义就修改为i到根的信息。
询问(l,r)时用l这棵线段树加r这棵线段树减去一个最近公共祖先的线段树再减去最近公共祖先父亲节点的线段树,就可以得出l到r路径上的信息。
【参考程序】

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

struct tnode
{
    int c,lc,rc;
} t[2600000]; int cnt;
int rt[110000],val[110000],dep[110000],f[110000][25],s[110000];
int n,m,root;
struct node
{
    int x,y,next;
}e[210000];int len,last[110000];

void Ins(int x, int y)
{
    e[++len].x=x;e[len].y=y;
    e[len].next=last[x];last[x]=len;
}

int Pos(int num)
{
    int l,r,mid,ret;
    l=1;
    r=n;
    while(l<=r)
    {
        mid=(l+r)/2;
        if(s[mid]<=num)
        {
            ret=mid;
            l=mid+1;
        }
        else r=mid-1;
    }
    return ret;
}

void Link(int &u,int l,int r,int p)
{
    if(!u)u=++cnt;
    t[u].c++;
    if(l==r)return ;
    int mid=(l+r)/2;
    if(p<=mid)Link(t[u].lc,l,mid,p);
    else Link(t[u].rc,mid+1,r,p);
}

void Merge(int &u1,int u2)
{
    if(!u1){u1=u2;return ;}
    if(!u2)return ;
    t[u1].c+=t[u2].c;
    Merge(t[u1].lc,t[u2].lc);
    Merge(t[u1].rc,t[u2].rc);
}

int Calc(int u1,int u2,int u3,int u4,int l,int r,int k)//u3是u1和u2的最近公共祖先,u4是最近公共祖先的父亲
{
    if(l==r)return s[l];
    int c=t[t[u1].lc].c+t[t[u2].lc].c-t[t[u3].lc].c-t[t[u4].lc].c;
    //u1这棵线段树加u2这棵线段树减去一个最近公共祖先的线段树再减去最近公共祖先父亲节点的线段树
    int mid=(l+r)/2;
    if(k<=c)return Calc(t[u1].lc,t[u2].lc,t[u3].lc,t[u4].lc,l,mid,k);
    else return Calc(t[u1].rc,t[u2].rc,t[u3].rc,t[u4].rc,mid+1,r,k-c);
}

void Build(int x,int fa)
{
    dep[x]=dep[fa]+1;
    f[x][0]=fa;
    for(int i=1;(1<<i)<=dep[x];i++)
        f[x][i]=f[f[x][i-1]][i-1];
    Merge(rt[x],rt[fa]);//将其与其父亲节点合并,其他都是倍增lca的过程
    for(int k=last[x];k;k=e[k].next)
    {
        int y=e[k].y;
        if(y!=fa)
            Build(y,x);
    }
}

int Lca(int x,int y)//求lca的过程
{
    if(dep[x]<dep[y])swap(x,y);
    for(int i=20;i>=0;i--)
        if(dep[x]-dep[y]>=(1<<i))
            x=f[x][i];
    if(x==y)return x;
    for(int i=20;i>=0;i--)
        if(dep[x]>=(1<<i)&&f[x][i]!=f[y][i])
            x=f[x][i],y=f[y][i];
    return f[x][0];
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&val[i]);
        s[i]=val[i];
    }
    sort(s+1,s+n+1);
    len=0;
    memset(last,0,sizeof(last));
    for(int i=1;i<n;i++)
    {
        int x, y;
        scanf("%d%d",&x,&y);
        Ins(x,y);
        Ins(y,x);
    }
    cnt=0;
    for(int i=1;i<=n;i++)Link(rt[i],1,n,Pos(val[i]));
    //一开始不直接合并,因为i-1并不一定是i的父亲节点
    root=1;
    Build(root,0);//构建f数组,建树
    while(m--)
    {
        int x,y,lca,k;
        scanf("%d%d%d",&x,&y,&k);
        lca=Lca(x,y);//求最近公共祖先
        printf("%d\n",Calc(rt[x],rt[y],rt[lca],rt[f[lca][0]],1,n,k));
    }
    return 0;
}

4 【主席树】逆序对数(caioj1444)

这道题要用到分块+树状数组+主席树。
分块就是指将一段区间分成多块啦,这个就先说一下。
我们一开始可能有个思路,用一个ans[i][j]数组表示从i起到第j个数的逆序列对数,这个中间的过程我们可以用树状数组来求出,这种方法虽然思路正确但时间复杂度很高O(n^2*log n),对于50000的数据肯定就爆了。
那我们再考虑我们的老朋友主席树。我能想到的方法就是枚举l到r的每个节点,主席树在线求出他们前面有多少对逆序对数,这种方法也对,但是时间复杂度是O(m*n*log n),当然也会爆。
那么我们这里可以分块,分成sqrt(n)块,我们的ans[i][j]就指从第i块的开头起,一直到第j个数中有多少对逆序对数,这样我们就可以预处理出右半部分的答案,剩下的部分就可以用主席树在线求。
那么我们预处理的时间复杂度就是O(n*sqrt n*log n),在线处理的时间复杂度为O(m*sqrt n*n),都不会爆。
【参考程序】

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>

using namespace std;

struct node
{
    int lc,rc,c;
}t[1600000];int cnt=0;
int rt[51000],a[51000],h[51000];
int n,m,belong[51000],l[250],r[250],anss[250][51000];
int s[51000],q[51000];

int Pos(int num)
{
    int mid,ans,l,r;
    l=1;
    r=n;
    while(l<=r)
    {
        mid=(l+r)/2;
        if(h[mid]<=num)
        {
            ans=mid;
            l=mid+1;
        }
        else r=mid-1;
    }
    return ans;
}

int lowbit(int x){return x&-x;}

void add(int x,int y)
{
    while(x<=n)
    {
        s[x]+=y;
        x+=lowbit(x);
    }
}

int getsum(int x)
{
    int sum=0;
    while(x)
    {
        sum+=s[x];
        x-=lowbit(x);
    }
    return sum;
}

void Link(int &u,int l,int r,int x)
{
    if(!u)u=++cnt;
    t[u].c++;
    if(l==r)return;
    int mid=(l+r)/2;
    if(x<=mid)Link(t[u].lc,l,mid,x);
    else Link(t[u].rc,mid+1,r,x);
}

void Merge(int &u1, int u2)
{
    if(!u1){u1=u2;return;}
    if(!u2)return;
    t[u1].c+=t[u2].c;
    Merge(t[u1].lc,t[u2].lc);
    Merge(t[u1].rc,t[u2].rc);
}

int Calc(int u1,int u2,int l,int r,int ll,int rr)
{
    if(!u1&&!u2)return 0;
    if(l==ll&&rr==r)return t[u1].c-t[u2].c;
    int mid=(ll+rr)/2;
    if(r<=mid)return Calc(t[u1].lc,t[u2].lc,l,r,ll,mid);
    else if(l>mid)return Calc(t[u1].rc,t[u2].rc,l,r,mid+1,rr);
    else return Calc(t[u1].lc,t[u2].lc,l,mid,ll,mid)+Calc(t[u1].rc,t[u2].rc,mid+1,r,mid+1,rr);
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        h[i]=a[i];
    }
    sort(h+1,h+1+n);
    for(int i=1;i<=n;i++)a[i]=Pos(a[i]);
    //求出a[i]离散化后的值
    int sq=int(sqrt(double(n+1)));
    for(int i=1;i<=n;i++)//分块的过程 
    {
        belong[i]=(i-1)/sq+1;
        r[belong[i]]=i;
        if(!l[belong[i]])l[belong[i]]=i;
    }
    int t=0;
    for(int i=1;i<=belong[n];i++)//用树状数组来预处理出anss数组。
    {
        memset(s,0,sizeof(s));
        anss[i][l[i]]=0;
        add(a[l[i]],1);
        for(int j=l[i]+1;j<=n;j++)
        {
            anss[i][j]=anss[i][j-1]+(j-l[i]-getsum(a[j]));
            add(a[j],1);
        }
    }
    for(int i=1;i<=n;i++)
    {
        Link(rt[i],1,n,a[i]);
        Merge(rt[i],rt[i-1]);
    }
    memset(s,0,sizeof(s));
    int ans=0;
    while(m--)
    {
        int L,R;
        scanf("%d%d",&L,&R);
        if(L>R)swap(L,R);
        int bl=belong[L],br=belong[R];
        if(bl==br)//如果两个点在同一个区间,就直接用主席树求逆序对数。
        {
            ans=0;
            for(int i=L;i<=R;i++)
                ans+=Calc(rt[R],rt[i],1,a[i]-1,1,n);
            printf("%d\n",ans);
        }
        else
        {
            ans=anss[bl+1][R];//预处理的答案
            for(int i=L;i<=r[bl];i++)//就直接用主席树求左区间的逆序对数。
                ans+=Calc(rt[R],rt[i],1,a[i]-1,1,n);
            printf("%d\n",ans);
        }
    }
    return 0;
}

5【主席树】去月球(caioj1447)

对于每一次修改都增加一条链,将这条链跟之前的链合并。
修改我们肯定要用到lazy标记了,因为这是可持续化线段树,标记是不用下传的哦,所以不用担心会改变历史的信息。
这就是大体思路,很easy~
【参考程序】

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

#define LL long long
using namespace std;

struct node
{
    LL c,lazy,lc,rc;
} t[5100000];
int cnt;
LL rt[110000],a[110000],s[110000],now,n,m;

void Build(LL &u,LL l,LL r)
{
    if(!u)u=++cnt;
    t[u].c=s[r]-s[l-1];
    if(l==r)return ;
    LL mid=(l+r)/2;
    Build(t[u].lc,l,mid);
    Build(t[u].rc,mid+1,r);
}

void Calc(LL u,LL l,LL r,LL cl,LL cr,LL &ret,LL mark)
{
    if(l==cl&&r==cr)
    {
        ret+=t[u].c;
        ret+=mark*(r-l+1);
        return ;
    }
    LL mid=(l+r)/2;
    //标记不用下传,只用累加就好了
    if(cr<=mid)Calc(t[u].lc,l,mid,cl,cr,ret,mark+t[u].lazy);
    else if(cl>mid)Calc(t[u].rc,mid+1,r,cl,cr,ret,mark+t[u].lazy);
    else
    {
        Calc(t[u].lc,l,mid,cl,mid,ret,mark+t[u].lazy);
        Calc(t[u].rc,mid+1,r,mid+1,cr,ret,mark+t[u].lazy);
    }
}

void Link(LL &u,LL l,LL r,LL cl,LL cr,LL c)
{
    if(!u)u=++cnt;
    t[u].c+=(cr-cl+1)*c;
    if(l==cl&&r==cr)
    {
        t[u].lazy+=c;//lazy标记~
        return ;
    }
    LL mid=(l+r)/2;
    if(cr<=mid)Link(t[u].lc,l,mid,cl,cr,c);
    else if(cl>mid)Link(t[u].rc,mid+1,r,cl,cr,c);
    else
    {
        Link(t[u].lc,l,mid,cl,mid,c);
        Link(t[u].rc,mid+1,r,mid+1,cr,c);
    }
}

void Merge(LL &u1,LL u2)
{
    if(!u1)
    {
        u1=u2;
        return ;
    }
    if(!u2)return ;
    t[u1].c+=t[u2].c;
    t[u1].lazy+=t[u2].lazy;
    Merge(t[u1].lc,t[u2].lc);
    Merge(t[u1].rc,t[u2].rc);
}

int main()
{
    scanf("%lld%lld",&n,&m);
    s[0]=0;
    for(LL i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
        s[i]=s[i-1]+a[i];
    }
    cnt=0;
    Build(rt[0],1,n);
    now=0;
    while(m--)
    {
        int kk;
        scanf("%d",&kk);
        if(kk==2)
        {
            LL l,r,ret;
            scanf("%lld%lld",&l,&r);
            ret=0;
            Calc(rt[now],1,n,l,r,ret,0);
            printf("%lld\n",ret);
        }
        else if(kk==4)
        {
            LL bt;
            scanf("%lld",&bt);
            for(int i=bt+1;i<=now;i++)rt[i]=0;
            now=bt;//将不用的线段树删除即可
        }
        else if(kk==1)
        {
            LL l,r,c;
            scanf("%lld%lld%lld",&l,&r,&c);
            now++;
            Link(rt[now],1,n,l,r,c);//每次修改插入一条一棵线段树,代表一个区间的修改
            Merge(rt[now],rt[now-1]);//让其与前面的链合并,这样它维护的就是前i个时间点的修改
        }
        else if(kk==3)
        {
            LL l,r,h,ret;
            scanf("%lld%lld%lld",&l,&r,&h);
            ret=0;
            Calc(rt[h],1,n,l,r,ret,0);
            printf("%lld\n",ret);
        }
    }
    return 0;
}

【练习】

1、caioj1445:【主席树】求区间种类
2、caioj1446:【主席树】简单询问
3、caioj1448:【主席树】简单查询
4、bzoj1146:[CTSC2008]网络管理Network
5、bzoj2653:middle