• [CF 191C]Fools and Roads[LCA Tarjan算法][LCA 与 RMQ问题的转化][LCA ST算法]


    参考:

    1. 郭华阳 - 算法合集之《RMQ与LCA问题》. 讲得很清楚!

    2. http://www.cnblogs.com/lazycal/archive/2012/08/11/2633486.html

    3. 代码来源yejinru


    题意:

    有一棵树, 按照顺序给出每条边, 再给出若干对点, 这两点之间的唯一的路( Simple path )上边权加1. 当所有对点处理完后, 按照边的输入顺序输出每条边的权.

    思路:

    LCA问题.

    最近公共祖先(Least Common Ancestors)LCA简介:对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。另一种理解方式是把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上深度最小的点。——百度百科

    问题等效于: 若点为( x, y ), 且有 LCA( x, y ) = z. 则 x, z   y, z 之间的边权均+1. 区间+1可以用差分数列的思想, 令dis[ x ] 为 x 点与其子节点之间的差值. x 点对应连接 x 和其父节点的边.


    解法一:

    Tarjan算法,离线操作.

    建树的方法除了用vector以外, 还可以换为链式前向星. 不过还是vector顺手些~

    p.s. 还是 [ 理论: 国家集训队论文 / ppt + 实践: 学长的模板 ] 这样的tempo比较靠谱~

    /*
    
    在树的任意两点所在的所有树的边权加一,输出最后所有的边权	
    
    LCA。我们可以离线操作,把所有的询问直接求出LCA,然后用数组
    dis[x] ++ ,   dis[y]++,
    dis[LCA(x,y)]  -=  2
    最后DFS统计一下答案
    
    */
    #include <set>
    #include <map>
    #include <list>
    #include <cmath>
    #include <queue>
    #include <stack>
    #include <string>
    #include <vector>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    typedef long long ll;
    typedef unsigned long long ull;
    
    #define debug puts("here")
    #define rep(i,n) for(int i=0;i<n;i++)
    #define rep1(i,n) for(int i=1;i<=n;i++)
    #define REP(i,a,b) for(int i=a;i<=b;i++)
    #define foreach(i,vec) for(unsigned i=0;i<vec.size();i++)
    #define pb push_back
    #define RD(n) scanf("%d",&n)
    #define RD2(x,y) scanf("%d%d",&x,&y)
    #define RD3(x,y,z) scanf("%d%d%d",&x,&y,&z)
    #define RD4(x,y,z,w) scanf("%d%d%d%d",&x,&y,&z,&w)
    #define All(vec) vec.begin(),vec.end()
    #define MP make_pair
    #define PII pair<int,int>
    #define PQ priority_queue
    #define cmax(x,y) x = max(x,y)
    #define cmin(x,y) x = min(x,y)
    #define fir first
    #define sec second
    
    /******** program ********************/
    
    const int MAXN = 2e5+5;
    
    vector< PII > edge[MAXN];
    vector< PII > ask[MAXN];
    int fa[MAXN];
    bool use[MAXN];
    int pa[MAXN];
    int n,m;
    int dis[MAXN];
    int a[MAXN],b[MAXN];
    int ans[MAXN];
    
    int find_set(int x){///并查集找父节点路径压缩
        if(fa[x]!=x)
            fa[x] = find_set(fa[x]);
        return fa[x];
    }
    
    /*********Tarjan算法的核心思想*********/
    void lca(int x,int f){
        //cout<<"x = "<<x<<endl;
        use[x] = true;///已经遍历
        fa[x] = x;///该节点插入并查集,并且自己为一个独立的集合
        foreach(i,ask[x]){///遍历有关节点x的所有询问
            int y = ask[x][i].first;///目标节点
            int id = ask[x][i].second;///输入的顺序
            //cout<<"dsadsa = "<<x<<" "<<y<<" "<<id<<endl;
            if(use[y])
                pa[id] = find_set(y);///如果已经遍历,那么答案就是它的父节点
        }
    
        foreach(i,edge[x]){///遍历所有儿子
            int y = edge[x][i].first;
            //cout<<"y = "<<y<<endl;
            if(use[y])continue;
            lca(y,x);///如果当前正在访问该子树的根,那么此前已访问过的节点的父节点必然是
            ///从根节点到该节点的唯一路上(*).这本身也是一个dfs
            fa[y] = x;///如果已经回溯,即完成了上一条语句对y节点子树的遍历,
            ///退出之后将y节点的父节点设为x,也就保证了这一步退出之后(*)条件仍然成立.
        }
    }
    
    void dfs(int x,int f){
        foreach(i,edge[x]){
            int y = edge[x][i].first;
            if(y==f)continue;
            dfs(y,x);
            int id = edge[x][i].second;
            dis[x] += dis[y];///类似于差分数列的思想
            ans[id] = dis[y];///从叶子节点向根统计结果
        }
    }
    
    int main(){
    
    //#ifndef ONLINE_JUDGE
    //	freopen("sum.in","r",stdin);
    	//freopen("sum.out","w",stdout);
    //#endif
    
        while(cin>>n){
            int x,y;
            rep(i,MAXN){//清零
                edge[i].clear();
                ask[i].clear();
            }
    
            memset(use,false,sizeof(use));//清零初始化
            REP(i,2,n){
                RD2(x,y);
                edge[x].pb( MP(y,i) );
                edge[y].pb( MP(x,i) );
            }
            RD(m);
            rep1(i,m){
                RD2(x,y);
                a[i] = x;//第i条路的两端点
                b[i] = y;
                ask[x].pb( MP(y,i) );//注意ask也是双向插入的
                ask[y].pb( MP(x,i) );
            }
            lca(1,0);
    
            memset(dis,0,sizeof(dis));
            rep1(i,m){
                x = a[i];
                y = b[i];
                dis[x] ++;///类似于差分数列的思想
                dis[y] ++;
                dis[ pa[i] ] -= 2;///保证增量向上至多只影响到lca),lca以上的路不受下面子树影响
                //cout<<x<<" "<<y<<" "<<pa[i]<<endl;
            }
    
            dfs(1,0);
    
            REP(i,2,n)
                printf("%d ",ans[i]);
            puts("");
        }
    
    	return 0;
    }
    

    自己敲一遍:

    #include <cstdio>
    #include <vector>
    #include <cstring>
    #include <utility>
    using namespace std;
    const int MAXN = 2e5+5;
    int n,m,x,y;
    vector<pair<int, int> > query[MAXN],edge[MAXN<<1];
    int a[MAXN],b[MAXN],dif[MAXN],ans[MAXN],lca[MAXN],fa[MAXN];
    bool vis[MAXN];
    
    int find_set(int x)
    {
        if(fa[x]!=x)
            fa[x] = find_set(fa[x]);
        return fa[x];
    }
    
    void LCA_Tarjan(int u)
    {
        vis[u] = true;
        fa[u] = u;
        for(size_t i=0;i<query[u].size();i++)
        {
            int v = query[u][i].first;
            int id = query[u][i].second;
            if(vis[v])
                lca[id] = find_set(v);///注意这里的id是询问的id,恰好对应一对节点
        }
        for(size_t i=0;i<edge[u].size();i++)
        {
            int v = edge[u][i].first;
            if(vis[v])  continue;
            LCA_Tarjan(v);
            fa[v] = u;
        }
    }
    
    void solve()
    {
        for(int i=0;i<m;i++)
        {
            x = a[i];
            y = b[i];
            dif[x]++; dif[y]++;///差分数列
            dif[lca[i]] -= 2;
        }
    }
    
    void dfs(int u, int f)
    {
        for(size_t i=0;i<edge[u].size();i++)
        {
            int v = edge[u][i].first;
            if(v==f)    continue;
            dfs(v, u);
            int id = edge[u][i].second;
            dif[u] += dif[v];
            ans[id] = dif[v];///这里的id是边的序号
        }
    }
    
    int main()
    {
        scanf("%d",&n);
        for(int i=1;i<n;i++)
        {
            scanf("%d %d",&x,&y);
            edge[x].push_back(make_pair(y,i));
            edge[y].push_back(make_pair(x,i));
        }
        scanf("%d",&m);
        for(int i=0;i<m;i++)
        {
            scanf("%d %d",&x,&y);
            a[i] = x;
            b[i] = y;///记下第i个询问对应的两端点
            query[x].push_back(make_pair(y,i));
            query[y].push_back(make_pair(x,i));
        }
        //选谁作为根是无所谓的,不妨就选1号为根.
        LCA_Tarjan(1);
        solve();
        dfs(1,0);
        for(int i=1;i<n;i++)
            printf("%d%c",ans[i],i==n-1?'
    ':' ');
    }
    

    解法二:

    ST算法( Sparse Table ). ST算法是解决RMQ问题的一种在线算法.

    RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题。——百度百科

    LCA与RMQ的相互转化

    首先说明LCA问题与RMQ问题为何可以相互转化.

    /************分割线************/


    ST算法

    ST算法是基于倍增思想设计的O(NlogN) - O(1)在线算法.


     

    简单来说:

    利用 dp 预处理出每一段的最值,对于每个询问,只要O(1)的时间便能得出答案。

    dp 如下:dp[ i ][ j ]表示从第i个位置开始的 2^j 个数中的最小值。转移方程如下:

    dp[i][j]=min(dp[i][j-1],dp[i+1<<(j-1)][j-1])

    这样,对于每个查询 x, y ( x < y )(在第 x 个位置到第 y 个位置的最值),答案就是

    min(dp[x][j],dp[y-(1<<j)+1][j])(其中j是(int)log2(y-x+1))
    ∵[x,x+(1<<j)]与[y-(1<<j),y]都是[x,y]的子区间且[x,x+1<<j]∪[y-1<<j]=[x,y]。

    至此RMQ问题就解决了,时间复杂度为O( nlogn )+O(1)* q(其中 q 为询问数量)

    求解LCA

    求LCA的其中一种算法便是转换成RMQ,利用ST算法求解。

    具体做法如下:将这棵树用深度优先遍历,每次遍历一个点(包括回溯)都添加进数组里面。找到所询问的点第一次出现的位置,两个位置所夹的点中深度最小的即为所求。

    /*
    
    在树的任意两点所在的所有树的边权加一,输出最后所有的边权
    
    LCA。我们可以在线操作,用数组
    dis[x] ++ ,   dis[y]++,
    dis[LCA(x,y)]  -=  2
    最后DFS统计一下答案
    
    */
    #include <set>
    #include <map>
    #include <list>
    #include <cmath>
    #include <queue>
    #include <stack>
    #include <string>
    #include <vector>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    typedef long long ll;
    typedef unsigned long long ull;
    
    #define debug puts("here")
    #define rep(i,n) for(int i=0;i<n;i++)
    #define rep1(i,n) for(int i=1;i<=n;i++)
    #define REP(i,a,b) for(int i=a;i<=b;i++)
    #define foreach(i,vec) for(unsigned i=0;i<vec.size();i++)
    #define pb push_back
    #define RD(n) scanf("%d",&n)
    #define RD2(x,y) scanf("%d%d",&x,&y)
    #define RD3(x,y,z) scanf("%d%d%d",&x,&y,&z)
    #define RD4(x,y,z,w) scanf("%d%d%d%d",&x,&y,&z,&w)
    #define All(vec) vec.begin(),vec.end()
    #define MP make_pair
    #define PII pair<int,int>
    #define PQ priority_queue
    #define cmax(x,y) x = max(x,y)
    #define cmin(x,y) x = min(x,y)
    #define fir first
    #define sec second
    
    /******** program ********************/
    
    const int MAXN = 200005;
    
    int n;
    vector< PII > edge[MAXN];
    int dp[MAXN][20];
    int dis[MAXN];
    
    int depth;
    int b[MAXN],bn;    //深度序列
    int f[MAXN];    //对应深度序列中的结点编号
    int p[MAXN];    //结点在深度序列中的首位置
    int ans[MAXN];
    
    void dfs(int x,int fa){
        int tmp = ++ depth;
        ///没有必要保证兄弟的深度相等,
        ///只要满足父节点深度小于儿子即可.
        ///而且是不能的!如果兄弟深度相等的话,
        ///就无法依据最近公共祖先的深度确定是哪一个节点了!
        ///这样令兄弟的深度有所不同就使得
        ///找到的"最小深度"可以直接对应节点
        ///(每个节点的深度都是不同的)
        b[++bn] = tmp;
        f[tmp] = x;
        p[x] = bn;
        foreach(i,edge[x]){
            int y = edge[x][i].first;
            if (y==fa) continue;
            dfs(y,x);
            b[++bn] = tmp;
        }
    }
    
    void rmq_init(int n){ //以深度序列做rmq
        rep1(i,n)
            dp[i][0]=b[i];
        int m = floor(log(n*1.0)/log(2.0));
        ///向下取整,因为向上取整并没有完整的2^m长度的区间
        rep1(j,m)
          for (int i=1; i<=n-(1<<j)+1; i++)///非递归的方式
              dp[i][j] = min(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
    }
    
    int rmq(int l,int r){
        int k = floor(log((r-l+1)*1.0)/log(2.0));
        return min( dp[l][k] , dp[r-(1<<k)+1][k] );
    }
    
    int lca(int a,int b){
        if (p[a]>p[b])
            swap(a,b);
        return f[ rmq(p[a],p[b]) ];
    }
    
    void qq(int x,int f){///dfs求ans
        foreach(i,edge[x]){
            int y = edge[x][i].first;
            if(y==f)continue;
            qq(y,x);
            int id = edge[x][i].second;
            dis[x] += dis[y];
            ans[id] = dis[y];
        }
    }
    
    int main(){
    
    
        while(cin>>n){
            rep(i,MAXN)
                edge[i].clear();
            depth = bn = 0;
    
            int x,y;
            REP(i,2,n){
                RD2(x,y);
                edge[x].pb( MP(y,i) );
                edge[y].pb( MP(x,i) );
            }
    
            dfs(1,0);
            rmq_init(bn);
    
            int m;
            RD(m);
            memset(dis,0,sizeof(dis));
            while(m--){
                RD2(x,y);
                dis[x] ++;
                dis[y] ++;
                dis[ lca(x,y) ] -= 2;
            }///和离线做法的差别就在于:
            ///可以直接求出lca而不需要在遍历树的过程中依据相同的端点来求
            qq(1,0);
    
            REP(i,2,n)
                printf("%d ",ans[i]);
            puts("");
        }
    
        return 0;
    }
    

    自己敲一遍:

    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    #include <cstring>
    #include <vector>
    #include <utility>
    using namespace std;
    const int MAXN = 1e5+5;
    
    vector< pair<int, int> > edge[MAXN];
    int n,depth;
    int dfs[MAXN<<1],dn;///dfs序深度序列.一个点会dfs多次,总次数为2*n-1次
    int DtoN[MAXN];///深度对应的节点编号
    int pos[MAXN];///节点第一次出现在dfs序中的位置
    int dp[MAXN<<1][20];
    int dis[MAXN],ans[MAXN];
    
    void build_dfs_series(int s,int f)
    {
        dfs[++dn] = ++depth;
        int tmp = depth;
        DtoN[depth] = s;
        pos[s] = dn;
        for(int i=0;i<edge[s].size();i++)
        {
            int y = edge[s][i].first;
            if(y==f)    continue;
            build_dfs_series(y, s);
            dfs[++dn] = tmp;
        }
    }
    
    void pre_rmq()
    {
        for(int i=1;i<=dn;i++)
            dp[i][0] = dfs[i];
        int m = floor(log(dn*1.0)/log(2.0));
        for(int j=1;j<=m;j++)
        {
            for(int i=1;i<=dn+1-(1<<j);i++)
                dp[i][j] = min(dp[i][j-1], dp[i+(1<<(j-1))][j-1]);
        }
    }
    
    int rmq(int l, int r)
    {
        int j = floor(log((r-l+1)*1.0)/log(2.0));
        return min(dp[l][j], dp[r-(1<<j)+1][j]);
    }
    
    int lca(int x, int y)
    {
        int l = pos[x], r = pos[y];
        if(l>r)
            swap(l, r);
        return DtoN[ rmq(l,r) ];
    }
    
    void cal(int s, int f)
    {
        for(int i=0;i<edge[s].size();i++)
        {
            int y = edge[s][i].first;
            if(y==f)    continue;
            int id = edge[s][i].second;
            cal(y, s);
            dis[s] += dis[y];
            ans[id] = dis[y];///每条边对应它的靠近叶子的节点
        }
    }
    
    int main()
    {
        scanf("%d",&n);
        for(int i=1,u,v;i<n;i++)
        {
            scanf("%d %d",&u,&v);
            edge[u].push_back( make_pair(v, i) );
            edge[v].push_back( make_pair(u, i) );
        }
        depth = 0;
        dn = 0;
        build_dfs_series(1, 0);
        pre_rmq();
        int m,x,y;
        scanf("%d",&m);
        for(int i=0;i<m;i++)
        {
            scanf("%d %d",&x,&y);
            dis[x]++;dis[y]++;
            dis[lca(x,y)] -= 2;
        }
        cal(1, 0);
        for(int i=1;i<n;i++)
            printf("%d%c",ans[i],(i==n-1)?'
    ':' ');
    }

    (有点难记啊...)


  • 相关阅读:
    面向对象(OOP:Objdec Oriented Programming)
    vue中v-model和v-bind区别
    DateTimeFormat
    html中frameset简介
    学习及资料地址
    mybatis+oracle批量新增带序列List对象
    Io流读取并输出文件(例如.mp3格式文件)
    Java从服务器下载图片保存到本地
    转:Java DecimalFormat的主要功能及使用方法
    数据库事务隔离级别-- 脏读、幻读、不可重复读(清晰解释)
  • 原文地址:https://www.cnblogs.com/james1207/p/3266743.html
Copyright © 2020-2023  润新知