• 图论——Floyd算法拓展及其动规本质


    一、Floyd算法本质

      首先,关于Floyd算法:

      Floyd-Warshall算法是一种在具有正或负边缘权重(但没有负周期)的加权图中找到最短路径的算法。算法的单个执行将找到所有顶点对之间的最短路径的长度(加权)。

      通俗一点说,Floyd就是可以用于求解多源汇最短路径的算法,也就是求连通图中任意两点间的最短路径,当然,如果不连通,它返回的就是无穷大(初始化为无穷大)。Floyd可以处理负权,但无法处理有负权环的图。

      接下去进入正题:

      众所周知,Floyd算法本质其实是动态规划。它其实是由三维数组的DP优化而来。

      我们用数组dis[i,j,k]表示从点i到点j,以前k个点作为中转点的最短路径长度。

      为了实现状态转移,我们把当前dis[i,j,k]的所有状态的集合划分为两类,一类是经过k点的,一类是不经过k点的。对于前者,显然dis[i,j,k]=dis[i,j,k-1];对于后者,我们可以得到dis[i,j,k]=dis[i,k,k-1]+dis[k,j,k-1],也就是i到k的最短路径长度加上k到j的最短路径长度。于是我们便可以得到状态转移方程:

      dis[i,j,k] = min(dis[i,j,k-1],dis[i,k,k-1]+dis[k,j,k-1]

      边界条件:dis[i,j,0] = w[i,j],即i与j之间的直接边的权值,若不存在则为正无穷;还有dis[i,i,0]=0。

      代码如下:

    void floyd_original() {
        for(int k=1;k<=n;k++) {
            for(int i=1;i<=n;i++) {
                for(int j=1;j<=n;j++) {
                    dis[i][j][k]=min(dis[i][j][k-1],dis[i][k][k-1]+dis[k][j][k-1]);
                }
            }
        }
    }

      类比前面背包问题的优化方式,我们发现对于每一层k,它的状态计算只与第k-1层的状态有关,那么我们便可以省略这一维。因为省略之后,在计算第k层的dis[i,j]时,我们所需的dis[i,k]和dis[k,j]还是上一层的。

      这一点用一个式子便可证明:

      dis[i,k,k] = min(dis[i,k,k-1],dis[i,k,k-1]+dis[k,k,k-1]) = min(dis[i,k,k-1], d[i,k,k-1]+0) = d[i,k,k-1]

      dis[k,j]同理即可得证。

      于是我们便可得到最普遍的二维数组的状态转移方程:

      dis[i,j] = min(dis[i,j],dis[i,k]+dis[k,j])

      从三维变成二维确实降低了空间开销,但是我们也可以发现时间复杂度是不变的,仍然是O(n³)

    二、Floyd算法变形解决有边数限制的最短路问题

      我们用三维数组d[i,j,e]表示点i到点j,经过e条边的最短路径长度。

      我们假设经过的倒数第二个点是k,那么我们很容易就可以得到状态转移方程:

      d[i,j,e]=min{d[i,k,e-1]+w[k,j]} k∈[1,n]

      代码如下:

    for(int e=1;e<=n;e++)
        for(int k=1;k<=n;k++)
            for(int i=1;i<=n;i++)
                for(int j=1;j<=n;j++)
                    d[i][j][e]=min(d[i][j][e],d[i,k,e-1]+w[k][j]);

      但是这样处理的时间复杂度高达O(n4),于是我们自然会想到要做一些优化。

      我们为了达到明显的指数级别的优化效果,我们选择二进制优化。

      假设限制的边数为s时,我们把第三维e表示成2e条边。那么我们预处理时只需要将2e<s的所有符合条件的e枚举完即可。然后便可以用若干个2的整数次幂的和表示出s。

      我们再用数组f[i,j,t]来表示状态,其中t表示边数为前t个2的整数次幂的和,那么我们就可以得到状态转移方程:

      f[i,j,t] = min{f[i,k,t-1]+d[k,j,s(t)]}

      其中s(t)表示将s进行2的指数幂分解后,所得的所有的2的幂中的第t个2的幂。k是i到j的最短路径中间经过的一个点,将路径中所有的边划分为前20+21+…+2t-1条与后2t条。

      那么我们就只需在最外层枚举k即可。这个算法的时间复杂度就可以降低到O(n3logn)

    算法思路:

      先预处理出d数组,d[i,j,e]表示从顶点i到顶点j,经过2k条边的最短路长度。找到中介点k,将路径边数分成两半。

      状态转移方程如下:

      d[i,j,e]=min(d[i,j,e],d[i,k,e-1]+d[k,j,e-1]}

      然后处理f数组。

      代码如下:

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    using namespace std;
    const int N = 1e3+10;
    const int K = log2(N);
    int n,m,s;
    int d[N][N][K],f[N][N][K]; //使用邻接矩阵存储图 
    
    void floyd(int s) //有边数限制的最短路问题 
    {
        memset(f,0x3f,sizeof f);
        int z[K],max_e=log2(s); //s为所限制的边数 
        
        memset(z,0,sizeof z);
        
        //先处理出s的2的指数次幂的分解,用z数组存储
        //如 p=2^1+2^4+2^5,则z[1]=1,z[2]=4,z[3]=5 
        
        int cnt=0,sum=0;
        while(s)
        {
            if(s&1)z[++cnt]=sum;
            sum++;
            s>>=1;
        }
    
        //处理d数组,d[i][j][k]表示从i到j经过2^k条边的最短路长度
        //复杂度 n^3*logs 
        for(int e=0;e<=max_e;e++)
            for(int i=1;i<=n;i++)d[i][i][e]=0; //处理d的边界 
        for(int e=1;e<=max_e;e++)
        {
            for(int k=1;k<=n;k++)
                for(int i=1;i<=n;i++)
                    for(int j=1;j<=n;j++)
                    {
                        //状态转移方程如下 
                        d[i][j][e]=min(d[i][j][e],d[i][k][e-1]+d[k][j][e-1]);
                        //找到中介点k,将2^e条边的最短路分成两半,分别是2^(e-1)条。 
                    }
        }
        
        for(int t=0;t<=cnt;t++)
            for(int i=1;i<=n;i++)f[i][i][t]=0; //处理f的边界 
        for(int t=1;t<=cnt;t++)
        {
            for(int k=1;k<=n;k++)
                for(int i=1;i<=n;i++)
                    for(int j=1;j<=n;j++)
                    {
                        //状态转移方程 
                        f[i][j][t]=min(f[i][j][t],f[i][k][t-1]+d[k][j][z[t]]);
                    }
        } 
        
        printf("%d
    ",f[1][n][cnt]);
    }
    
    int main()
    {
        scanf("%d%d%d",&n,&m,&s);
        
        memset(d,0x3f,sizeof d); // d数组的初始化 
        
        for(int i=1;i<=m;i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            d[u][v][0]=d[v][u][0]=w; // d数组的赋值 
        }
        
        floyd(s);
        return 0;
    }

    关于算法适用范围:

      个人认为,由于时间复杂度如此感人,相比于同样用于处理“有边数限制的最短路问题”的Bellman-Ford算法的最坏情况,也就是遇到完全图时,O(nm)变成O(n3),也是过犹不及。

      但是,当题目毒瘤到一定程度的时候,当出题人变态到一种境界的时候,边数极其之多,Bellman-Ford算法所用以存储图的边集数组所需的空间开销极大,超过限制时,就是这个算法大展拳脚的时候了。

       大家对这个算法有兴趣的话可以去看一下这道题:POJ 3613

  • 相关阅读:
    小结:机器学习基础部分
    概率图:HMM:Evaluation问题(前向算法/后向算法)
    概率图:GMM求解:EM优化算法的导出(从【ELBO+KL】和【ELBO+Jensen】两个角度导出)
    概率图:GMM:EM算法及其收敛性证明
    概率图:高斯混合模型(GMM)
    概率图基础:D-separation;全局Markov性质;Markov Blanket
    概率图基础:概率基本概念、条件独立性、图求解联合概率的规则合理性推理
    mysql索引失效
    mysql 统计行数count(*)
    mysql如何收缩表空间
  • 原文地址:https://www.cnblogs.com/ninedream/p/11203285.html
Copyright © 2020-2023  润新知