权值线段树,动态开点与线段树合并

· · 算法·理论

不好吃的线段树半家桶。

权值线段树

本质是叶子节点下标意义的变化。

一般情况下,线段树的每条线段负责维护区间 a_l\sim a_r 的信息。存在两个叶子节点 tree_l,tree_r,有 tree_l.l=tree_l.r=l,tree_r.l=tree_r.r=r。权值线段树表示为:tree_l.l=tree_l.r=a_l,tree_r.l=tree_r.r=a_r

翻译成人话:用线段树维护桶。

进而可以实现以下操作:

之类的东西。

struct Segment_Tree{
    #define leftson(p) p<<1
    #define rightson(p) p<<1|1
    #define push_up(p) tree[p].val=tree[leftson(p)].val+tree[rightson(p)].val
    struct TREE{
        int l,r;
        int val;//l,r:当前节点所管辖的元素l~r。val:区间内的元素总数
    }tree[MAXN<<2];
    void build(int l,int r,int p){
        tree[p].l=l,tree[p].r=r,tree[p].val=0;
        if(l==r){return;}
        int mid=l+r>>1;
        build(l,mid,leftson(p));
        build(mid+1,r,rightson(p));
        push_up(p);
    }
    void update(int x,int v,int p){//将元素x增加v个
        if(tree[p].l==tree[p].r){
            tree[p].val+=v;
            return;
        }
        int mid=tree[p].l+tree[p].r>>1;
        if(x<=mid)update(x,v,leftson(p));
        else update(x,v,rightson(p));
    }//就是单点修改
    int query(int l,int r,int p){//查询元素l~r内的总元素个数
        if(tree[p].l>=l&&tree[p].r<=r)return tree[p].val;
        int mid=tree[p].l+tree[p].r>>1;
        int res=0;
        if(l<=mid)res+=query(l,r,leftson(p));
        if(r>mid)res+=query(l,r,rightson(p));
        return res;
    }//就是区间查询
    int kth(int k,int p){//查询序列中第k大的元素是哪个
        if(tree[p].l==tree[p].r)return tree[p].l;//找到了返回元素
        int mid=tree[p].l+tree[p].r>>1;
        int s1=tree[leftson(p)].val,s2=tree[rightson(p)].val;//我们令线段树管辖的序列是递增的,右儿子的元素普遍较大
        if(k<=s2)return kth(k,rightson(p));//如果k就在右儿子里就返回
        else return kth(k-s2,leftson(p));//如果k在左儿子里,就找左儿子中第k-s2大的元素。
    }
}ST;

动态开点线段树

我称其为和面流线段树,水多加面面多加水。

一般线段树要开 4N 的空间,事实上需要的点在 1\sim 2N是稠密的,在2N\sim4N 是稀疏的。并且我们根据进制思想,一棵维护区间 a_1\sim a_n 的线段树实际需要的空间为:\sum_{i=0}^{logn}2^i<2n,因此每棵线段树都浪费了至少 2N 的空间。

观察语句:

#define leftson(p) p<<1

#define rightson(p) p<<1|1

这样可以使每个节点 p 的左右儿子编号都是 2p,2p+1 且不出现重复。同时也导致开了大量闲置的叶子节点。

节点编号是可以做到连续的,我们只需指定每个节点的左右儿子编号即可。

至于“水多加面”:只有我们要更新或查询一个先前还没有指定儿子节点变好的节点时,我们才去更新这个点的子节点信息,而不是在 ST.build() 函数中全部写好。

2024.2.5 updated:

可见动态开点线段树是现场钦定儿子的管辖范围的。由于修改一般是单点修改,所以添加一个点一定只会落在一个子节点中,所以在开点时会导致一个节点缺少一个儿子,因为目前那个儿子所管辖的区间还没有元素被加入。

对空间的节省使得动态开点权值线段树拥有天然的离散性,进而如果使用了此数据结构是不需要对值域离散化的。

以下是实现。

