图的算法应用 ---- Floyd

17 篇文章 2 订阅
8 篇文章 0 订阅

Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。

在这里插入图片描述

小哼希望在出发前知道任意两个城市之间的最短路程。

上图中有4个城市8条公路,公路上的数字表示这条公路的长短。请注意这些公路是单向的。我们现在需要求任意两个城市之间的最短路程,也就是求任意两个点之间的最短路径。这个问题这也被称为“多源最短路径”问题。

现在需要一个数据结构来存储图的信息,我们仍然可以用一个4*4的矩阵(二维数组e)来存储。比如1号城市到2号城市的路程为2,则设e[1][2]的值为2。2号城市无法到达4号城市,则设置e[2][4]的值为∞。另外此处约定一个城市自己是到自己的也是0,例如e[1][1]为0。

在这里插入图片描述

我们回到最开始的问题,最短路径问题,如何求任意两点之间的最短路径呢?

我们通过之前的学习,我们知道通过深搜或者广搜都可以求出两点的最短路径,所以进行n^2 遍深搜或广搜,即对每个点都进行一次深搜或者广搜,我们就可以求得最短路径。但是我们最常用求最短路径的算法的就是bellman-ford,dijkstra,spfa,floyd算法。

如果求任意两点之间的最短路径,两点之间可以直接到达但却不是最短的路径,要让任意两点(例如从顶点a点到顶点b)之间的路程变短,只能引入第三个点(顶点k),并通过这个顶点k中转即a->k->b,才可能缩短原来从顶点a点到顶点b的路程。那么这个中转的顶点k是1~n中的哪个点呢?甚至有时候不只通过一个点,而是经过两个点或者更多点中转会更短。比如上图中从4号城市到3号城市(4->3)的路程e[4][3]原本是12。如果只通过1号城市中转(4->1->3),路程将缩短为11(e[4][1]+e[1][3]=5+6=11)。其实1号城市到3号城市也可以通过2号城市中转,使得1号到3号城市的路程缩短为5(e[1][2]+e[2][3]=2+3=5)。所以如果同时经过1号和2号两个城市中转的话,从4号城市到3号城市的路程会进一步缩短为10。通过这个的例子,我们发现每个顶点都有可能使得另外两个顶点之间的路程变短。好,下面我们将这个问题一般化。

当任意两点之间不允许经过第三个点时,这些城市之间最短路程就是初始路程,如下:

在这里插入图片描述

假如现在只允许经过1号顶点,求任意两点的最短路径我们应该怎么求呢??

此时我们只需判断e[i][1] + e[1][j] 是否比e[i][j] 要小即可。我们来说明一下e[i][j] 和 e[i][1] + e[1][j] 表示的是什么意思,e[i][j] 就是便是从I号定点到 j 号顶点之间的路程,e[i][1] + e[1][j] 表示的是从 i 号顶点到 1 号顶点,再从1号顶点到 j 号顶点的路径之和。

在这其中,I是从1n循环,j也是从1n循环,具体这一步的实现代码如下。

