1. 程式人生 > >主席樹再探

主席樹再探

(零基礎者出門左拐)
最近又雙叒學了主席樹,打了幾道模板題。
感覺還行
主席樹,在我看來就是線段樹的可持化 (一開始以為主席樹只是可持久化權值線段樹)。在題目中需要建多顆線段樹或權值線段樹且相鄰的線段樹差別不大(一般就一個點不一樣)時就可以用主席樹。運用可持久化的思想,我們並不需要重新構建一顆線段樹,因為只需要改一個點,所以線段樹只需要新多出\(logn\)個節點,其他的節點繼承前面的線段樹就行了(所以一般都要開始建一顆空樹)。這樣一來,我們建樹的時間複雜度和、這一堆線段樹的空間複雜度變成了\(nlogn\),真是佩服人類的智慧。

【模板】可持久化線段樹 1(主席樹)

嗯,模板題,求區間第k大,我們找到對應的兩顆線段樹\(root[r]\)

\(root[l-1]\)然後在這兩顆線段數上同時二分就行了。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=2e5+100;
int num,n,m,a[N],b[N],tot,root[N],w[N*20],ch[N*20][2];
void build(int l,int r,int &now){
    now=++num;
    if(l==r)return;
    int mid=(l+r)>>1;
    build(l,mid,ch[now][0]);
    build(mid+1,r,ch[now][1]);
}
void ins(int l,int r,int x,int pre,int &now){
    now=++num;
    w[now]=w[pre]+1;
    if(l==r)return;
    ch[now][0]=ch[pre][0];
    ch[now][1]=ch[pre][1];
    int mid=(l+r)>>1;
    if(x>mid)ins(mid+1,r,x,ch[pre][1],ch[now][1]);
    else ins(l,mid,x,ch[pre][0],ch[now][0]);
}
int check(int l,int r,int k,int pre,int now){
    if(l==r)return l;
    int tmp=w[ch[now][0]]-w[ch[pre][0]];
    int mid=(l+r)>>1;
    if(tmp>=k)return check(l,mid,k,ch[pre][0],ch[now][0]);
    else return check(mid+1,r,k-tmp,ch[pre][1],ch[now][1]);
}
int read(){
    int sum=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
    return sum*f;
}
int main(){
    n=read();m=read();
    for(int i=1;i<=n;i++)a[i]=read(),b[++tot]=a[i];
    sort(b+1,b+1+tot);
    tot=unique(b+1,b+1+tot)-b-1;
    for(int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+1+tot,a[i])-b;
    build(1,tot,root[0]);
    for(int i=1;i<=n;i++)ins(1,tot,a[i],root[i-1],root[i]);
    while(m--){
        int l=read(),r=read(),k=read();
        printf("%d\n",b[check(1,tot,k,root[l-1],root[r])]);
    }
    return 0;
}

非常短以下的主席樹不加說明就是模板的這種主席樹。


[SDOI2010]粟粟的書架

天啊,這題強行二合一。
就說說\(n=1\)的情況。一開始想的是樹狀陣列套平衡樹,或套權值線段樹的。\(m=500000\)。。。再見。
考慮用主席樹,我們在主席樹上維護兩個東西\(sum[i]\)代表權值和,\(num[i]\)代表數量和。然後在主席樹上二分,先考慮右子樹,如果\(sum[ch[now][1]]-sum[ch[pre][1]]<k,k\)就減去\(sum[ch[now][1]]-sum[ch[pre][1]]\)然後答案加上\(num[ch[now][1]]-num[ch[pre][1]]\)