struct Segment_Tree{//线段树维护区间修改和单点查询。
    int tot=1,root=1;//tot代表节点编号,root指向树根,因为形如(&p)的函数是无法向其中填入常量(例如1)的。 
    struct TREE{
        int lson,rson;//一个节点所管理的区间和自己的编号没有关系,只和父节点管理的区间有关。
        //如此追溯直到 tree[1].l=1,tree[1].r=n。进而每个节点是不必要也不能定义l,r的。
        int val,tag;
    }tree[MAXN<<1];//只需要二倍空间。然而线段树合并时得开20倍,原因之后说。
    inline void push_up(int p){
        tree[p].val=tree[tree[p].lson].val+tree[tree[p].rson].val;
        return;//此时左右子节点就不是p*2之类的了,是自己定义出来的儿子下标。
    }
    inline void spread(int p,int l,int r){//与一般线段树最大的不同在下放操作。
    //p为父节点的下标,l,r为父节点管辖的区间,由tree[1]递推得来。
        if(!tree[p].lson)tree[p].lson=++tot;
        if(!tree[p].rson)tree[p].rson=++tot;//如果子节点还没用就不管,用了发现没定义就就地定义。
        int mid=l+r>>1;
        tree[tree[p].lson].val+=tree[p].tag*(mid-l+1);//就地赋予子节点管辖区间并作修改。
        tree[tree[p].rson].val+=tree[p].tag*(r-mid);//lson管l~mid,rson为mid+1~r。
        tree[tree[p].lson].tag+=tree[p].tag;
        tree[tree[p].rson].tag+=tree[p].tag;
        tree[p].tag=0;
    }
    inline void update(int l,int r,int k,int ul,int ur,int &p){//由于节点管辖区间不是固定的,所以每次要从[1,n]推得。
    //l,r为当前节点管辖区间,ul,ur为修改区间。
    //当前节点p可能是没开过的,如果真没开过那就就地给父亲节点的儿子赋值就行,所以是&p。
        if(!p)p=++tot;
        if(l>=ul&&r<=ur){
            tree[p].val+=k*(r-l+1);
            tree[p].tag+=k;//和一般线段树一样。
            return;
        }
        spread(p,l,r);//每次下放的父亲节点要指明其管辖区间。
        int mid=l+r>>1;
        if(ul<=mid)update(l,mid,k,ul,ur,tree[p].lson);//安排左子节点区间并修改。
        if(ur>mid)update(mid+1,r,k,ul,ur,tree[p].rson);//安排右子节点区间并修改。
        push_up(p);
    }
    inline int query(int l,int r,int ux,int p){
        if(!p)return 0;//没开过这个点,也就是从来没更新过,直接返回0。
        if(l>=ux&&r<=ux)return tree[p].val;
        spread(p,l,r);
        int mid=l+r>>1;
        int ans=0;
        if(ux<=mid)return query(l,mid,ux,tree[p].lson);
        else return query(mid+1,r,ux,tree[p].rson);//同上操作。
    }
}ST;

例题

将询问按照右端点排序。

这个人在区间上只会采出现了至少两次的花。换句话说,点 a_i 与其颜色 col_i 对答案有贡献,当且仅当 col_i 上次出现的位置 last_{col_i} 满足 l<=last_{col_i}<a_i<=r。预处理一下即可。

loc_i 表示 col_i 上次出现的位置,last_icol_i 本次出现的位置,则区间 last_i\sim col_i 上的答案增加 1 贡献,没有 last 就更新一下,有 last 就直接增加答案,用线段树维护区间。

然后就被卡了。

题解里用的是树状数组,也就是说我们以很细小的冗余时间T了本题,那么就可以用动态开点优化,因为没开过的点根本就不用看。

#include<bits/stdc++.h>
#define MAXN 2000005
using namespace std;
int n,c,m;
int col[MAXN],loc[MAXN],last[MAXN];
struct node{
    int l,r,id;
}q[MAXN];
int ans[MAXN];
bool cmp(node a,node b){
    return a.r<b.r;
}
struct Segment_Tree{//上面的板子就是从这粘来的,不解释了。
    int tot=1,root=1;
    struct TREE{
        int lson,rson;
        int val,tag;
    }tree[MAXN<<1];
    inline void push_up(int p){
        tree[p].val=tree[tree[p].lson].val+tree[tree[p].rson].val;
        return;
    }
    inline void spread(int p,int l,int r){
        if(!tree[p].lson)tree[p].lson=++tot;
        if(!tree[p].rson)tree[p].rson=++tot;
        int mid=l+r>>1;
        tree[tree[p].lson].val+=tree[p].tag*(mid-l+1);
        tree[tree[p].rson].val+=tree[p].tag*(r-mid);
        tree[tree[p].lson].tag+=tree[p].tag;
        tree[tree[p].rson].tag+=tree[p].tag;
        tree[p].tag=0;
    }
    inline void update(int l,int r,int k,int ul,int ur,int &p){
        if(!p)p=++tot;
        if(l>=ul&&r<=ur){
            tree[p].val+=k*(r-l+1);
            tree[p].tag+=k;
            return;
        }
        spread(p,l,r);
        int mid=l+r>>1;
        if(ul<=mid)update(l,mid,k,ul,ur,tree[p].lson);
        if(ur>mid)update(mid+1,r,k,ul,ur,tree[p].rson);
        push_up(p);
    }
    inline int query(int l,int r,int ux,int p){
        if(!p)return 0;
        if(l>=ux&&r<=ux)return tree[p].val;
        spread(p,l,r);
        int mid=l+r>>1;
        int ans=0;
        if(ux<=mid)return query(l,mid,ux,tree[p].lson);
        else return query(mid+1,r,ux,tree[p].rson);
    }
}ST;
signed main(){
    scanf("%d%d%d",&n,&c,&m);
    for(int i=1;i<=n;i++){
        scanf("%lld",&col[i]);
        loc[i]=last[col[i]];
        last[col[i]]=i;//花的顺序不会变,就地预处理。
    }
    for(int i=1,l,r;i<=m;i++){
        scanf("%d%d",&l,&r);
        q[i].l=l,q[i].r=r;q[i].id=i;
    }
    sort(q+1,q+1+m,cmp);
    int tmp=1;
    for(int i=1;i<=n;i++){
        if(loc[i])ST.update(1,n,1,loc[loc[i]]+1,loc[i],ST.root);//能更新就更新
        while(q[tmp].r==i&&tmp<=m){
            ans[q[tmp].id]=ST.query(1,n,q[tmp].l,ST.root);
            ++tmp;//排好序后线性处理询问,我们只需要知道l点能采多少即可。 
        }
        if(tmp>m)break;
    }
    for(int i=1;i<=m;i++)
        printf("%d\n",ans[i]);
    return 0;
}

