线段树合并

  线段树合并就是把两个维护相同区间的线段树合并到一块.

  什么叫维护相同区间?就是每次操作的最大的那个区间是一样的,根节点维护的区间一样,根节点的左儿子维护的也一样,右儿子维护的也一样,左儿子的左儿子....

  显然只需要建树的时候都建成一样的就好了.一般我们采用动态开点.

  由于线段树合并的均摊复杂度是log的,虽然我看不懂证明可以开成权值线段树.

  对于两个线段树该如何合并呢?我们递归的定义:

  1.对于线段树a,b.如果只有a有当前区间或只有b有当前区间,那么把那个认定为新线段树的节点.

  2.否则我们同时递归a的左儿子和b的左儿子,递归完后把刚认定的节点作为a的左儿子.

  3.递归a的右儿子和b的右儿子,递归完后把刚认定的节点作为a的右儿子.

  4.把a认定为新线段树的节点.

  如果有两个满的线段树,每次复杂度是O(n)还多的.但是如果刚开始有n个线段树,每个线段树用动态开点维护一个数,那么合并n-1次后均摊复杂度是nlogn的.

  校内oj并没有找到例题.我口述一个例题好了.

  给你一棵有n个点的树,树上每个节点都有一种颜色 ci,求以每个点为根的子树出现最多的颜色的和.

  这是一个最简单的线段树合并了.先对每个节点的每个颜色做一个权值线段树,从根dfs整颗树.dfs完儿子后,合并父亲和儿子的权值线段树,记录答案.没了.

  口胡还是开心的.我又写了一道题以供解析.

  https://www.luogu.org/problemnew/show/P3224.

  询问的是与x相连通的所有岛中第k重要的.假如没有修改操作,可以对每个连通块弄一个权值线段树,每次询问是logn对吧.现在修改操作相当于把两个连通块联通,我们想到可以写线段树合并.

  想到连通块就会想到并查集.为了解决 "如果xy原来就联通,合并线段树又错误了"这个问题,写一个并查集即可.

  

int n,m,tot;
int rt[100010],fa[100010];
struct tree
{
    int l,r,sum,ans;
}tr[2000010];
int get(int x)
{
    return fa[x]==x?x:fa[x]=get(fa[x]);
}
void New(int &now,int l,int r,int x,int id)
{
    if(!now) now=++tot;
    if(l==r)
    {
        tr[now].sum++;
        tr[now].ans=id;
        return ;
    }
    int mid=l+r>>1;
    if(x<=mid)
        New(tr[now].l,l,mid,x,id);
    else 
        New(tr[now].r,mid+1,r,x,id);
    tr[now].sum=tr[tr[now].l].sum+tr[tr[now].r].sum;
}
int merge(int a,int b,int l,int r)
{
    if(!a||!b) 
        return a+b;
    if(l==r)
    {
        tr[a].sum+=tr[b].sum;
        tr[a].ans=max(tr[a].ans,tr[b].ans);
        return a;
    }
    int mid=l+r>>1;
    tr[a].l=merge(tr[a].l,tr[b].l,l,mid);
    tr[a].r=merge(tr[a].r,tr[b].r,mid+1,r);
    tr[a].sum=tr[tr[a].l].sum+tr[tr[a].r].sum;
    return a;
}
int ask(int now,int l,int r,int x)
{
    if(x>tr[now].sum) return -1;
    if(l==r) 
        return tr[now].ans;
    int mid=l+r>>1;
    if(tr[tr[now].l].sum>=x)
        return ask(tr[now].l,l,mid,x);
    else return ask(tr[now].r,mid+1,r,x-tr[tr[now].l].sum);
}
int main()
{
    //freopen("123.in","r",stdin);
    n=read(),m=read();
    for(int i=1;i<=n;++i)
        fa[i]=i;
    for(int i=1;i<=n;++i) 
        New(rt[i],1,n,read(),i);
    for(;m;m--)
    {
        int x=get(read()),y=get(read());
        if(x==y) continue;
        else
        {
            merge(rt[x],rt[y],1,n);
            fa[y]=x;
        }
    }
    for(int Q=read();Q;Q--)
    {
        char ch;cin>>ch;
        if(ch=='Q')
        {
            int x=get(read());
            write(ask(rt[x],1,n,read()));
        }
        else
        {
            int x=get(read()),y=get(read());
            if(x==y) 
                continue;
            else
            {
                merge(rt[x],rt[y],1,n);
                fa[y]=x;
            }
        }
    }
    return 0;
}
洛谷p3224

  个人认为线段树合并好想好写.是一个线段树思想的拓展.很有意义吧.

  

posted @ 2019-03-08 06:45  zzuqy  阅读(256)  评论(0编辑  收藏  举报