,然後遞迴左子樹,否則遞迴右子樹。最後\(l=r\)時答案再加上當前\(xl>=k\)的最小正整數解。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=205;
const int MAXN=1050;
const int NN=500100;
int n,m,q,w[N][N][MAXN],num[N][N][MAXN],root[NN],sum[NN*41],Num[NN*41],cnt,ch[NN*41][2];
inline int read(){
    int sum=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
    return sum*f;
}
int getw(int x1,int y1,int x2,int y2,int k){
    return w[x2][y2][k]-w[x2][y1-1][k]-w[x1-1][y2][k]+w[x1-1][y1-1][k];
}
int getnum(int x1,int y1,int x2,int y2,int k){
    return num[x2][y2][k]-num[x2][y1-1][k]-num[x1-1][y2][k]+num[x1-1][y1-1][k];
}
void work1(){
    int a[N][N];
    int mx=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            a[i][j]=read(),mx=max(mx,a[i][j]);
    for(int k=1;k<=mx+1;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++){
                w[i][j][k]=w[i-1][j][k]+w[i][j-1][k]-w[i-1][j-1][k]+(a[i][j]>=k?a[i][j]:0);
                num[i][j][k]=num[i-1][j][k]+num[i][j-1][k]-num[i-1][j-1][k]+(a[i][j]>=k?1:0);
            }
    while(q--){
        int x=read(),y=read(),xx=read(),yy=read(),h=read();
        if(getw(x,y,xx,yy,1)<h){
            printf("Poor QLW\n");
            continue;
        }
        int l=1,r=mx;
        int ans=mx+1;;
        while(l<=r){
            int mid=(l+r)>>1;
            if(getw(x,y,xx,yy,mid)<h){
                ans=mid;
                r=mid-1;
            }
            else l=mid+1;
        }
        printf("%d\n",getnum(x,y,xx,yy,ans)+(h-getw(x,y,xx,yy,ans)-1)/(ans-1)+1);
    }
} 
void build(int l,int r,int &now){
    now=++cnt;
    if(l==r)return;
    int mid=(l+r)>>1;
    build(l,mid,ch[now][0]);
    build(mid+1,r,ch[now][1]);
}
void add(int l,int r,int x,int pre,int &now){
    now=++cnt;
    Num[now]=Num[pre]+1;
    sum[now]=sum[pre]+x;
    if(l==r)return;
    ch[now][1]=ch[pre][1];
    ch[now][0]=ch[pre][0];
    int mid=(l+r)>>1;
    if(x>mid)add(mid+1,r,x,ch[pre][1],ch[now][1]);
    else add(l,mid,x,ch[pre][0],ch[now][0]);
}
int check(int l,int r,int A,int B,int k){
    int ans=0;
    while(l<r){
        int mid=l+r>>1;
        int lch=sum[ch[B][1]]-sum[ch[A][1]];
        if(lch<k) ans+=Num[ch[B][1]]-Num[ch[A][1]],k-=lch,r=mid,B=ch[B][0],A=ch[A][0];
        else l=mid+1,B=ch[B][1],A=ch[A][1];
    }
    ans+=(k+l-1)/l;
    return ans;
}
void work2(){
    int a[NN];
    for(int i=1;i<=m;i++)a[i]=read();
    build(1,1000,root[0]); 
    for(int i=1;i<=m;i++)add(1,1000,a[i],root[i-1],root[i]);
    while(q--){
        int x=read(),y=read(),xx=read(),yy=read(),h=read();
        if(sum[root[yy]]-sum[root[y-1]]<h){
            printf("Poor QLW\n");
            continue;
        }
        printf("%d\n",check(1,1000,root[y-1],root[yy],h));
    }
}
int main(){
    n=read();m=read();q=read();
    if(n!=1)work1();
    else work2();
    return 0;
}

[SDOI2013]森林

詢問一個森林中兩點間路徑經過點的第k小,動態連邊。保證是一個森林。強制線上。