线段树合并

顾名思义,就是把两棵线段树合并一棵线段树,保留或合并两棵树已有的数据(如果有可加性的话),这颗线段树的节点信息,编号等都由先前两棵树得来。

这个东西的全名叫动态开点权值线段树合并。也就是缝合了前面提到的两者的一种操作。

对于两棵待合并的线段树 A,B,合并操作实现如下:

    int merge(int x,int y){
        if(!x)return y;
        if(!y)return x;
        ls(x)=merge(ls(x),ls(y));
        rs(x)=merge(rs(x),rs(y));//合并节点编号
        push_up(x);//合并节点信息
        return x;//返回合并好的节点编号,与无注释两行同作用。
    }

例题

不妨让权值线段树维护:当前节点的子树中有多少节点权值比它大。叶子节点存的是当前元素有多少个,query时直接扫数量即可。

然而影响当前节点答案的是它的子树。我们要先对树结构进行 dfs ,退出当前节点时合并他自己以及他的所有子节点,就地统计答案后方能得到“自己的子树中有多少大于它权值的点”。

void dfs(int u,int fa){
    for(int i=0;i<edge[u].size();i++){
        int v=edge[u][i];
        if(v==fa)continue;
        dfs(v,u);
        rt[u]=ST.merge(rt[u],rt[v]);
    }
    ans[u]=ST.query(1,n,val[u]+1,rt[u]);//query为查询有多少点权大于等于当前点。这是权值线段树,直接看节点val[u]右侧节点值之和即可。
    //由上文策略,退出当前节点时统计一下线段树中有多少点大于自己。
    //之后本节点会作为子节点合并到他的父节点,统计父节点时再用。
}

但是这个数据太大了得先离散化。

int n;
struct node{
    int v,id;
}vaL[MAXN];
bool cmp(node a,node b){
    return a.v<b.v;
}
int val[MAXN],rt[MAXN];
map<int,int>nti;
int itn[MAXN],tmp;

...

int main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)scanf("%lld",&vaL[i].v),val[i]=vaL[i].v,vaL[i].id=i;
    sort(vaL+1,vaL+1+n,cmp);
    for(int i=1;i<=n;i++){
        if(!nti[vaL[i].v])nti[vaL[i].v]=++tmp,itn[tmp]=vaL[i].v;
        val[vaL[i].id]=nti[vaL[i].v];
    }
//网上好多是lowerbound和unique离散化,看不懂就先不这么用了。

由于线段树合并要求的是动态开点的权值线段树,故上传操作如下:

    void update(int l,int r,int k,int &p){
        if(!p)p=++tot;//没有当前节点或者儿子时先开点。
        if(l==r){//到达叶子节点后维护当前元素有多少个。
            ++tree[p].val;
            return;
        }
        int mid=l+r>>1;//没有到达叶子节点时维护当前区间内有多少元素
        if(k<=mid)update(l,mid,k,ls(p));//出于&p的设定,赋值儿子节点编号是顺带的。
        else update(mid+1,r,k,rs(p));
        push_up(p);
    }
                                 ...
signed main(){
...
    for(int i=2,fa;i<=n;i++){
        scanf("%lld",&fa);
        edge[fa].push_back(i);
        edge[i].push_back(fa);//建图,给dfs跑。
    }
    for(int i=1;i<=n;i++)ST.update(1,n,val[i],rt[i]);//出于&p的设定,赋值当前点在线段树中的实际点编号也是顺带的。
    ...

    return 0;                                 
}

然后是查询与合并操作。

    int query(int l,int r,int x,int p){//元素值为(l,r)的区间中有多少是大于等于x的?
        if(!p)return 0;//要查询的节点是开过的点,没开过的就返回0
        if(l>=x)return tree[p].val;//当前节点区间左端点值已经大于x了。
        //权值线段树中节点编号直接反应权值大小。直接加上整个节点的数目。
        int res=0;
        int mid=l+r>>1;
        if(mid>=x)res+=query(l,mid,x,ls(p));//x卡在半区间左侧,说明(l,mid)内也有可能有大于等于x的元素存在。
        res+=query(mid+1,r,x,rs(p));//找右半区间有多少大于等于x的节点。

        //因而查询区间被分割为:完全大于x的区间,可能大于x的区间。
        //可能大于x的区间最终会因为二分值mid而被完全分割成完全大于x的小段。
        return res;
    }
    int merge(int x,int y){//线段树合并操作,上文就是从这里复制的。
        if(!x)return y;
        if(!y)return x;
        ls(x)=merge(ls(x),ls(y));
        rs(x)=merge(rs(x),rs(y));
        push_up(x);
        return x;
    }

