线段树合并

线段树合并

线段树合并可以使很多跑不过的暴力,特别是树上暴力的时间复杂度正确,与树分治的区别在于,线段树合并必须依次处理节点,但优势在于,保持了树的形态。

算法思路

引入

CF600E Lomsat gelral

使用一个数组记录该子树内的颜色出现次数。

每次每个节点暴力将儿子的信息合并到自己的数组上,因而求出答案。

显然这样的操作,空间和时间都不允许,我们需要效率更高的方式。

过程

线段树合并会将两颗形态相同的线段树的对应信息合并为一棵线段树。

我们考虑先每个节点开一棵动态开点的线段树,存下颜色出现次数,记录颜色区间的最大值和最大颜色编号和。

然后进行线段树合并,这个过程本质上非常暴力。

设两颗线段树为 A 树和 B 树。

从 1 号节点开始,遍历 A 树和 B 树中线段树的对应节点。

递归到某个节点时,如果 A 树中该节点为空,或者 B 树中该节点为空,直接返回另一棵树上的节点。

递归到线段树叶子节点时,区间大小为 1,看做两个元素合并值。

根据儿子节点求出当前节点状态。

void merge(int &p1,int p2,int l,int r)
{
    if(!p1||!p2) {p1=p1+p2;return ;}
    if(l==r) {/*do something*/;return ;}
    int mid=(l+r)>>1;
    push_down(p1);
    push_down(p2);//下传懒标记
    merge(tree[p1].lch,tree[p2].lch,l,mid);
    merge(tree[p1].rch,tree[p2].rch,mid+1,r);
    updata(p1);//根据儿子从新求值
}

这里 \(p1\) 直接引用回传了更改后节点对应的编号。

回到引入的题目,该题的合并直接将同一颜色对应节点的值求和即可。

时间复杂度

在树上的合并,时间复杂度在 \(O(n\log n)\),因为每一次合并的花费相当于 \(O(重复节点个数)\),然而每一条路径只有可能是原树中一个节点更新的,所以每条路径合并一次就相当于将原树中两个节点变为一个。

其他情况下,可以使用类似的方式分析时间复杂度,主要从一条路径合并在原情况中的变化展开讨论。

引入代码

#include<bits/stdc++.h>
using namespace std;

#define int long long
#define lch(p) tree[p].lch
#define rch(p) tree[p].rch

const int maxn=1e5+5;

struct linetree
{
    int tot;
    struct treenode{int lch,rch,sum,res;}tree[maxn*30];
    void updata(int p)
    {
        if(tree[lch(p)].sum>tree[rch(p)].sum) tree[p].sum=tree[lch(p)].sum,tree[p].res=tree[lch(p)].res;
        else if(tree[lch(p)].sum<tree[rch(p)].sum) tree[p].sum=tree[rch(p)].sum,tree[p].res=tree[rch(p)].res;
        else tree[p].sum=tree[lch(p)].sum,tree[p].res=tree[lch(p)].res+tree[rch(p)].res;
    }
    void insert(int &p,int l,int r,int co,int val)
    {
        if(l>co||r<co) return ;
        if(!p) p=++tot;
        if(l==r)
        {
            tree[p].sum+=val;
            tree[p].res=co;
            return ;
        }
        int mid=(l+r)>>1;
        insert(lch(p),l,mid,co,val);
        insert(rch(p),mid+1,r,co,val);
        updata(p);
    }
    void merge(int &p1,int p2,int l,int r)
    {
        if(!p1||!p2) {p1=p1+p2;return ;}
        if(l==r) {tree[p1].sum+=tree[p2].sum;return ;}
        int mid=(l+r)>>1;
        merge(lch(p1),lch(p2),l,mid);
        merge(rch(p1),rch(p2),mid+1,r);
        updata(p1);
    }
}T;
struct Edge
{
    int tot;
    int head[maxn];
    struct edgenode{int to,nxt;}edge[maxn*2];
    void add(int x,int y)
    {
        tot++;
        edge[tot].to=y;
        edge[tot].nxt=head[x];
        head[x]=tot;
    }
}E;