首先我們可以在樹上建主席樹,每一個點建一顆權值線段樹,繼承他父親線段樹的資訊。
然後處理詢問時,我們可以用\(sum[x]+sum[y]-sum[lca]-sum[fa[lca]]\)來表示\(x,y\)路徑上的權值線段樹。然後我們在這4個權值線段樹樹上二分就行了。
然後我們如何處理,動態連邊呢。我們用啟發式合併的思想,把小的樹接在大的樹上面,然後在小樹裡\(dfs\)重構就行了。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=101000;
int cnt,head[N],f[N],fa[N][23],dep[N],root[N],a[N],size[N],b[N],tot;
int sum[N*600],ch[N*600][2],num,n,m,q,ans;
struct edge{
    int to,nxt;
}e[N*2];
void add_edge(int u,int v){
    cnt++;
    e[cnt].nxt=head[u];
    e[cnt].to=v;
    head[u]=cnt;
    cnt++;
    e[cnt].nxt=head[v];
    e[cnt].to=u;
    head[v]=cnt;
}
int find(int x){
    if(f[x]==x)return x;
    else return f[x]=find(f[x]);
}
void merge(int x,int y){
    int fx=find(x),fy=find(y);
    f[fy]=fx;size[fx]+=size[fy];
}
int getlca(int x,int y){
    if(dep[x]<dep[y])swap(x,y);
    for(int i=20;i>=0;i--)
        if(dep[fa[x][i]]>=dep[y])x=fa[x][i];
    if(x==y)return x;
    for(int i=20;i>=0;i--)
        if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
    return fa[x][0];
}
void build(int l,int r,int &now){
    now=++num;
    if(l==r)return;
    int mid=(l+r)>>1;
    build(l,mid,ch[now][0]);
    build(mid+1,r,ch[now][1]);
}
void add(int l,int r,int x,int pre,int &now){
    now=++num;
    sum[now]=sum[pre]+1;
    if(l==r)return;
    ch[now][0]=ch[pre][0];
    ch[now][1]=ch[pre][1];
    int mid=(l+r)>>1;
    if(x>mid)add(mid+1,r,x,ch[pre][1],ch[now][1]);
    else add(l,mid,x,ch[pre][0],ch[now][0]);
}
void dfs(int u,int f){
    fa[u][0]=f;
    for(int i=1;i<=20;i++)fa[u][i]=fa[fa[u][i-1]][i-1];
    dep[u]=dep[f]+1;
    add(1,tot,a[u],root[f],root[u]);
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==f)continue;
        dfs(v,u);
    }
}
int check(int x,int y,int lca,int flca,int l,int r,int k){
    while(l<r){
        int mid=(l+r)>>1;
        int tmp=sum[ch[x][0]]+sum[ch[y][0]]-sum[ch[lca][0]]-sum[ch[flca][0]];
        if(tmp>=k)x=ch[x][0],y=ch[y][0],lca=ch[lca][0],flca=ch[flca][0],r=mid;
        else k-=tmp,x=ch[x][1],y=ch[y][1],lca=ch[lca][1],flca=ch[flca][1],l=mid+1;
    }
    return b[l];
}
int read(){
    int sum=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
    return sum*f;
}
int main(){
    int hhh=read();
    n=read(),m=read(),q=read();
    for(int i=1;i<=n;i++)f[i]=i,size[i]=1,a[i]=read(),b[i]=a[i];
    sort(b+1,b+1+n);
    tot=unique(b+1,b+1+n)-b-1;
    for(int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+1+tot,a[i])-b;
    build(1,tot,root[0]);
    for(int i=1;i<=m;i++){
        int u=read(),v=read();
        add_edge(u,v);merge(u,v);
    }
    for(int i=1;i<=n;i++)
        if(f[i]==i)dfs(i,0);
    char s[3];
    while(q--){
        scanf("%s",s);
        if(s[0]=='Q'){
            int x=read(),y=read(),k=read();
            x^=ans;y^=ans;k^=ans;
            int lca=getlca(x,y);
            ans=check(root[x],root[y],root[lca],root[fa[lca][0]],1,tot,k);
            printf("%d\n",ans);
        }
        else{
            int x=read()^ans,y=read()^ans;
            if(size[x]>size[y])swap(x,y);
            add_edge(x,y);merge(x,y);dfs(y,x);
        }
    }
    return 0;
}

[CQOI2015]任務查詢系統

一開始受到HNOI2015摘果子的啟發寫了一發樹套樹,然後就T了

這題要求求覆蓋一個點的前k小區間和。強制線上。