大概就是这样了。放一下总代码水字数。

#include<bits/stdc++.h>
#define int long long
#define MAXN 100005
using namespace std;
int n;
struct node{
    int v,id;
}vaL[MAXN];
bool cmp(node a,node b){
    return a.v<b.v;
}
int val[MAXN],rt[MAXN];
map<int,int>nti;
int itn[MAXN],tmp;
vector<int>edge[MAXN];
struct Segment_Tree{
    #define ls(p) tree[p].lson
    #define rs(p) tree[p].rson
    #define push_up(p) tree[p].val=tree[ls(p)].val+tree[rs(p)].val
    int tot;
    struct TREE{
        int val;
        int lson,rson;
    }tree[MAXN*20];
    void update(int l,int r,int k,int &p){
        if(!p)p=++tot;
        if(l==r){
            ++tree[p].val;
            return;
        }
        int mid=l+r>>1;
        if(k<=mid)update(l,mid,k,ls(p));
        else update(mid+1,r,k,rs(p));
        push_up(p);
    }
    int query(int l,int r,int x,int p){
        if(!p)return 0;
        if(l>=x)return tree[p].val;
        int res=0;
        int mid=l+r>>1;
        if(mid>=x)res+=query(l,mid,x,ls(p));
        res+=query(mid+1,r,x,rs(p));
        return res;
    }
    int merge(int x,int y){
        if(!x)return y;
        if(!y)return x;
        ls(x)=merge(ls(x),ls(y));
        rs(x)=merge(rs(x),rs(y));
        push_up(x);
        return x;
    }
}ST;
int ans[MAXN];
void dfs(int u,int fa){
    for(int i=0;i<edge[u].size();i++){
        int v=edge[u][i];
        if(v==fa)continue;
        dfs(v,u);
        rt[u]=ST.merge(rt[u],rt[v]);
    }
    ans[u]=ST.query(1,n,val[u]+1,rt[u]);
}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)scanf("%lld",&vaL[i].v),val[i]=vaL[i].v,vaL[i].id=i;
    sort(vaL+1,vaL+1+n,cmp);
    for(int i=1;i<=n;i++){
        if(!nti[vaL[i].v])nti[vaL[i].v]=++tmp,itn[tmp]=vaL[i].v;
        val[vaL[i].id]=nti[vaL[i].v];
    }
    for(int i=2,fa;i<=n;i++){
        scanf("%lld",&fa);
        edge[fa].push_back(i);
        edge[i].push_back(fa);
    }
    for(int i=1;i<=n;i++)ST.update(1,n,val[i],rt[i]);
    dfs(1,0);
    for(int i=1;i<=n;i++)printf("%lld\n",ans[i]);
    return 0;
}

最后只会留下一颗线段树。然这棵线段树集成了先前小树的各种节点。先前的小树也可以被理解为是“残破的”,因为只负责维护部分信息。也就是说整个 dfs 与合并过程都是建树过程

本题中的查询操作对“线段树的残破性”是必须的。当前处理的节点对“残破性”的要求是:线段树只能维护他自己以及他的子节点。自始至终只来得及开一颗线段树,在合并过程中就满足了部分点的答案,进行更新即可。

为什么不用板题讲?

因为我用的网上的树剖+树上差分过的。

所以在这讲一下树上差分。

树上差分

最早的想法是靠板题学线段树合并。然而

刚学完树剖。树上路径修改状操作反正我是只会用树剖写的。

然后发现不会写,查看题解,确实有树剖写法,用到了树上差分。

树上差分就是序列上差分的扩展。

差分树上,节点 i 的值 a_i 反应的是点 i 的实际权值 val_i 与其父亲节点权值 val_{fa} 的权值差,因此,进行路径 (1,i) 的遍历并汇总即可还原节点 i

进行路径遍历并汇总即可还原节点。这句话很重要,看不懂代码先想想明不明白这句话的意思。

完成剖分后,对于一条链的底端点的下一个点 x 与顶点 top。在当前路径上增加救济粮 k 可以理解为:

val[x]-=k;
val[top]+=k;
namespace HPD{
    void modify(int x,int y,int k){
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]])swap(x,y);
            tags[dfn[top[x]]].push_back(k);//对链上的点打tag
            tags[dfn[x]+1].push_back(-k);//更新到点x,实际要更新dfn[x]+1这个点的差分值。
            x=father[top[x]]; 
        }
        if(dep[x]>dep[y])swap(x,y);
        tags[dfn[x]].push_back(k),tags[dfn[y]+1].push_back(-k);
    }
}