int n;
int c[maxn],rt[maxn],ans[maxn];

void dfs(int u,int f)
{
    for(int i=E.head[u];i;i=E.edge[i].nxt)
    {
        int v=E.edge[i].to;
        if(v==f) continue;
        dfs(v,u);
        T.merge(rt[u],rt[v],1,n);
    }
    ans[u]=T.tree[rt[u]].res;
}

signed main()
{
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
    {
        int x;
        scanf("%lld",&x);
        T.insert(rt[i],1,n,x,1);
    }
    for(int i=1;i<n;i++)
    {
        int u,v;
        scanf("%lld%lld",&u,&v);
        E.add(u,v);
        E.add(v,u);
    }

    dfs(1,0);
    for(int i=1;i<=n;i++) printf("%lld ",ans[i]);
}

例题

例一 P4556 Vani有约会 雨天的尾巴 /【模板】线段树合并

使用树上差分,将一段路径的加操作看做四个单点修改,线段树维护每个点的救济粮种类个数,儿子向父亲合并自己的线段树即可。

#include<bits/stdc++.h>
using namespace std;

#define inf 2e5

const int maxn=5e5+5;

struct linetree
{
    int tot;
    struct treenode{int sum,res,lch,rch;}tree[maxn*20];
    void updata(int p)
    {
        int lch=tree[p].lch,rch=tree[p].rch;
        if(tree[lch].sum<tree[rch].sum) tree[p].res=tree[rch].res,tree[p].sum=tree[rch].sum;
        else tree[p].res=tree[lch].res,tree[p].sum=tree[lch].sum;
    }
    void insert(int &p,int l,int r,int co,int val)
    {
        if(r<co||l>co) return ;
        if(!p) p=++tot;
        if(l==r)
        {
            tree[p].sum+=val;
            tree[p].res=co;
            return ;
        }
        int mid=(l+r)>>1;
        insert(tree[p].lch,l,mid,co,val);
        insert(tree[p].rch,mid+1,r,co,val);
        updata(p);
    }
    void merge(int &p1,int p2,int l,int r)
    {
        if(!p1||!p2) {p1=p1+p2;return ;}
        if(l==r) {tree[p1].sum+=tree[p2].sum;return ;}
        int mid=(l+r)>>1;
        merge(tree[p1].lch,tree[p2].lch,l,mid);
        merge(tree[p1].rch,tree[p2].rch,mid+1,r);
        updata(p1);
    }
}T;
struct Edge
{
    int tot;
    int head[maxn];
    struct edgenode{int to,nxt;}edge[maxn*2];
    void add(int x,int y)
    {
        tot++;
        edge[tot].to=y;
        edge[tot].nxt=head[x];
        head[x]=tot;
    }
}E;

int n,m;
int f[maxn][25],deep[maxn],rt[maxn],ans[maxn];

void dfs(int u)
{
    for(int i=E.head[u];i;i=E.edge[i].nxt)
    {
        int v=E.edge[i].to;
        if(deep[v]) continue;
        deep[v]=deep[u]+1;
        f[v][0]=u;
        for(int j=1;j<=20;j++) f[v][j]=f[f[v][j-1]][j-1];
        dfs(v);
    }
}
int Lca(int x,int y)
{
    if(deep[x]<deep[y]) swap(x,y);
    for(int i=20;i>=0;i--) if(deep[f[x][i]]>=deep[y]) x=f[x][i];
    if(x==y) return x;
    for(int i=20;i>=0;i--) if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
    return f[x][0];
}
void gtans(int u)
{
    for(int i=E.head[u];i;i=E.edge[i].nxt)
    {
        int v=E.edge[i].to;
        if(v==f[u][0]) continue;
        gtans(v);
        T.merge(rt[u],rt[v],1,inf);
    }
    ans[u]=T.tree[rt[u]].res;
    if(T.tree[rt[u]].sum==0) ans[u]=0;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<n;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        E.add(u,v);
        E.add(v,u);
    }

    deep[1]=1;
    dfs(1);

    for(int i=1;i<=m;i++)
    {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        T.insert(rt[x],1,inf,z,1);
        T.insert(rt[y],1,inf,z,1);
        int lca=Lca(x,y);
        T.insert(rt[lca],1,inf,z,-1);
        if(f[lca][0]) T.insert(rt[f[lca][0]],1,inf,z,-1);
    }
    gtans(1);
    for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
}