但主席樹的解法其實差不多,因為主席樹有字首和的思想,我們把每一個區間拆開,在\(l\)處加區間的權值,在\(r+1\)處減區間的權值,然後用主席樹維護一個字首和。
我們就可以單點查詢了。在主席樹上二分就行了。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
const int N=1e5+10;
int read(){
    int sum=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
    return sum*f;
}
int n,m,num,tot;
int a[N],b[N],root[N<<6];
long long ans=1;
struct tree {
    long long sum; 
    int cnt,l,r;
}t[N<<6];
vector<int>be[N],ed[N];
void update(int &u,int l,int r,int pre,int pos,int v){
    u=++tot; t[u]=t[pre];
    t[u].cnt+=v, t[u].sum+=1ll*v*b[pos];
    if(l==r) return;
    int mid=(l+r)>>1;
    if(pos<=mid) update(t[u].l,l,mid,t[pre].l,pos,v);
    else update(t[u].r,mid+1,r,t[pre].r,pos,v);
}
long long query(int u,int l,int r,int k){
    int num=t[t[u].l].cnt;
    if(l==r) return t[u].sum/(1ll*t[u].cnt)*1ll*k;
    int mid=(l+r)>>1;
    if(k<=num) return query(t[u].l,l,mid,k);
    else return query(t[u].r,mid+1,r,k-num)+t[t[u].l].sum;
}
int main(){
    m=read(),n=read();
    for(int i=1;i<=m;i++) {
        int x=read(),y=read();
        a[i]=read(),b[i]=a[i];
        be[x].push_back(i), ed[y+1].push_back(i);
    }
    sort(b+1,b+1+m); int num=unique(b+1,b+1+m)-b-1;
    for(int i=1;i<=n;i++) {
        root[i]=root[i-1];
        for(int j=0;j<be[i].size();j++) {
            int p=lower_bound(b+1,b+1+num,a[be[i][j]])-b;
            update(root[i],1,num,root[i],p,1);
        }
        for(int j=0;j<ed[i].size();j++) {
            int p=lower_bound(b+1,b+1+num,a[ed[i][j]])-b;
            update(root[i],1,num,root[i],p,-1);
        }
    }
    for(int i=1;i<=n;i++) {
        int x=read(),a=read(),b=read(),c=read(),k=(1ll*a*ans+b)%c+1;
        if(k>t[root[x]].cnt) ans=t[root[x]].sum;
        else ans=query(root[x],1,num,k);
        printf("%lld\n",ans);
    }
    return 0;
}

[國家集訓隊]middle

