主席树全纪录

本文详细介绍了主席树的概念及其在解决特定问题中的应用,如查询小于某值的元素个数、求区间第k大等,并提供了经典例题解析及代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

主席树

主席树讲解
感觉主席树就是一种功能强大的“前缀和”
裸题当然不多了,不过主席树的题目还是有一些特征的

  • 需要支持线段树的操作,同时需要历史版本

  • 多半解决第k大(权值主席树,可以查询小于某个值的元素个数
    重要的事情:可以查询小于某个值的元素个数,可以查询小于某个值的元素个数,可以查询小于某个值的元素个数

  • 主席树的形式是多种多样啦
    比较常见的是建立在序列上:序列下标,时间推移
    当然也可以建立在树上,不过这种情况就需要把树变成一个“序列”:dfs序

  • 多半是权值线段树

  • 空间40倍

经典例题:主席树求区间第k大

经典例题:主席树求树上路径第k大
关于这道题我要说两句(虽然我没有写出AC代码)

因为主席树可以查询小于某个值的元素个数

我们在解决k大问题的时候就可以用二分的思想在主席树上查找

但是树上路径的第k大有一些细节,显然我们先要处理出dfs序

一开始我以为是树链剖分+主席树,但是树链剖分会把路径分成若干个区间,无法保证信息的完整性
也就是说我们只能在外层二分答案,每次用树链剖分判断,复杂度应该是 O(nlog2n) O ( n l o g 2 n )

We can do better! W e   c a n   d o   b e t t e r !
树上的路径 (u,v)=(root,u)+(root,v)(root,lca(u,v))(root,fa[lca(u,v)]) ( u , v ) = ( r o o t , u ) + ( r o o t , v ) − ( r o o t , l c a ( u , v ) ) − ( r o o t , f a [ l c a ( u , v ) ] )
我们用主席树维护每一条树链
也就是说,我们只是按照dfs序的顺序,对于结点 dfs[i] d f s [ i ] ,ta的历史版本是 fa[dfs[i]] f a [ d f s [ i ] ] (不是 dfs[i1] d f s [ i − 1 ]
(代码中,每个结点都用新编号表示了,不过我想了一下,不用新编号也是OK的)
这样直接在主席树中查找即可
复杂度 O(logn) O ( l o g n )

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

using namespace std;

const int N=100010;
int n,nn,m,num[N],dfn[N],clo=0,pre[N][21],deep[N];
int val[N],V[N],st[N],tot=0,sz=0,root[N];
struct node{
    int y,nxt;
}way[N<<1];
struct Tree{
    int sum,lc,rc;
}t[N*40];

void add(int u,int w) {
    tot++;way[tot].y=w;way[tot].nxt=st[u];st[u]=tot;
    tot++;way[tot].y=u;way[tot].nxt=st[w];st[w]=tot;
}

void dfs(int now,int fa,int dep) {
    pre[now][0]=fa;
    deep[now]=dep;
    for (int i=1;i<20;i++) {
        if ((1<<i)>dep) break;
        pre[now][i]=pre[pre[now][i-1]][i-1];
    }
    num[now]=++clo;                            //结点在dfs中的编号(新编号) 
    dfn[clo]=now;                              //dfsfor (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa) 
            dfs(way[i].y,now,dep+1);
}

int lca(int x,int y) {
    if (deep[x]<deep[y]) swap(x,y);
    int d=deep[x]-deep[y];
    if (d)
        for (int i=0;i<20&&d;i++,d>>=1)
            if (d&1)
                x=pre[x][i];
    if (x==y) return x;
    for (int i=19;i>=0;i--)
        if (pre[x][i]!=pre[x][i]) {
            x=pre[x][i];
            y=pre[y][i];
        }
    return pre[x][0];
}

void insert(int &now,int l,int r,int v) {
    sz++;
    t[sz]=t[now];
    now=sz;
    t[now].sum++;
    if (l==r) return;
    int mid=(l+r)>>1;
    if (v<=mid) insert(t[now].lc,l,mid,v);
    else insert(t[now].rc,mid+1,r,v);
}

int solve(int x,int y,int k) {
    int a=x,b=y,c=lca(x,y),d=pre[c][0];
    a=root[num[a]],b=root[num[b]],c=root[num[c]],d=root[num[d]];
    int l=1,r=nn;
    while (l<r) {
        int mid=(l+r)>>1;
        int tmp=t[t[a].lc].sum+t[t[b].lc].sum-t[t[c].lc].sum-t[t[d].lc].sum;
        if (tmp>=k) {
            r=mid;
            a=t[a].lc; b=t[b].lc; c=t[c].lc; d=t[d].lc;
        }
        else {
            l=mid+1;
            k-=tmp;
            a=t[a].rc; b=t[b].rc; c=t[c].rc; d=t[d].rc;
        }
    }
    return V[l];
}

int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++) scanf("%d",&val[i]),V[i]=val[i];

    sort(V+1,V+1+n);
    nn=unique(V+1,V+1+n)-V-1;
    for (int i=1;i<=n;i++) val[i]=lower_bound(V+1,V+1+nn,val[i])-V;

    for (int i=1;i<n;i++) {
        int u,w;
        scanf("%d%d",&u,&w);
        add(u,w);
    }
    dfs(1,0,1);
    for (int i=1;i<=n;i++) {     //按照dfs序建立主席树 
        int now=dfn[i];
        root[i]=root[num[pre[now][0]]];
        insert(root[i],1,nn,val[now]);
    }

    for (int i=1;i<=m;i++) {
        int x,y,k;
        scanf("%d%d%d",&x,&y,&k);
        int ans=solve(x,y,k);
        printf("%d",ans); 
        if (i!=m) printf("\n");
    }
    return 0;
}