例二 P3224 HNOI2012 永无乡

使用并查集,每个并查集的根处建立一棵权值线段树,区间表示排名在这个区间内的点的个数,每次加边使用并查集判断,执行线段树合并,查询直接查。

#include<bits/stdc++.h>
using namespace std;

#define lch(p) tree[p].lch
#define rch(p) tree[p].rch

const int maxn=2e5+5;

int p[maxn],fp[maxn];

struct linetree
{
    int tot;
    struct treenode{int lch,rch,sum;}tree[maxn*30];
    void updata(int p)
    {
        tree[p].sum=tree[lch(p)].sum+tree[rch(p)].sum;
    }
    void insert(int &p,int l,int r,int x,int val)
    {
        if(l>x||r<x) return ;
        if(!p) p=++tot;
        if(l==r)
        {
            tree[p].sum+=val;
            return ;
        }
        int mid=(l+r)>>1;
        insert(lch(p),l,mid,x,val);
        insert(rch(p),mid+1,r,x,val);
        updata(p);
    }
    int fd(int p,int l,int r,int k)
    {
        int mid=(l+r)>>1;
        if(tree[p].sum<k) return -1;
        if(l==r) return fp[l];
        if(tree[lch(p)].sum>=k) return fd(lch(p),l,mid,k);
        return fd(rch(p),mid+1,r,k-tree[lch(p)].sum);
    }
    void merge(int &p1,int p2,int l,int r)
    {
        int mid=(l+r)>>1;
        if(!p1||!p2){p1=p1+p2;return;}
        if(l==r){tree[p1].sum+=tree[p2].sum;return ;}
        merge(lch(p1),lch(p2),l,mid);
        merge(rch(p1),rch(p2),mid+1,r);
        updata(p1);
    }
}T;

int n,m;
int rt[maxn],f[maxn];

int fr(int u){return f[u]==u?u:f[u]=fr(f[u]);}
void link(int u,int v)
{
    int fu=fr(u),fv=fr(v);
    if(fu==fv) return ;
    T.merge(rt[fu],rt[fv],1,n);
    f[fv]=fu;
}
void query(int u,int k)
{
    int fu=fr(u);
    printf("%d\n",T.fd(rt[fu],1,n,k));
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) f[i]=i;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&p[i]);
        fp[p[i]]=i;
        T.insert(rt[i],1,n,p[i],1);
    }
    for(int i=1;i<=m;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        link(u,v);
    }
    int q;
    scanf("%d",&q);
    while(q--)
    {
        char op;
        int x,y;
        cin>>op;
        scanf("%d%d",&x,&y);
        if(op=='B') link(x,y);
        else query(x,y);
    }
}

例三 P7563 JOISC 2021 Day (4Worst Reporter 4)

很好的线段树合并+树上 dp 的题目。

做出这道题你会对线段树合并有更多启发。

P7563 JOISC 2021 Day4 最悪の記者 4 (Worst Reporter 4) 题解

推荐练习

P3605 USACO17JAN Promotion Counting P

P8844 传智杯 #4 初赛 小卡与落叶

P8959 「CGOI-3」灵气

posted @ 2024-01-24 22:32  彬彬冰激凌  阅读(23)  评论(0编辑  收藏  举报