zoukankan      html  css  js  c++  java
  • 分层图模型详解

    概论

      分层图,即拆点,是图论问题中一种常见的建图技巧,应用较为广泛。深入理解并掌握这种技巧,对设计算法解决一些图论问题会颇有助益。

      类比动态规划中的状态机模型,当单纯的一个点无法表示清楚其上信息时,我们可以考虑拆点,将一个状态拆成多个状态,这样就可以把信息理清楚了。李煜东在《算法竞赛进阶指南》中指出,动态规划对状态空间的遍历构成一张有向无环图,遍历顺序就是该有向无环图的一个拓扑序。拆点分层之后如何建边,以及原问题的答案对应分层图中的哪个点,我们都可以通过动态规划的思想来考虑,然后将其还原回图中。

      分层图的一般模型为:在原图上,进行k次特殊操作,然后求解。我们一般便会将原图复制成k+1张完全一样的图,每张图分别表示进行了0次、1次、2次、……、k次特殊操作,然后在图之间(即每一层之间)根据某些关系连边。

      最后的图大概长这个样子:

    例题

    [POJ3662]通信线路

    题目链接

    题意简述

      在无向图上求出一条从1到N的路径,可指定路径上K条边权值为0,然后最小化路径上的最大边权。

    算法概述

      这题是分层图最短路的一道典型例题。

      考虑dp[p,k]表示从1号点到p号点,当前已经指定了k条边权值为0时,路径上权值最大的边权是多少。考虑其任一出边(p,v,w),则dp[p,k]能够更新的状态有dp[v,k]=max(dp[p,k],w)①,dp[v,k+1]=dp[p,k]②。

      根据上述动态规划的思想建立分层图,将dp的状态表示记为点,如dp[p,k]则记为p+k*n,这样可以防止点号的重复。对于①式,dp[v,k]与dp[p,k]同在第k+1层,之间连原边即可,即从p+k*n向v+k*n连一条权值为w的边。对于②式,则由p+k*n向v+(k+1)*n连一条权值为0的边。

      注意是无向图,故边连双向。

      最后答案的答案,我们可以先看dp状态,根据我们dp的状态表示,最后的答案应是dp[n,0],dp[n,1],dp[n,2],…,dp[n,k]中的最小值,所以对应到图上,即为n,2*n,3*n,…,(k+1)*n上的最短距离。

      有一个比较偷懒的技巧,可以在跑最短路之前,从n向2*n,2*n向3*n,…,k*n向(k+1)*n连一条长度为0的单向边,这样答案就只需要看(k+1)*n上的值即可。

      用Dijkstra算法的时间复杂度是O(km*logkn)

      当然这题还有二分答案转化为01最短路问题的解法,我们在此不予赘述。这个解法有很大局限性,我们可以看到,下一道例题,仅仅是最后询问不同,却无法用这种解法解决,但分层图的做法仍能很好地解决。

    参考代码

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    using namespace std;
    const int N=1e3+10,M=4e7+10,MAXK=1e3+10;
    struct Edge{
        int to,next,w;
    }edge[M]; int idx;
    int h[N*MAXK];
    int dis[N*MAXK],vis[N*MAXK];
    int n,m,K;
    
    void add_edge(int u,int v,int w){edge[++idx]={v,h[u],w};h[u]=idx;}
    
    void dijkstra()
    {
        memset(vis,0,sizeof vis);
        memset(dis,0x3f,sizeof dis);
        
        priority_queue<pair<int,int> > q;
        
        dis[1]=0;
        q.push(make_pair(0,1));
        
        while(!q.empty())
        {
            int k=q.top().second;
            q.pop();
            if(vis[k])continue;
            vis[k]=1;
            for(int i=h[k];~i;i=edge[i].next)
            {
                int to=edge[i].to,w=edge[i].w;
                if(dis[to]>max(dis[k],w))
                {
                    dis[to]=max(dis[k],w);
                    q.push(make_pair(-dis[to],to));
                }
            }
        }
    }
    
    int main()
    {
        memset(h,-1,sizeof h);
        scanf("%d%d%d",&n,&m,&K);
        while(m--)
        {
            int u,v,w;scanf("%d%d%d",&u,&v,&w);
            for(int i=0;i<=K;i++)
            {
                add_edge(u+i*n,v+i*n,w);
                add_edge(v+i*n,u+i*n,w);
            }
            for(int i=0;i<K;i++)
            {
                add_edge(u+i*n,v+(i+1)*n,0);
                add_edge(v+i*n,u+(i+1)*n,0);
            }
        }
        
        dijkstra();
        
        if(dis[(K+1)*n]==0x3f3f3f3f)printf("-1
    ");
        else printf("%d
    ",dis[(K+1)*n]);
        
        return 0;
    }
    

    [JLOI2011]飞行路线

    题目链接

    题意简述

      给定一张无向图,可以指定图中k条边权值为0,求给定两点s,t之间的最短路径。

    算法概述

      裸的分层图,分析见上题,算法中唯一不同之处即为求路径上最大边权改为求路径上边权总和。

    参考代码

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    using namespace std;
    const int N=1e4+10,M=5e4+10,MAXK=11;
    struct Edge{
    	int to,next,w;
    }edge[M*MAXK*4]; int idx;
    int h[N*MAXK];
    int dis[N*MAXK],vis[N*MAXK];
    int n,m,K,s,t;
    
    void add_edge(int u,int v,int w){edge[++idx]={v,h[u],w};h[u]=idx;}
    
    void dijkstra(int s)
    {
    	memset(vis,0,sizeof vis);
    	memset(dis,0x3f,sizeof dis);
    	
    	priority_queue<pair<int,int> > q;
    	
    	dis[s]=0;
    	q.push(make_pair(0,s));
    	
    	while(!q.empty())
    	{
    		int k=q.top().second;
    		q.pop();
    		if(vis[k])continue;
    		vis[k]=1;
    		for(int i=h[k];~i;i=edge[i].next)
    		{
    			int to=edge[i].to,w=edge[i].w;
    			if(dis[to]>dis[k]+w)
    			{
    				dis[to]=dis[k]+w;
    				q.push(make_pair(-dis[to],to));
    			}
    		}
    	}
    }
    
    int main()
    {
    	memset(h,-1,sizeof h);
    	scanf("%d%d%d",&n,&m,&K);
    	scanf("%d%d",&s,&t);
    	while(m--)
    	{
    		int u,v,w;scanf("%d%d%d",&u,&v,&w);
    		for(int i=0;i<=K;i++)
    		{
    			add_edge(u+i*n,v+i*n,w);
    			add_edge(v+i*n,u+i*n,w);
    		}
    		for(int i=0;i<K;i++)
    		{
    			add_edge(u+i*n,v+(i+1)*n,0);
    			add_edge(v+i*n,u+(i+1)*n,0);
    		}
    	}
    	for(int i=0;i<K;i++)add_edge(t+i*n,t+(i+1)*n,0);
    	dijkstra(s);
    	printf("%d
    ",dis[t+K*n]);
    }
    

    [CH6101]最优贸易

    题目链接

    题意简述

      给定一张有向图,无边权,每个点有一个点权w,求从1到n的路径上,w[q]-w[p]的最大值。其中p和q为路径上的点,且必须先经过p再经过q。

    算法概述

      一个简单的做法是跑正着求一遍以1为源点的最短路,反着求一遍以n为源点的最长路,然后枚举每个点,两个dist作差取max即可(具体可看下面附的代码)。

      但是这种做法也有局限,当图中有边权存在时,这种做法就不太好做了。

      所以这里介绍一种通用的做法——分层图最短路。

      还是先看以dp的思想来分析,设dp[p,s]为从1到p这个点,当前p的状态为s的最长路,其中s=0,1,2。0表示当前还未进行交易,1表示当前已买入但未卖出,2表示当前已完成一次交易(即已买入且卖出一次)。

      考虑状态转移,首先dp[p,1]=dp[p,0]-w[p],dp[p,2]=dp[p,1]+w[p],其次对于p的每一条出边(p,v),dp[v,s]=dp[p,s]。

      故我们可建立三层完全一样的图,然后考虑层间的建边,对于每个点,只需从p向p+n连一条长度为-w[p]的边,从p+n向p+2*n连一条长度为w[p]的边即可。

      最后答案即为dp[n,2],也就是3*n这个点上的信息。

    参考代码

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    using namespace std;
    const int N=3e5+10,M=1e6+10;
    struct Edge{
        int to,next,w;
    }edge[3*M+N];int idx;
    int h[N];
    
    void add_edge(int u,int v,int w){edge[++idx]={v,h[u],w};h[u]=idx;}
    
    int dis[N],vis[N];
    int price[N];
    int n,m;
    
    void spfa()
    {
        memset(vis,0,sizeof vis);
        memset(dis,-0x3f,sizeof dis);
    
        queue<int> q;
        dis[1]=0,vis[1]=1;
        q.push(1);
        while(!q.empty())
        {
            int p=q.front();
            q.pop();
            vis[p]=0;
            for(int i=h[p];~i;i=edge[i].next)
            {
                int to=edge[i].to,w=edge[i].w;
                if(dis[to]<dis[p]+w)
                {
                    dis[to]=dis[p]+w;
                    if(!vis[to])
                    {
                        q.push(to);
                        vis[to]=1;
                    }
                }
            }
        }
    }
    
    int main()
    {
        memset(h,-1,sizeof h);
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)scanf("%d",&price[i]);
        while(m--)
        {
            int a,b,c;scanf("%d%d%d",&a,&b,&c);
            for(int i=0;i<3;i++)
            {
                add_edge(a+i*n,b+i*n,0);
                if(c==2)add_edge(b+i*n,a+i*n,0);
            }
        }
        for(int i=1;i<=n;i++)
        {
            add_edge(i,i+n,-price[i]);
            add_edge(i+n,i+n+n,price[i]);
        }
        spfa();
        printf("%d
    ",dis[3*n]);
        return 0;
    }
    

      附:

    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    #include <queue>
    using namespace std;
    const int N=1e5+10,M=2e6+10;
    struct Edge{
        int to,ne;
    }edge1[M<<1],edge2[M<<1]; int idx1,idx2;
    int h1[N],h2[N];
    int n,m;
    int d[N],vis[N],f[N];
    int price[N];
    
    void add_edge(Edge edge[],int h[],int &idx,int u,int v)
    {
        edge[++idx].to=v;
        edge[idx].ne=h[u];
        h[u]=idx;
    }
    
    void SPFA(int dis[],int s,int lab,Edge edge[],int h[])
    {
        if(!lab)memset(dis,0x3f,sizeof d);
        else memset(dis,-0x3f,sizeof f);
        dis[s]=price[s];
        
        queue<int> q;
        q.push(s);
        vis[s]=1;
        while(!q.empty())
        {
            int k=q.front();
            q.pop();
            vis[k]=0;
            
            for(int i=h[k];~i;i=edge[i].ne)
            {
                int to=edge[i].to;
                if(!lab)
                {
                    if(dis[to]>min(dis[k],price[to]))
                    {
                        dis[to]=min(dis[k],price[to]);
                        if(!vis[to]){q.push(to);vis[to]=1;}
                    }
                }
                else
                {
                    if(dis[to]<max(dis[k],price[to]))
                    {
                        dis[to]=max(dis[k],price[to]);
                        if(!vis[to]){q.push(to);vis[to]=1;}
                    }
                }
                
            }
        }
    }
    
    int main()
    {
        memset(h1,-1,sizeof h1);
        memset(h2,-1,sizeof h2);
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)scanf("%d",&price[i]);
        for(int i=1;i<=m;i++)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            add_edge(edge1,h1,idx1,a,b);
            add_edge(edge2,h2,idx2,b,a);
            if(c==2)
            {
                add_edge(edge1,h1,idx1,b,a);
                add_edge(edge2,h2,idx2,a,b);
            }
        }
        SPFA(d,1,0,edge1,h1);
        SPFA(f,n,1,edge2,h2);
        
        int res=0;
        for(int i=1;i<=n;i++)
            res=max(res,f[i]-d[i]);
        
        printf("%d
    ",res);
        
        return 0;
    }
    

      

  • 相关阅读:
    在Eclipse或者STS中使用SVN插件
    SVN的忽略、版本回退、版本冲突(window)
    SVN的图标集
    HTML5+NodeJs实现WebSocket即时通讯
    import和require的区别
    正则表达式 金额验证
    vue 弹窗弹窗禁止滑动的方法 看了网上很多方法 都是扯淡 直接贴上代码
    uni-app关于小程序及app端第三方微信登陆问题
    用 async/await 来处理异步
    微信小程序 子组件调用父组件方法
  • 原文地址:https://www.cnblogs.com/ninedream/p/13454983.html
Copyright © 2011-2022 走看看