for (i = 1;i <= n; ++i) {
    for (j = 1;j <= n; ++j) {
	      if ( e[i][j] > e[i][1]+e[1][j] ) {
	          e[i][j] = e[i][1]+e[1][j];
	      }
     }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在只允许过 1号顶点的情况下,任意两点之间的路程更新为:

在这里插入图片描述

通过上图我们发现,在只通过1号顶点中转的情况下,3号和2号顶点(e[3][2])、4号顶点到2号顶点(e[4][2])以及4号顶点到3号顶点(e[4][3])的路程都变短了。

接下来继续求在只允许经过1和2号两个顶点的情况下任意两点之间的最短路程。如何做呢?我们需要在只允许经过1号顶点时任意两点的最短路程的结果下,再判断如果经过2号顶点是否可以使得i号顶点到j号顶点之间的路程变得更短。即判断 e[i][2] + e[2][j] 是否比 e[i][j] 要小,具体实现代码如下:

//经过1号顶点
for(i=1;i<=n;i++)
   for(j=1;j<=n;j++)
        if (e[i][j] > e[i][1]+e[1][j])
            e[i][j]=e[i][1]+e[1][j];
 
 
//经过2号顶点
for(i=1;i<=n;i++)
    for(j=1;j<=n;j++)
        if (e[i][j] > e[i][2]+e[2][j])
            e[i][j]=e[i][2]+e[2][j];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在只允许更新1号和2号顶点的情况下,任意两点之间的路径更新为:

在这里插入图片描述

通过上图我们可以看出来,在相比只允许1号顶点进行中转的情况,这里允许通过1号和2号顶点进行中转,使得e[1][3] 和e[4][3]的路程变得更短了。

同理,我们在只允许通过1,2,3号顶点的情况下,求任意两点之间的最短路程。任意两点的最短路程更新为:

在这里插入图片描述

最后是允许所有顶点作为中转,任意两点的最终路程为:

在这里插入图片描述

整个算法过程虽然说起来很麻烦,但是核心代码就那么几行,不信你看:

for(int k = 1 ; k <= n ; k ++)
{
        for(int i = 1 ; i <= n ; i ++)
        {
                for(int j = 1 ; j <= n ; j ++)
                {
                        if(e[i][j] > e[i][k] + e[k][j])
                                e[i][j] = e[i][k] + e[k][j];
                 }
        }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这个代码的基本思路就是我们从最开始的只允许经过1号顶点进行中转,接下来只允许1,2进行中转。。。。。允许经过1~n 号所有的顶点进行中转,求任意两点的最短路径。

这个算法的完整代码:

#include<bits/stdc++.h>
using namespace std;
const int INF = 99999999;
int main()
{
        int e[10][10] , n , m , t1 , t2 , t3;
        cin>>n>>m;  //n表示顶点个数,m表示边的条数
        for(int i = 1 ; i <= n ; i ++)
        {
                for(int j = 1 ; j <= n ; j ++)
                {
                        if(i == j)
                                e[i][j] = 0 ;
                        else
                                e[i][j] = INF;
                }
        }
        for(int i = 1 ; i <= m ; i ++)
        {
                cin>>t1>>t2>>t3;
                e[t1][t2] = t3;
        }
 
        //核心代码
        for(int k = 1 ; k <= n ; k ++)
        {
                for(int i = 1 ; i <= n ; i ++)
                {
                        for(int j = 1 ; j <= n ; j ++)
                        {
                                if(e[i][j] > e[i][k] + e[k][j])
                                        e[i][j] = e[i][k] + e[k][j];
                        }
                }
        }
 
        for(int i = 1 ; i <= n ; i ++)
        {
                for(int j = 1 ; j <= n ; j ++)
                {
                        printf("%3d",e[i][j]);
                }
                cout<<endl;
        }
        return 0 ;
}
 
/*
4 8
1 2 2
1 3 6
1 4 4
2 3 3
3 1 7
3 4 1
4 1 5
4 3 12
*/

我们来分析一下算法:

Floyd优缺点分析:

优点:比较容易容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。
缺点:时间复杂度比较高 ( n 3 ) (n^3)(n3) ,不适合计算大量数据,当数据稍微大点儿的时候就可以选择其他的算法来解决问题了,不然也会是超时。

Floyd算法与Dijkstra算法的不同

1.Floyd算法是求任意两点之间的距离,是多源最短路,而Dijkstra(迪杰斯特拉)算法是求一个顶点到其他所有顶点的最短路径,是单源最短路。
2.Floyd算法属于动态规划,我们在写核心代码时候就是相当于推dp状态方程,Dijkstra(迪杰斯特拉)算法属于贪心算法。
3.Dijkstra(迪杰斯特拉)算法时间复杂度一般是 O ( n 2 ) O(n^2)O(n2) ,Floyd算法时间复杂度是 O ( n 3 ) O(n^3)O(n3),Dijkstra(迪杰斯特拉)算法比Floyd算法块。
4.Floyd算法可以算带负权的,而Dijkstra(迪杰斯特拉)算法是不可以算带负权的。并且Floyd算法不能算负权回路。

prim算法(普利姆算法):对图G(V,E)设置集合S,存放已访问的顶点,然后每次从集合V-S中选择与集合S的最短距离最小的一个顶点(记为u),访问并加入集合S。之后,令顶点u为中介点,优化所有从u能到达的顶点v与集合S之间的最短距离。执行n次(n为顶点个数),直到集合S已包含所有顶点。

算法实例演示
首先让我们来看一个example。如下图所示,图a是一个连通图(右图是图a对应的邻接矩阵,假设图中的边的权值大于0),我们现在基于该图来演示Prim算法的过程。


我们选择一个起点,然后在与起点相连且未被选的节点中选择一个权值最小的节点,将该节点与其相连边添加入生成树。假设起点是0节点,与0节点相连且未被选的节点是{1,2,3},分别对应的权值是{6,1,5},可见当前最小的权值1,权值最小的节点就是2节点,所以将2节点和0-2的边添加入生成树,如图b所示。

接着我们在与已选节点相连且未被选的节点中选择一个权值最小的节点,将该节点与其相连边添加入生成树。当前已选节点是0,2节点,与已选节点相连且未被选的节点有{1,3,4,5},分别对应的权值是{(6,5),(5,5),6,4,},可见当前最小的权值4,权值最小的节点就是5节点,所以将5节点和2-5的边添加入生成树,如图c所示。(其实在编程时,我们只需记录与更新当前较小的那个权值,如与{1,3,4,5}对应的权值我们只需记录{5,5,6,4},当然我们也需利用了另一个数组来加以区别当前权值对应的连接点,如当前权值{5,5,6,4}所对应的连接点就是{2,0,2,2})

接着我们继续在与已选节点相连且未被选的节点中选择一个权值最小的节点,将该节点与其相连边添加入生成树。当前已选节点是0,2,5节点,与已选节点相连且未被选的节点有{1,3,4},分别对应的权值是{(6,5),(2,5,5),(6,6),}(其实当前我们可只记录{5,2,6},同时记录其对应的连接点分别是{2,5,2}),可见当前最小的权值2,权值最小的节点就是3节点,所以将3节点和5-3的边添加入生成树,如图d所示。

接着我们依照上一次的步骤继续在与已选节点相连且未被选的节点中选择一个权值最小的节点,将该节点与其相连边添加入生成树。如图e,f所示。最终图f就是我们通过Prim算法得到的最小生成树了。

prim算法基本思想:对图G(V,E)设置集合S来存放已被访问的顶点,然后执行n次下面的两个步骤(n为顶点个数)

每次从集合V-S中选择与集合S最近的一个顶点(记为u),访问u并将其加入集合S,同时把这条离集合S最近的边加入最小生成树。
令顶点u作为集合S与集合V-S连接的接口,优化从u能到达的未访问顶点v与集合S的最短距离
prim算法的具体实现

集合S的实现方法:使用一个bool型数组vis[]表示顶点是否已被访问。其中vis[i]==true表示顶点Vi已被访问,vis[i]==false则表示顶点Vi未被访问。
不妨令int型数组d[]来存放顶点Vi(0<=i<=n-1)与集合S的最短距离。初始时除了起点s的d[s]赋为0,其余顶点都赋为一个很大的数来表示INF,即不可达。
prim算法与Dijkstra算法区别:
Dijkstra算法的数组d[]含义为起点s达到顶点Vi的最短距离。
prim算法的数组d[]含义为顶点Vi与集合S的最短距离

prim算法伪代码

Prim(G, d[])
{
    初始化;
    for(循环n次)
    {
        u = 使d[u]最小的还未被访问的顶点的标号;
        记u已被访问;
        for(从u出发能到达的所有顶点v)
        {
            if(v未被访问 && 以u为中介点使得v与集合S的最短距离d[v]更优)
            {
                将G[u][v]赋值给v与集合S的最短距离d[v];
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Dijkstra算法和prim算法实际上是相同思路,数组d[]含义不同。

定义MAXV为最大顶点数,INF为一个很大的数字

const int MAXV = 1000; //最大顶点数
const int INF = 100000000; //设INF为一个很大的数
1
2
邻接矩阵版
int n, G[MAXV][MAXV]; //n为顶点数,MAXV为最大顶点数
int d[MAXV]; //顶点与集合S的最短距离
bool vis[MAXV] = {false}; //标记数组,vis[i] == true表示访问。初值均为false
intprim() //默认0号为初始点,函数返回最小生成树的边权之和
{
    fill(d, d + MAXV, INF); //fill函数将整个d数组赋为INF
    d[0] = 0; //只有0号顶点到集合S的距离为0,其余全是INF
    int ans = 0; //存放最小生成树的边权之和
    for(int i = 0; i < n; i++) //循环n次
    {
        int u = -1, MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
        for(int j = 0; j < n; j++) //找到未访问的顶点中d[]最小的
        {
            if(vis[j] == false && d[j] < MIN)
            {
                u = j;
                MIN = d[j];
            }
        }
        //找不到小于INF的d[u],则剩下的顶点和集合S不连通
        if(u == -1)
            return -1;
        vis[u] = true; //标记u为已访问
        ans += d[u]; //将与集合S距离最小的边加入最小生成树
        for(int v = 0; v < n; v++)
        {
            //v未访问 && u能到达v && 以u为中介点可以使v离集合S更近
            if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v])
            {
                d[v] = G[u][v]; //将G[u][v]赋值给d[v]
            }
        }
    }
    return ans; //返回最小生成树的边权之和
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
邻接表版
struct Node
{
    int v, dis; //v为边的目标顶点,dis为边权
};
vector<Node> Adj[MAXV]; //图G,Adj[u]存放从顶点u出发可以到达的所有顶点
int n; //n为顶点数,图G使用邻接表实现,MAXV为最大顶点数
int d[MAXV]; //顶点与集合S的最短距离
bool vis[MAXV] = {false}; //标记数组,vis[i] == true表示已访问。初值均为false

int prim() //默认0号为初始点,函数返回最小生成树的边权之和
{
    fill(d, d + MAXV, INF); //fill函数将整个d数组赋为INF
    d[0] = 0; //只有0号顶点到集合S的距离为0,其余全是INF
    int ans = 0; //存放最小生成树的边权之和
    for(int i = 0; i < n; i++) //循环n次
    {
        int u = -1, MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
        for(int j = 0; j < n; j++) //找到未访问的顶点中d[]最小的
        {
            if(vis[j] == false && d[j] < MIN)
            {
                u = j;
                MIN = d[j];
            }
        }
        //找不到小于INF的d[u],则剩下的顶点和集合S不连通
        if(u == -1)
            return -1;
        vis[u] = true; //标记u为已访问
        ans += d[u]; //将与集合S距离最小的边加入最小生成树
        //只有下面这个for与邻接矩阵的写法不同
        for(int j = 0; j < Adj[u].size(); j++)
        {
            int v = Adj[u][j].v; //通过邻接表直接获得u能到达的顶点v
            if(vis[v] == false && Adj[u][j].dis < d[v])
            {
                //如果v未访问 && 以u为中介点可以使v离集合S更近
                d[v] = Adj[u][j].dis;
            }
        }
    }
    return ans; //返回最小生成树的边权之和
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
例题
求最小生成树

从V0开始,依次找到各顶点边权最小的,然后相连


#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXV = 1000; //最大顶点数
const int INF = 1000000000; //设INF为一个很大的数

int n, m, G[MAXV][MAXV]; //n为顶点数,MAXV为最大顶点数
int d[MAXV]; //顶点与集合S的最短距离
bool vis[MAXV] = {false}; //标记数组,vis[i]==true表示已访问。初值均为false

int prim() //默认0号为初始点,函数返回最小生成树的边权之和
{
    fill(d, d + MAXV, INF); //fill函数将整个d数组赋为INF
    d[0] = 0; //只有0号顶点到集合S的距离为0,其余全是INF
    int ans = 0; //存放最小生成树的边权之和
    for(int i = 0; i < n; i++) //循环n次
    {
        int u = -1, MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
        for(int j = 0; j < n; j++) //找到未访问的顶点中d[]最小的
        {
            if(vis[j] == false && d[j] < MIN)
            {
                u = j;
                MIN = d[j];
            }
        }
        //找不到小于INF的d[u],则剩下的顶点和集合S不连通
        if(u == -1)
            return -1;
        vis[u] = true; //标记u为已访问
        ans += d[u]; //将与集合S距离最小的边加入最小生成树
        for(int v = 0; v < n; v++)
        {
            //v未访问 && u能到达v && 以u为中介点可以使v离集合S更近
            if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v])
            {
                d[v] = G[u][v]; //将G[u][v]赋值给d[v]
            }
        }
    }
    return ans; //返回最小生成树的边权之和
}

int main()
{
    int u, v, w;
    scanf("%d%d", &n, &m); //顶点个数、边数
    fill(G[0], G[0] + MAXV * MAXV, INF); //初始化图G
    for(int i = 0; i < m; i++)
    {
        scanf("%d%d%d", &u, &v, &w); //输入u,v以及边权
        G[u][v] = G[v][u] = w; //无向图
    }
    int ans = prim(); //prim算法入口
    printf("%d\n", ans);
    return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//输入数据
6 10 //6个顶点,10条边,以下10行10边
0 1 4 //边0->1与1->0的边权为4,下同
0 4 1
0 5 2
1 2 6
1 5 3
2 3 6
2 5 5
3 4 4
3 5 5
4 5 3
1
2
3
4
5
6
7
8
9
10
11
12
//输出结果
15
 

图的算法: 克鲁斯卡尔算法_百度百科

迪克斯特拉算法_百度百科

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HehuaTang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值