至于为什么要打tag,向量tag的具体意义:

题目中给出的操作是:给路径上放类型 z 的救济粮,然我们现在实现的是给路径上每个点权值加上 z

但是我们忽略掉了剖分时的线段树的类型。

如果这是一棵权值线段树,“给节点权值加 z”这样的操作实际为:将权值 z 的个数 +1

线段树叶子节点 p 是一个桶。对于叶子节点,tree[p].val 为第 p 种救济粮的个数。对于非叶子节点,tree[p].valp 所管辖的第 (l,r) 类救济粮中最多个数的那个的个数,tree[p].res 则为最多个数救济粮的种类。由此写出线段树。

struct Segment_Tree{
    #define leftson(p) p<<1
    #define rightson(p) p<<1|1
    struct TREE{
        int l,r;
        int val,tag;
        int res;
    }tree[MAXN<<2];
    void push_up(int p){
        if(tree[leftson(p)].val>=tree[rightson(p)].val)tree[p].res=tree[leftson(p)].res,tree[p].val=tree[leftson(p)].val;//选举个数最多救济粮的信息并存储到父亲节点。
        else tree[p].res=tree[rightson(p)].res,tree[p].val=tree[rightson(p)].val;
    }
    void build(int l,int r,int p){
        tree[p].l=l,tree[p].r=r,tree[p].val=tree[p].tag=0;
        if(l==r){tree[p].res=l;return;}//初始化编号
        int mid=l+r>>1;
        build(l,mid,leftson(p));
        build(mid+1,r,rightson(p));
        push_up(p);
    }   
    void update(int x,int k,int p){//将第x种救济粮个数增加k
        if(tree[p].l==tree[p].r){
            tree[p].val+=k;
            tree[p].tag+=k;
            return;
        }
        int mid=tree[p].l+tree[p].r>>1;
        if(x<=mid)update(x,k,leftson(p));
        else update(x,k,rightson(p));
        push_up(p);
    }
}ST;

关于答案统计。

重链剖分是按照 dfn 建树的,因此还原节点时也用 dfn 统计。

    for(int i=1;i<=n;i++){
        for(int id=0;id<tags[i].size();id++){
            int num=tags[i][id];
            if(num>0)ST.update(num,1,1);
            else ST.update(-num,-1,1);
        }
        ans[rk[i]]=ST.tree[1].val?ST.tree[1].res:0;
    }