经典例题:主席树+二分
经典例题:主席树+树链剖分
主席树难题:精神污染


可修改主席树(树套树)

可修改主席树讲解

普通主席树应该只能不停的插入,一旦修改,之后的所有主席树都要修改
所以我们在外面套上一层树状数组
(毕竟主席树就是一种程度上的前缀和,和树状数组不谋而合)
这样修改的时候就只用维护 logn l o g n 棵主席树了
不过,在询问的时候我们也是需要询问 logn l o g n 棵主席树的

经典例题:可修改主席树求区间第k大

注意:
在可修改主席树中,没有root[i]=root[i-1]
因为在可修改主席树中,每棵树维护的是一个专属于ta的区间
也就是说一个区间内的元素插入一棵主席树即可
而这些主席树之间是没有关系的,不存在继承关系,直接在本身上修改即可(不可修改的主席树需要继承上一时刻的状态)

还有,我们在change的时候,还是矢志不渝的sz++;
(这样不会很浪费空间么?没错,我们只能浪费空间了)

我们需要辅助数组记录一下询问时需要的root

统计有多少元素小于当前值时,一定是统计左儿子

#include<bits/stdc++.h>

using namespace std;

const int N=10010;
int n,m,a[N],V[N],root[N],sz=0,tot=0,L[N],R[N],cntl,cntr;
struct node{
    int x,y,v,type;
}Q[N];
struct Tree{
    int sum,lc,rc;
}t[N*40];

void change(int &now,int l,int r,int v,int z) {
    sz++;
    t[sz]=t[now];
    now=sz;
    t[now].sum+=z;
    if (l==r) return;
    int mid=(l+r)>>1;
    if (v<=mid) change(t[now].lc,l,mid,v,z);
    else change(t[now].rc,mid+1,r,v,z);
}

int ask(int l,int r,int k) {
    if (l==r) return l;
    int suml=0,sumr=0;
    for (int i=1;i<=cntl;i++) 
        suml+=t[t[L[i]].lc].sum;
    for (int i=1;i<=cntr;i++)
        sumr+=t[t[R[i]].lc].sum;

    int mid=(l+r)>>1;
    int tmp=sumr-suml;
    if (tmp>=k) {
        for (int i=1;i<=cntl;i++)
            L[i]=t[L[i]].lc;
        for (int i=1;i<=cntr;i++)
            R[i]=t[R[i]].lc;
        return ask(l,mid,k);
    }
    else {
        for (int i=1;i<=cntl;i++)
            L[i]=t[L[i]].rc;
        for (int i=1;i<=cntr;i++)
            R[i]=t[R[i]].rc;
        k-=tmp;
        return ask(mid+1,r,k);
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++) scanf("%d",&a[i]),V[++tot]=a[i];
    char s[5];
    for (int i=1;i<=m;i++) {
        scanf("%s",s);
        if (s[0]=='Q') {
            scanf("%d%d%d",&Q[i].x,&Q[i].y,&Q[i].v);
            Q[i].x--;                                      //
            Q[i].type=1;
        }
        else {
            scanf("%d%d",&Q[i].x,&Q[i].v); 
            V[++tot]=Q[i].v;
            Q[i].type=2;
        }
    }

    sort(V+1,V+1+tot);
    tot=unique(V+1,V+1+tot)-V-1;
    for (int i=1;i<=n;i++) {
        a[i]=lower_bound(V+1,V+1+tot,a[i])-V;
        for (int j=i;j<=n;j+=(j&(-j)))
            change(root[j],1,tot,a[i],1);
    }

    for (int i=1;i<=m;i++) 
        if (Q[i].type==1) {
            cntl=cntr=0;
            for (int j=Q[i].x;j>0;j-=(j&(-j))) L[++cntl]=root[j];
            for (int j=Q[i].y;j>0;j-=(j&(-j))) R[++cntr]=root[j];
            printf("%d\n",V[ask(1,tot,Q[i].v)]);
        }
        else {
            for (int j=Q[i].x;j<=n;j+=(j&(-j)))
                change(root[j],1,tot,a[Q[i].x],-1);

            Q[i].v=lower_bound(V+1,V+1+tot,Q[i].v)-V;
            for (int j=Q[i].x;j<=n;j+=(j&(-j)))
                change(root[j],1,tot,Q[i].v,1);

            a[Q[i].x]=Q[i].v;
        }

    return 0; 
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值