zoukankan      html  css  js  c++  java
  • 图论学习:树上问题(LCA,树链剖分)

    图论:树上问题(LCA,树链剖分)

    一.LCA(Least Common Ancestors):最近公共祖先

    对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u和v的祖先且x的深度尽可能大。在这里,一个节点也可以是它自己的祖先
    ——百度百科

    求LCA的常用算法:
    1.离线算法:LCA(tarjan)
    2.在线算法:倍增LCA和ST表,树链剖分

    1. LCA(Tarjan):

    LCA的离线算法用的不多,这里不做多讲:
    具体看这篇博客,讲解很详细

    空间太大了。。

    点击查看折叠代码块
    /*
    LCA 离线Tarjan
    */
    #include <bits/stdc++.h>
    using namespace std;
    const int maxn=5e5+10;
    struct node{
        int v,next;
    }e[maxn];
    int head[maxn],cnt=0;
    int fa[maxn];
    
    int getfa(int x){
        return x==fa[x]?x:fa[x]=getfa(fa[x]);
    }
    void add(int u,int v){
        e[++cnt].v=v;
        e[cnt].next=head[u];
        head[u]=cnt;
    }
    int n,m,s;
    struct que{
        int id,note;
    }p[maxn];
    vector<que> vt[maxn];
    
    bool vis[maxn];
    int ans[maxn];
    
    void dfs(int u,int f){
        for (int i=head[u];i;i=e[i].next){//一直往下遍历
            int v=e[i].v;
            if(v!=f){
                dfs(v,u);
                fa[v]=u;
            }
        }
        for (int i=0;i<vt[u].size();i++){
            if(vis[vt[u][i].note]){
                ans[vt[u][i].id]=getfa(vt[u][i].note);
            }
        }
        vis[u]=1;
    }
    int main(){
        scanf("%d%d%d",&n,&m,&s);
        for (int i=1;i<=n;i++) fa[i]=i;
    
        for (int i=1;i<=n-1;i++){
            int u,v;
            scanf("%d %d",&u,&v);
            add(u,v);
            add(v,u);
        }
        for (int i=1;i<=m;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            vt[u].push_back((que){i,v});
            vt[v].push_back((que){i,u});
        }
        dfs(s,0);
        for (int i=1;i<=m;i++) {
            printf("%d
    ",ans[i]);
        }
        return 0;
    }
    

    2.倍增LCA

    倍增算法:
    假设有4个数a[1]-a[4],现在我要从a[1]找到a[4],一种普遍方法是a[1]--a[2]--a[3]--a[4]这样子,但这样的速度太慢,我们考虑如下方法:
    设f[i][j]表示a[i]的后第 2j 次方的数的下标值,则有:
    f[1][0]=2,f[2][0]=3,f[3][0]=4
    f[1][1]=3,f[2][1]=4

    这样我们便可以找f[1][1]和f[3][0]便可以从a[1]到a[4]了

    f满足这样一个性质:

    [f[i][j]=f[f[i][j-1]][j-1] ]

    这便是倍增的思想,那么如何用倍增求得LCA呢?
    我们需要有几个步骤:

    设树上两个节点为x和y
    1.处理出每个节点从根节点开始到自身的深度dep[i](用dfs遍历的方式即可)
    2.我们假设节点的深度关系为dep[x]>dep[y],则首先需要把x向上移动到和y同一深度的位置上(用倍增的方法移动)
    3.若此时x=y,则表示x(或者y)是LCA
    4.x!=y,则用倍增的方法同时移动x和y,x=f[x][j],y=f[y][j] (j=2n, 2n-1, 2n-2 ...20),一直移动直到f[x][j]=f[y][j],停止不动,继续看下一个j
    5.最后LCA则为f[x][0]

    点击查看折叠代码块
    /*
    LCA(最近公共祖先)模板
    
    倍增LCA
    */
    #include <bits/stdc++.h>
    using namespace std;
    
    const int maxn=5e5+10;
    int fa[maxn][30];
    int n,m,s;
    int head[maxn],cnt=0;
    int dep[maxn];
    
    struct node{
        int v,next;
    }e[maxn<<1];
    
    void add(int u,int v){
        e[++cnt].v=v;
        e[cnt].next=head[u];
        head[u]=cnt;
    }
    
    void dfs(int u,int f){
        dep[u]=dep[f]+1;
        fa[u][0]=f;
        for (int i=1;(1<<i)<=dep[u];i++){
            fa[u][i]=fa[fa[u][i-1]][i-1];
        }
        for (int i=head[u];i;i=e[i].next){
            int v=e[i].v;
            if(v!=f) dfs(v,u);
        }
    }
    
    int lca(int a,int b){
        if(dep[a]<dep[b]) swap(a,b);
        for (int i=25;i>=0;i--){
            int d=dep[a]-dep[b];
            if(d>=(1<<i)) a=fa[a][i];
        }
        if(a==b) return a;
        for (int i=25;i>=0;i--){
            if(fa[a][i]!=fa[b][i]){
                a=fa[a][i];
                b=fa[b][i];
            }
        }
        return fa[a][0];
    }
    
    int main(){
        scanf("%d%d%d",&n,&m,&s);
        for (int i=1;i<=n-1;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            add(u,v);
            add(v,u);
        }
        dfs(s,0);
        for (int i=1;i<=m;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            int ans=lca(u,v);
            printf("%d
    ",ans);
        }
        return 0;
    }
    

    PS:如果要求树上任意两点距离则为d[x]+d[y]-2*d[lca(x,y)]

    二.树链剖分

    既然树上倍增又好打又好调,干嘛还用树链剖分?这是因为树上倍增应用没有树链剖分广,比如:
    求:

    在一棵树上进行路径的权值修改,询问路径权值和、路径权值极值

    后两个操作可以做,但是进行路径权值修改就无法用树上倍增做。

    于是,就需要用树链剖分:

    指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、BST、SPLAY、线段树等)来维护每一条链。
    ——百度百科

    并且树剖的用处比倍增多,倍增能做的题树剖一定能做,反过来则否(非学不可了qwq

    树链剖分分为多种,这里主要讲轻重链剖分:

    概念什么的参考这个:
    https://blog.csdn.net/enjoy_pascal/article/details/78277008

    主要讲怎么实现:
    1.预处理:
    两次dfs
    第一次dfs——处理出每个点的重儿子son[],子树大小size[],深度d[]及父节点f[],具体实现很简单,回溯时直接比较当前子节点和重儿子子树大小关系来更新重儿子
    第二次dfs——处理出每个节点所在的重链的链头

    2.维护:线段树,树状数组,Splay等等。。

    3.在线处理

    应用:

    1.求LCA:

    1.若x所在的重链的链头不等于y所在的重链的链头,表明x和y不在同一条重链上
    2.记x和y两点中链头的深度较深的那个点为z
    3.将z调到他的链头的father
    4.重复以上步骤,知道x与y的链头相等(即在同一条重链上)
    5.此时x和y中深度较小的那个点即为LCA

    LCA不需要求权值,直接预处理完不用维护了

    点击查看折叠代码块
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    using namespace std;
    const int maxn=5e5+10;
    int n,m,s;
    
    int head[maxn],cnt=0;
    int d[maxn],size[maxn],son[maxn],fa[maxn];//每个节点的深度,子树的大小,重儿子,父节点
    int top[maxn];//每个点所在的重链的链头
    
    struct node{
        int v,next;
    }e[maxn<<1];
    
    void add(int u,int v){
        e[++cnt].v=v;
        e[cnt].next=head[u];
        head[u]=cnt;
    }
    
    void dfs1(int x,int f,int deep){
        size[x]++;
        fa[x]=f;
        d[x]=deep;
        int maxson=-1;
        for (int i=head[x];i;i=e[i].next){
            int v=e[i].v;
            if(v!=f){
                fa[v]=x;
                dfs1(v,x,deep+1);
                size[x]+=size[v];
                if(size[v]>maxson){
                    son[x]=v;
                    maxson=size[v];
                }
            }
        }
    }
    
    void dfs2(int x,int tp){
        top[x]=tp;
        if(son[x]) dfs2(son[x],tp);
        for (int i=head[x];i;i=e[i].next){
            int v=e[i].v;
            if(v!=fa[x] && v!=son[x]){
                dfs2(v,v);
            }
        }
    }
    
    int lca(int x,int y){
        while(top[x]!=top[y]){
            if(d[top[x]]<d[top[y]]) swap(x,y);//保证x是所在链的链头深度较大的那个
            x=fa[top[x]];
        }
        return d[x]<d[y]?x:y;
    }
    
    int read(){
        int s=0;
        char c=getchar();
        while(c<'0' || c>'9') c=getchar();
        while(c>='0' && c<='9'){
            s=s*10+c-'0';
            c=getchar();
        }
        return s;
    }
    
    int main(){
        scanf("%d%d%d",&n,&m,&s);
        for (int i=1;i<=n-1;i++){
            int u,v;
            u=read();v=read();
            add(u,v);
            add(v,u);
        }
        dfs1(s,0,1);
        dfs2(s,s);
    
        for (int i=1;i<=m;i++){
            int u,v;
            u=read();v=read();
            int ans=lca(u,v);
            printf("%d
    ",ans);
        }
        return 0;
    }
    

    2.求树上路径信息。维护树上的区间信息,求区间信息,维护某一个子树信息,求子树信息

    例题:
    原题链接
    已知一棵包含 NN 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

    操作 1: 格式: 1 x y z 表示将树从 x到 y结点最短路径上所有节点的值都加上 z。
    操作 2: 格式: 2 x y 表示求树从 x到 y 结点最短路径上所有节点的值之和。
    操作 3: 格式: 3 x z 表示将以 x 为根节点的子树内所有节点值都加上 z。
    操作 4: 格式: 4 x 表示求以 x 为根节点的子树内所有节点值之和。

    先进行预处理
    我们先用两次dfs处理出每个节点的各种信息,还要加上一个点的id值(表示第二次dfs的时候是第几个遍历到的,比如说如果2号点是第一个遍历到的则id[2]=1)

    处理完信息后,我们就把一个树分成了若干链,可以发现,在同一条链上的点的id值是连续的。

    具体过程如图:
    在这里插入图片描述
    标号:
    在这里插入图片描述
    经过重链剖分后可以变为若干条链:(蓝色的表示终边,红色的点表示重儿子)
    在这里插入图片描述

    分为若干条链:(1-5-6-7)(8)(9)(2-3)(4)
    在这里插入图片描述

    重新标号

    在这里插入图片描述

    于是各点的顺序变为:
    id[1]=1,id[5]=2,id[6]=3,id[7]=4,id[8]=5
    id[9]=6,id[2]=7,id[3]=8,id[4]=9

    然后便可用数据结构(线段树,splay,树状数组等)维护了

    这里采用线段树维护
    则原线段树的9个叶子结点按照顺序分别存放1,5,6,7,8,9,2,3,4
    相邻的点是在一个连续的区间,比如1,5,6,7四个点

    具体如何实现四个操作看代码:

    点击查看折叠代码块
    #include <bits/stdc++.h>
    using namespace std;
    const int maxn=1e5+10;
    
    struct node{
        int v,next;
    }e[maxn<<1];
    int head[maxn],cnt=0,ct=0;
    int size[maxn];//子树的节点大小
    int son[maxn];//重儿子
    int top[maxn];//链头
    int fa[maxn];//父亲节点
    int d[maxn];//深度
    int id[maxn];//节点的新标号
    
    int sum[maxn<<2],lazy[maxn<<2],a[maxn],w[maxn];
    int mode,n,m,s;
    
    void add(int u,int v){
        e[++cnt].v=v;
        e[cnt].next=head[u];
        head[u]=cnt;
    }
    
    void dfs1(int x,int f,int deep){
        size[x]=1;
        d[x]=deep;
        fa[x]=f;
        int maxson=-1;
        for (int i=head[x];i;i=e[i].next){
            int v=e[i].v;
            if(v!=f){
                fa[v]=x;
                dfs1(v,x,deep+1);
    			size[x]+=size[v];
                if(size[v]>maxson){
                    son[x]=v;
                    maxson=size[v];
                }
            }
        }
    }
    
    void dfs2(int x,int tp){
        id[x]=++ct;
        w[ct]=a[x];//将原节点的信息复制 
        top[x]=tp;
        if(son[x]) dfs2(son[x],tp);
        for (int i=head[x];i;i=e[i].next){
            int v=e[i].v;
            if(v!=fa[x] && v!=son[x]){
                dfs2(v,v);
            }
        }
    }
    
    void pushdown(int l,int r,int rt){
        if(lazy[rt]){
            int m=(l+r)>>1;
            int d=lazy[rt];
            lazy[rt<<1]+=d;
            lazy[rt<<1|1]+=d;
            sum[rt<<1]=(sum[rt<<1]+(m-l+1)*d) % mode;
            sum[rt<<1|1]=(sum[rt<<1|1]+(r-m)*d) % mode;
            lazy[rt]=0;
        }
    }
    
    void pushup(int rt){
        sum[rt]=(sum[rt<<1]+sum[rt<<1|1]) % mode;
    }
    
    void build(int l,int r,int rt){
        if(l==r){
            sum[rt]=w[l];
            if(sum[rt]>mode) sum[rt] %=mode;
            return ;
        }
        int m=(l+r)>>1;
        build(l,m,rt<<1);
        build(m+1,r,rt<<1|1);
        pushup(rt);
    }
    
    void update(int L,int R,int v,int l,int r,int rt){
        if(L<=l && r<=R){
            lazy[rt]=(lazy[rt]+v) % mode;
            sum[rt]=(sum[rt]+(r-l+1)*v) % mode;
            return ;
        }
        pushdown(l,r,rt);
        int m=(l+r)>>1;
        if(L<=m) update(L,R,v,l,m,rt<<1);
        if(R>m) update(L,R,v,m+1,r,rt<<1|1);
        pushup(rt);
    }
    
    int query(int L,int R,int l,int r,int rt){
        if(L<=l && r<=R){
            return sum[rt] % mode;
        }
        pushdown(l,r,rt);
        int m=(l+r)>>1;
        int ans=0;
        if(L<=m) ans=(ans+query(L,R,l,m,rt<<1)) % mode;
        if(R>m) ans=(ans+query(L,R,m+1,r,rt<<1|1)) % mode;
        return ans;
    }
    /*线段树单点查询这里用不到
    int querypos(int pos,int l,int r,int rt){
    	if(l==r){
    		return sum[rt];
    	}
    	pushdown(l,r,rt);
    	int m=(l+r)>>1;
    	if(pos<=m) querypos(pos,l,m,rt<<1);
    	else querypos(pos,m+1,r,rt<<1|1);
    }
    */
    
    int Qdis(int x,int y){//操作2
        int ans=0;
        while(top[x]!=top[y]){//当两个点不在同一条链上
            if(d[top[x]]<d[top[y]]) swap(x,y);//把x点改为所在链顶端的深度更深的那个点
            ans+=query(id[top[x]],id[x],1,n,1);
            ans%=mode;
            x=fa[top[x]];//把x跳到x所在链顶端的那个点的上面一个点
        }
        //直到两个点处于一条链上
        if(d[x]>d[y]) swap(x,y);//把x点改为深度较小的那个点
        ans+=query(id[x],id[y],1,n,1);//这时再加上此时两个点的区间和即可
        return ans%=mode;
    }
    
    void Updis(int x,int y,int k){//操作1
        k%=mode;
        while(top[x]!=top[y]){
            if(d[top[x]]<d[top[y]]) swap(x,y);
            update(id[top[x]],id[x],k,1,n,1);
            x=fa[top[x]];
        }
        if(d[x]>d[y]) swap(x,y);
        update(id[x],id[y],k,1,n,1);
    }
    
    int Qnode(int x){//操作4
        return query(id[x],id[x]+size[x]-1,1,n,1) % mode;//子树区间右端点为id[x]+size[x]-1 
    }
    
    void Upnode(int x,int k){//操作3
        update(id[x],id[x]+size[x]-1,k,1,n,1);
    }
    
    int main(){
        scanf("%d%d%d%d",&n,&m,&s,&mode);
        for (int i=1;i<=n;i++){
            scanf("%d",&a[i]);
        }
        
        for (int i=1;i<=n-1;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            add(u,v);add(v,u);
        }
        dfs1(s,0,1);dfs2(s,s);build(1,n,1);//预处理
        //询问
        
        for (int i=1;i<=m;i++){
            int op;
            scanf("%d",&op);
            if(op==1){
                int x,y,z;
                scanf("%d%d%d",&x,&y,&z);
                Updis(x,y,z);
            }
            else if(op==2){
                int x,y;
                scanf("%d%d",&x,&y);
                int ans=Qdis(x,y);
                printf("%d
    ",ans);
            }
            else if(op==3){
                int x,z;
                scanf("%d%d",&x,&z);
                Upnode(x,z);
            }
            else if(op==4){
                int x;
                scanf("%d",&x);
                int ans=Qnode(x);
                printf("%d
    ",ans);
            }
        }
        return 0;
    }
    
    
    你将不再是道具,而是成为人如其名的人
  • 相关阅读:
    自学Java0711
    自学Java0710
    自学Java0709
    自学Java0708
    Leetcode刷题集
    网站收集
    674. 最长连续递增序列『简单』
    680. 验证回文字符串 Ⅱ『简单』
    686. 重复叠加字符串匹配『简单』
    693. 交替位二进制数『简单』
  • 原文地址:https://www.cnblogs.com/wsl-lld/p/13393650.html
Copyright © 2011-2022 走看看