在树的 $dfs$ 序上扫描点,扫描到一个点时用权值线段树更新点信息。 将一条链拆解为 $(l_1,r_1)...(l_m,r_m)$ 后,实际为在 $l_i$ 处放置 $z$ 品种救济粮,在 $r_i+1$ 处减去这个救济粮。 根据树上差分的思想还原节点,统计答案即可。 ```cpp #include<bits/stdc++.h> #define MAXN 100005 using namespace std; int n,q; vector<int> edge[MAXN]; int father[MAXN],son[MAXN],siz[MAXN],dep[MAXN]; void dfs1(int u,int fa){ dep[u]=dep[fa]+1; father[u]=fa; siz[u]=1; int maxson=-1; for(int i=0;i<edge[u].size();i++){ int v=edge[u][i]; if(v==fa)continue; dfs1(v,u); siz[u]+=siz[v]; if(siz[v]>maxson)son[u]=v,maxson=siz[v]; } } int top[MAXN],dfn[MAXN],rk[MAXN],tim; void dfs2(int u,int topf){ top[u]=topf; dfn[u]=++tim; rk[tim]=u; if(!son[u])return; dfs2(son[u],topf); for(int i=0;i<edge[u].size();i++){ int v=edge[u][i]; if(v==father[u]||v==son[u])continue; dfs2(v,v); } } struct Segment_Tree{ #define leftson(p) p<<1 #define rightson(p) p<<1|1 struct TREE{ int l,r; int val,tag; int res; }tree[MAXN<<2]; void push_up(int p){ if(tree[leftson(p)].val>=tree[rightson(p)].val)tree[p].res=tree[leftson(p)].res,tree[p].val=tree[leftson(p)].val; else tree[p].res=tree[rightson(p)].res,tree[p].val=tree[rightson(p)].val; } void build(int l,int r,int p){ tree[p].l=l,tree[p].r=r,tree[p].val=tree[p].tag=0; if(l==r){tree[p].res=l;return;} int mid=l+r>>1; build(l,mid,leftson(p)); build(mid+1,r,rightson(p)); push_up(p); } void update(int x,int k,int p){ if(tree[p].l==tree[p].r){ tree[p].val+=k; tree[p].tag+=k; return; } int mid=tree[p].l+tree[p].r>>1; if(x<=mid)update(x,k,leftson(p)); else update(x,k,rightson(p)); push_up(p); } }ST; int range; vector<int>tags[MAXN]; namespace HPD{ void modify(int x,int y,int k){ while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); tags[dfn[top[x]]].push_back(k); tags[dfn[x]+1].push_back(-k); x=father[top[x]]; } if(dep[x]>dep[y])swap(x,y); tags[dfn[x]].push_back(k),tags[dfn[y]+1].push_back(-k); } } int ans[MAXN]; int main(){ scanf("%d%d",&n,&q); for(int i=1,u,v;i<n;i++){ scanf("%d%d",&u,&v); edge[u].push_back(v); edge[v].push_back(u); } dfs1(1,0); dfs2(1,1); for(int i=1,x,y,val;i<=q;i++){ scanf("%d%d%d",&x,&y,&val); range=max(range,val); HPD::modify(x,y,val); } ST.build(1,range,1); for(int i=1;i<=n;i++){ for(int id=0;id<tags[i].size();id++){ int num=tags[i][id]; if(num>0)ST.update(num,1,1); else ST.update(-num,-1,1); } ans[rk[i]]=ST.tree[1].val?ST.tree[1].res:0; } for(int i=1;i<=n;i++)printf("%d\n",ans[i]); return 0; } ``` ### **例题思路与代码** **[永无乡](https://www.luogu.com.cn/problem/P3224)** 给定岛屿排名求第 $k$ 小的数,套权值线段树板子即可,注意查询返回的是点编号不是点权值,额外维护一个 $tree_{id}$ 即可。 考虑合并操作,显然就是线段树合并。但是涉及到联通问题,所以还需要一个并查集,每次获得当前联通块所在根节点。记得合并的是联通块的首领节点,调过样例就切了。比蓝还好做。 ```cpp #include<bits/stdc++.h> #define MAXN 300005 using namespace std; int n,m,q; int rt[MAXN]; int father[MAXN]; int getf(int x){ if(father[x]==x)return father[x]; else return father[x]=getf(father[x]); } struct Segment_Tree{ #define ls(p) tree[p].lson #define rs(p) tree[p].rson #define push_up(p) tree[p].val=tree[ls(p)].val+tree[rs(p)].val int tot=1; struct TREE{ int lson,rson; int val,id; }tree[MAXN*20]; inline void update(int l,int r,int k,int &p,int idx){ if(!p)p=++tot; if(l==r){ ++tree[p].val; tree[p].id=idx; // printf("tree %d .idx=%d\n",p,idx); return; } int mid=l+r>>1; if(k<=mid)update(l,mid,k,ls(p),idx); else update(mid+1,r,k,rs(p),idx); push_up(p); } inline int query(int l,int r,int k,int p){ if(!p)return 0; if(l==r)return tree[p].id; int s1=tree[ls(p)].val,s2=tree[rs(p)].val; int mid=l+r>>1; if(k<=s1)return query(l,mid,k,ls(p)); else return query(mid+1,r,k-s1,rs(p)); } inline int merge(int x,int y){ if(!x)return y; if(!y)return x; ls(x)=merge(ls(x),ls(y)); rs(x)=merge(rs(x),rs(y)); push_up(x); return x; } }ST;//线段树合并板子,唯一的区别在查询函数返回值为点编号,最早更新的时候注意一下。 int main(){ scanf("%d%d",&n,&m); for(int i=1,val;i<=n;i++){ scanf("%d",&val); ST.update(1,n,val,rt[i],i);//更新权值。 } for(int i=1;i<=n;i++)father[i]=i; for(int i=1,u,v;i<=m;i++){ scanf("%d%d",&u,&v); int fu=getf(u),fv=getf(v); if(fu!=fv){//已经联通的节点就不要再合并了免得出问题。 father[fv]=fu; rt[fu]=ST.merge(rt[fu],rt[fv]); } } scanf("%d",&q); for(int i=1,x,y,k;i<=q;i++){ char opt; cin>>opt; if(opt=='B'){ scanf("%d%d",&x,&y); int fu=getf(x),fv=getf(y); if(fu!=fv){ father[fv]=fu; rt[fu]=ST.merge(rt[fu],rt[fv]);//线段树合并。 } } if(opt=='Q'){ scanf("%d%d",&x,&k); x=getf(x); int res=ST.query(1,n,k,rt[x]); printf("%d\n",res?res:-1);//无解输出-1注意即可。 } } return 0; } ``` **[魔法少女LJJ](https://h.hszxoj.com/d/hzoj/p/P1623)** 永无乡 plus。 啊这个是真的恶心。 1,2,5 操作就是永无乡,讨论 3,4,6,7 操作。 - 对于操作 3,4,可以按照题意硬搞:把所有权值小于或者大于等于 $x$ 的清零,统计个数 $val$,给节点 $x$ 加上 $val$。于是出现了懒标记问题,合并操作里是不能出现 push_up 状物的不然一直交一直 wa,我也不知道为什么。 - 关于懒标记:只需要标记是否清零就可以了。 - 对于操作 6,如果硬要维护乘积的话得高精,那我就不会了。不过他让比对大小,于是对乘积取对数,维护乘积变成了维护对数和。~~(这谁能想到啊)~~ - 对于操作 7,输出 $tree[root_a].val$。 操作 8,9 是不出现的,要不然我也不会做。 调起来细节很多,第一天晚上都没调出来,第二天重构了一遍就过了,懒得看之前错哪了。 ```cpp #include<bits/stdc++.h> #define MAXN 400005 using namespace std; int T,n; int rt[MAXN]; const int inf=1e9; int father[MAXN]; int getf(int x){ if(father[x]==x)return x; return father[x]=getf(father[x]); } struct Segment_Tree{ #define ls(p) tree[p].lson #define rs(p) tree[p].rson int tot; struct TREE{ int lson,rson; int tag,val; double gval; }tree[MAXN*20]; inline void push_up(int p){ tree[p].val=tree[ls(p)].val+tree[rs(p)].val; tree[p].gval=tree[ls(p)].gval+tree[rs(p)].gval; return; } inline void spread(int p){ if(tree[p].tag){ tree[ls(p)].val=tree[rs(p)].val=0; tree[ls(p)].gval=tree[rs(p)].gval=0; tree[ls(p)].tag=tree[rs(p)].tag=1; tree[p].tag=0; return; } } inline void update(int l,int r,int x,double logval,int k,int &p){ if(!p)p=++tot; if(l==r){ tree[p].val+=k; // printf("p=%d nowval:%d\n",p,tree[p].val); tree[p].gval+=(1.0*k*logval); tree[p].tag=0; return; } spread(p); // printf("nowp:%d\n",p); int mid=l+r>>1; if(x<=mid)update(l,mid,x,logval,k,ls(p)); else update(mid+1,r,x,logval,k,rs(p)); push_up(p); } inline int query(int l,int r,int ul,int ur,int p){ if(!p)return 0; if(l>=ul&&r<=ur)return tree[p].val; //printf("nowp:%d,ul,ur=%d %d \n",p,ul,ur); spread(p); int mid=l+r>>1; int res=0; if(ul<=mid)res+=query(l,mid,ul,ur,ls(p));//,printf("led to:%d %d node:%d\n",l,mid,ls(p)); if(ur>mid)res+=query(mid+1,r,ul,ur,rs(p));//,printf("res\n"); return res; } inline void del(int l,int r,int ul,int ur,int p){ if(!p)return; if(l>=ul&&r<=ur){ tree[p].gval=0; tree[p].val=0; tree[p].tag=1; return; } spread(p); int mid=l+r>>1; if(ul<=mid)del(l,mid,ul,ur,ls(p)); if(ur>mid)del(mid+1,r,ul,ur,rs(p)); push_up(p); } int kth(int l,int r,int k,int p){ if(l==r)return l; spread(p); int mid=l+r>>1; if(k<=tree[ls(p)].val)return kth(l,mid,k,ls(p)); else return kth(mid+1,r,k-tree[ls(p)].val,rs(p)); } inline int merge(int x,int y){ if(!y)return x; if(!x)return y; spread(x),spread(y); tree[x].val+=tree[y].val; tree[x].gval+=tree[y].gval; ls(x)=merge(ls(x),ls(y)); rs(x)=merge(rs(x),rs(y)); return x; } double getlog(int p){ return tree[p].gval; } int getsum(int p){ return tree[p].val; } }ST; int main(){ scanf("%d",&T); while(T--){ int opt,x,y,a,b,k; scanf("%d",&opt); if(opt==1){ scanf("%d",&x); ST.update(1,inf,x,log(x),1,rt[++n]); father[n]=n; //printf("nowdot:%d updatedval:%d\n",n,ST.query(1,inf,x,x,rt[n])); } if(opt==2){ scanf("%d%d",&x,&y); x=getf(x),y=getf(y); if(x!=y){ father[y]=x; rt[x]=ST.merge(rt[x],rt[y]); // printf("eff merged:%d & %d,newroot:%d\n",x,y,rt[x]); } } if(opt==3){ scanf("%d%d",&a,&x); a=getf(a); int val=ST.query(1,inf,1,x,rt[a]); ST.del(1,inf,1,x,rt[a]); ST.update(1,inf,x,log(x),val,rt[a]); } if(opt==4){ scanf("%d%d",&a,&x); a=getf(a); int val=ST.query(1,inf,x,inf,rt[a]); ST.del(1,inf,x,inf,rt[a]); ST.update(1,inf,x,log(x),val,rt[a]); } if(opt==5){ scanf("%d%d",&a,&k); a=getf(a); // printf("father=%d,root:%d\n",a,rt[a]); printf("%d\n",ST.kth(1,inf,k,rt[a])); } if(opt==6){ scanf("%d%d",&a,&b); a=getf(a),b=getf(b); printf("%d\n",ST.getlog(rt[a])>ST.getlog(rt[b])); } if(opt==7){ scanf("%d",&a); a=getf(a); printf("%d\n",ST.getsum(rt[a])); } } return 0; } ``` 一棵线段树合并中其实同时存在了好多线段树。 **[大融合](https://www.luogu.com.cn/problem/P4219)** 之前跟同学玩ygo白龙卡组必塞一张大融合。 但是这道题跟ygo没有关系,令人忍俊不禁。 单次查询 $(x,y)$ 输出的答案是断开改边后两点所属联通块大小之积,边是肯定不能真断的,考虑用子树大小硬凑。 子树大小难以在线更新,所以题解提出:先离线操作,建好最终的树(森林)后,用线段树合并与并查集充当连边操作。然对于查询操作,考虑对最终的树跑 $dfs$ 序,对边 $(u,v)$ 负载的运算有公式: $\forall dfn_u<dfn_v,ans=(ST.query(1,dfn[v]-1,rt_{x})+ST.query(dfn[v]+siz[v],n,rt_{x}))*(ST.query(dfn[v],dfn[v]+siz[v]-1,rt_x))