二分答案想到了,然後小於的為-1,大於等於的為1,中間一塊加上,一個最大字首,一個最大字尾的套路也知道,然後就不會了。
聽說是陳立傑出的題
然後該怎麼辦?上主席樹,我們一個初步的想法是每一個權值,都建一顆1和-1的普通線段樹上面維護區間和,區間最大字首,最大字尾。然後我們發現,相鄰兩個權值的線段樹,很相近,只有一個權值的點不一樣,只需要改\(logn\)個節點,然後就是主席樹了。那麼假如一個權值有很多點該怎麼辦。只需要一個一個插入,然後以最後一次插入完成後的根作為這個權值的根就行了。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
const int N=20100;
vector<int> vec[N];
int n,m,a[N],b[N],ans,root[N],num,q[6];
int sum[N*20],ml[N*20],mr[N*20],tot,ch[N*20][2];
int read(){
    int sum=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
    return sum*f;
}
void update(int now){
    sum[now]=sum[ch[now][0]]+sum[ch[now][1]];
    ml[now]=max(ml[ch[now][0]]+sum[ch[now][1]],ml[ch[now][1]]);
    mr[now]=max(mr[ch[now][1]]+sum[ch[now][0]],mr[ch[now][0]]);
}
void build(int l,int r,int &now){
    if(now==0)now=++tot;
    if(l==r){
        sum[now]=ml[now]=mr[now]=1;
        return ;
    }
    int mid=(l+r)>>1;
    build(l,mid,ch[now][0]);
    build(mid+1,r,ch[now][1]);
    update(now);
}
void add(int l,int r,int x,int pre,int &now){
    now=++tot;
    if(l==r){sum[now]=ml[now]=mr[now]=-1;return;}
    ch[now][0]=ch[pre][0];
    ch[now][1]=ch[pre][1];
    int mid=(l+r)>>1;
    if(x>mid)add(mid+1,r,x,ch[pre][1],ch[now][1]);
    else add(l,mid,x,ch[pre][0],ch[now][0]);
    update(now);
}
int check(int l,int r,int L,int R,int now){
    if(L>R)return 0;
    if(l==L&&r==R){
        return sum[now];
    }
    int mid=(l+r)>>1;
    if(L>mid)return check(mid+1,r,L,R,ch[now][1]);
    else if(R<=mid)return check(l,mid,L,R,ch[now][0]);
    else return check(l,mid,L,mid,ch[now][0])+check(mid+1,r,mid+1,R,ch[now][1]); 
}
int check_L(int l,int r,int L,int R,int now){
    if(l==L&&r==R)return ml[now];
    int mid=(l+r)>>1;
    if(L>mid)return check_L(mid+1,r,L,R,ch[now][1]);
    else if(R<=mid)return check_L(l,mid,L,R,ch[now][0]);
    else{
        int tmp1=check_L(mid+1,r,mid+1,R,ch[now][1]);
        int tmp2=check(mid+1,r,mid+1,R,ch[now][1])+check_L(l,mid,L,mid,ch[now][0]);
        return max(tmp1,tmp2);
    }
}
int check_R(int l,int r,int L,int R,int now){
    if(l==L&&r==R)return mr[now];
    int mid=(l+r)>>1;
    if(L>mid)return check_R(mid+1,r,L,R,ch[now][1]);
    else if(R<=mid)return check_R(l,mid,L,R,ch[now][0]);
    else{
        int tmp1=check_R(l,mid,L,mid,ch[now][0]);
        int tmp2=check(l,mid,L,mid,ch[now][0])+check_R(mid+1,r,mid+1,R,ch[now][1]);
        return max(tmp1,tmp2);
    }
}
bool judge(int x){
    int tmp1=check(1,n,q[2]+1,q[3]-1,root[x]);
    int tmp2=check_L(1,n,q[1],q[2],root[x]);
    int tmp3=check_R(1,n,q[3],q[4],root[x]);
    if(tmp1+tmp2+tmp3>=0)return true;
    else return false; 
}
int work(){
    int l=1,r=num,tmp;
    while(l<=r){
        int mid=(l+r)>>1;
        if(judge(mid)){
            tmp=mid;
            l=mid+1;
        }
        else r=mid-1;
    }
    return tmp;
}
int main(){
    n=read();
    for(int i=1;i<=n;i++)a[i]=read(),b[i]=a[i];
    sort(b+1,b+1+n);
    num=unique(b+1,b+1+n)-b-1;
    build(1,n,root[0]);
    for(int i=1;i<=n;i++){
        a[i]=lower_bound(b+1,b+1+num,a[i])-b;
        vec[a[i]].push_back(i);
    }
    for(int i=1;i<=num;i++){
        root[i]=root[i-1];
        if(vec[i-1].size())for(int j=0;j<vec[i-1].size();j++)
            add(1,n,vec[i-1][j],root[i],root[i]);
    }
    m=read();
    while(m--){
        q[1]=(read()+ans)%n+1,q[2]=(read()+ans)%n+1,q[3]=(read()+ans)%n+1,q[4]=(read()+ans)%n+1;
    //  q[1]=read(),q[2]=read(),q[3]=read(),q[4]=read();
        sort(q+1,q+1+4);
        ans=work();
        printf("%d\n",b[ans]);
        ans=b[ans];
    }
    return 0;
}

總結一下。主席樹一般可以幹什麼?區間第k大,區間大於x的數的個數(和)。一般都需要在主席樹上二分。然後主席樹有字首和的思想。可以很好的應用在樹上用差分拼出一個路徑。也可以求出差分再用主席樹求字首,支援單點查詢,對於包含一個點的區間的資訊,我們這一用到這個技巧。然後就是對於建很多顆線段樹,但相鄰的線段樹比較相近時,也可以可持久化。