坑点:没事不要用父亲节点 u 表示公式,u 不止一个儿子因此其所属子树节点编号最高能到 dfn[v]-1,进而其子树大小 siz[u] \neq siz[v]+1

因为线段树还没有合并,所以 1\sim dfn[v] 等的遍历不会获得最终的节点个数。很有意思。

#include<bits/stdc++.h>
#define MAXN 100005
#define int long long
using namespace std;
int n,q;
vector<int>edge[MAXN];
struct node{
    int type;
    int x,y;
}wzw[MAXN];
int father[MAXN];
int getf(int x){
    if(father[x]==x)return x;
    else return father[x]=getf(father[x]);
}
int dfn[MAXN],siz[MAXN],tim,efa[MAXN];
void dfs(int u,int fa){
    dfn[u]=++tim;
    efa[u]=fa;
    siz[u]=1;
    for(int i=0;i<edge[u].size();i++){
        int v=edge[u][i];
        if(v==fa)continue;
        dfs(v,u);
        siz[u]+=siz[v];
    }   
}
int rt[MAXN];
struct Segment_Tree{
    #define ls(p) tree[p].lson
    #define rs(p) tree[p].rson
    #define push_up(p) tree[p].val=tree[ls(p)].val+tree[rs(p)].val
    int tot;
    struct TREE{
        int lson,rson;
        int val;
    }tree[MAXN*20];
    inline void update(int l,int r,int x,int &p){
        if(!p)p=++tot;
        if(l==r){
            ++tree[p].val;
            return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(l,mid,x,ls(p));
        else update(mid+1,r,x,rs(p));
        push_up(p);
    }
    inline int query(int l,int r,int ul,int ur,int p){
        if(!p)return 0;
        if(l>=ul&&r<=ur)return tree[p].val;
        int mid=l+r>>1;
        int res=0;
        if(ul<=mid)res+=query(l,mid,ul,ur,ls(p));
        if(ur>mid)res+=query(mid+1,r,ul,ur,rs(p));
        return res;
    }
    inline int merge(int x,int y){
        if(!x)return y;
        if(!y)return x;
        ls(x)=merge(ls(x),ls(y));
        rs(x)=merge(rs(x),rs(y));
        push_up(x);
        return x;
    }
}ST;
signed main(){
    scanf("%lld%lld",&n,&q);
    for(int i=1;i<=n;i++)father[i]=i;
    for(int i=1,u,v;i<=q;i++){
        char opt;
        cin>>opt;
        if(opt=='A'){
            scanf("%lld%lld",&u,&v);
            edge[u].push_back(v);
            edge[v].push_back(u);
            wzw[i].type=1;
            wzw[i].x=u,wzw[i].y=v;  
        }
        else{
            scanf("%lld%lld",&u,&v);
            wzw[i].x=u,wzw[i].y=v;
        }
    }
    for(int i=1;i<=n;i++)if(!dfn[i])dfs(i,0);
    for(int i=1;i<=n;i++)ST.update(1,n,dfn[i],rt[i]);
    for(int i=1,opt,u,v;i<=q;i++){
        u=wzw[i].x,v=wzw[i].y,opt=wzw[i].type;
        if(opt){
            u=getf(u),v=getf(v);
            father[v]=u;
            rt[u]=ST.merge(rt[u],rt[v]);
        }
        else{
            if(v==efa[u])swap(u,v);
            int fx=getf(u),fy=getf(v);
            int val1=ST.query(1,n,dfn[v],dfn[v]+siz[v]-1,rt[fx]);
            int val2=ST.query(1,n,1,dfn[v]-1,rt[fx])+ST.query(1,n,dfn[v]+siz[v],n,rt[fx]);
        //  printf("%lld %lld\n",val1,val2);
            printf("%lld\n",val1*val2);
        }
    }
    return 0;
}

实力足够后会添加:线段树合并 维护 dp