最短路径经典算法其三——Floyd

最短路径经典算法其三——Floyd

  • 前言
  • 算法代码模板和原理讲解
  • 代码实现
  • 例题讲解
    • P1119 灾后重建 [ 提高+/省选-]
      • 题目
      • 思路讲解
      • 代码实现细节
      • 测试样例
    • UVA247 Calling Circles[提高+/省选-]
      • 题目
      • 思路讲解:
      • 代码实现细节
      • 测试样例
  • 结语

前言

  hello,大家好吖,继我们上次讲了最短路径经典算法的 Dijkstra 和 Bellman-Ford 算法之后,相信大家对于最短路径的求解方法已经有了一些概念和基础,还有点迷糊的可以顺着代码的思路去画个图或者让程序打印出每一步的中间结果,就可以很明显地看出执行过程啦~

  今天我们来讲最短路径经典算法其三—— Floyd算法。Floyd是多源多点的最短路径求解算法,之前讲的两个都是单源单点,如果要求任意一个节点A到节点B的最短路径,对于n个节点的无负环图我们需要运行n次Dijksta或者Bellman-Ford才可以得到所有可能的路径。所以今天讲的Floyd对于那种稠密图即节点很多,节点间的连接也很多的图来说效率更高,一次求解便可以得到任意节点到其他节点的最短路径。

CSDN (゜-゜)つロ 干杯

算法代码模板和原理讲解

  Floyd算法的代码模板相比其他两个简单的多,本质上就是三重循环的贪心求解过程:

for k in range(N):  # 可使用的节点集合范围0~n-1
		for i in range(N):  # 起点
			for j in range(N):  # 终点
				ad_mat[i][j] = min(ad_mat[i][j], ad_mat[i][k]+ad_mat[k][j])

  请记住上面的模板,很多题目的解都可以直接套用,复杂一点的可能需要增加一些判断条件,主体逻辑还是依旧。

  我们可以知道,节点A到节点B的最短路径无非两种情况

  1. 节点A直接到节点B最短
  2. 节点A通过一些中间节点C,D,…… 到达节点B最短

  Floyd就是去在所有可能成为中间节点的集合里去寻找是否存在使得路径更短的节点组合所以第一层循环可以理解为在所有节点组成的中间节点集合中找备选组(找备胎?? ),而后两层循环就是列举起点和终点的组合

  是不是有点穷举和贪心那味儿了~时间复杂度也很容易看出
O ( N 3 ) O(N^3) O(N3)
  

代码实现

  我们来看看针对一般性最短路径的代码实现:

def Floyd(directed=True):
	"""
		Simple implement of Floyd Algorithm

	Args:
		directed (bool, optional): [whether directed graph or not]. Defaults to True.
	"""
	N, K, s, e = list(map(int, input().split()))  # 节点数、连接数、起点终点标号
	limit = 10000
	ad_mat = [[limit for i in range(N)] for j in range(N)]  # 邻接矩阵
	for i in range(K):
		u, v, w = list(map(int, input().split()))
		ad_mat[u][v] = w
		# 无向图
		if not directed:
			ad_mat[v][u] = w
	for i in range(N):
		ad_mat[i][i] = 0

	# core
	for k in range(N):  # 可使用的节点集合范围0~n-1
		for i in range(N):  # 起点
			for j in range(N):  # 终点
				ad_mat[i][j] = min(ad_mat[i][j], ad_mat[i][k]+ad_mat[k][j])

	print(ad_mat[s][e])
	return

  基本的数据读入和处理都和之前的一致,算法核心逻辑也就四句,看懂前面的讲解之后也就很好理解了。

  我们来看下样例测试:

>>> Floyd()
7 9 0 6
0 1 9
1 2 5
2 3 2
1 3 20
3 4 14
4 5 3
3 5 8
5 6 10
6 1 7

[output]
34
Used time: 0.01447s

  可以看到运行时间基本和Bellman-Ford在一个量级,还是可以接受的。


例题讲解

  算法原理讲了,样例也讲了,总感觉有点不过瘾,我们这次来加点料,来几道真材实料的算法题看看怎么实际应用我们讲过的知识吧。


P1119 灾后重建 [ 提高+/省选-]

题目

  B地区在地震过后,所有村庄都造成了一定的损毁,而这场地震却没对公路造成什么影响。但是在村庄重建好之前,所有与未重建完成的村庄的公路均无法通车。 换句话说,只有连接着两个重建完成的村庄的公路才能通车,只能到达重建完成的村庄。

  给出B地区的村庄数N,村庄编号从0到N-1,和所有M条公路的长度,公路是双向的。 并给出第 i 个村庄重建完成的时间 t_i,你可以认为是同时开始重建并在第 t_i 天重建完成,并且在当天即可通车。若 t_i 为0则说明地震未对此地区造成损坏,一开始就可以通车。之后有Q个询问(x, y, t),对于每个询问你要回答在第t天,从村庄x到村庄y的最短路径长度为多少。如果经过若干个已重建完成的村庄无法找到从x村庄到y村庄的路径,或者村庄x或村庄y在第t天仍未重建完成 ,则需要返回 -1。

  (具体输入输出格式点击 题目链接 查看

  输入输出样例

[input]
4 5
1 2 3 4
0 2 1
2 3 1
3 1 2
2 1 4
0 3 5
4
2 0 2
0 1 2
0 1 3
0 1 4
[output]
-1
-1
5
4

思路讲解

  题目有点绕,放在最短路径(题目也有明示)的大框架下,这道题里的节点显示就是各个村庄。但是每个村庄在一定的天数之后才修建好,即各个节点的使用有时间节点。

  其实这里更加考察大家对Floyd算法本质原理的理解,我们上面提到,Floyd是去找到能够使得起点到终点路径最短的中间节点集合。 这里,村庄逐渐修好,即各个节点随着时间可以开始使用,也就是 我们可以搜索的总的中间节点集合在慢慢变大,那我们只需要随着时间增加对新开通的节点的check,本质上不会影响最后的结果,因为如果最短路径一定存在,那必定是其他所有节点(除起点终点)组成的集合的一个子集(包括空集)。 既然每次都只有一个或者若干个节点启用,我们可以实现一个利用单中间节点的Floyd函数。

代码实现细节

  首先我们处理输入数据并初始化一些中间变量:

import sys
	limit = sys.maxsize
	# 村庄数,公路数
	n, m = list(map(int, input().split()))
	# 各个村庄的重建时间
	T = list(map(int, input().split()))
	# 节点间路径矩阵
	mat = [[limit for _ in range(n + 1)] for _ in range(n + 1)]
	# 初始化
	for i in range(n+1):
		mat[i][i] = 0
	# 本题默认无向图
	for _ in range(m):
		u, v, w = list(map(int, input().split()))
		mat[u][v] = mat[v][u] = w
	# 查询数
	query = int(input())

  然后是单中间节点的Floyd函数:

# 当一个节点更新时,通过它进行全局三角降距
def update(k):
    nonlocal limit, mat
    for i in range(n+1):
    	for j in range(n+1):
    		if mat[i][j] > mat[i][k]+mat[k][j]:
    			mat[i][j] = mat[j][i] = mat[i][k]+mat[k][j]
	return

  最后是处理询问判断和结果输出:

# core
while query:
	query -= 1
	x, y, t = list(map(int, input().split()))
	# 起终点村庄有还没重建好的情况
	if T[x] > t or T[y] > t:
		print('-1')
	else:
		# 启用符合时间条件的节点并进行Floyd,输入的询问时间t是递增的
		for i in range(n):
			if T[i] <= t:
				update(i)
		if mat[x][y] == limit:  # 不连通
			print('-1')
		else:
			print(mat[x][y])

测试样例

>>> P1119()
4 5
1 2 3 4
0 2 1
2 3 1
3 1 2
2 1 4
0 3 5
4
2 0 2
0 1 2
0 1 3
0 1 4

[output]
-1
-1
5
4
Used time: 0.05462s

UVA247 Calling Circles[提高+/省选-]

题目

  如果两个人互相打电话(直接或者间接),则说他们在同一个电话圈里。例如,a打给b,b打给c,c打给d,d打给a,则这四个人在同一个圈里;如果e打给f,而f不打给e,则不能推出e和f在同一个电话圈。 输入n (n≤25) 个人的m次电话,找出所有的电话圈。人名只包含字母,不超过25个字符,且不重复。

  (具体输入输出格式点击 题目链接 查看

  输入输出样例

Input:
5 6
Ben Alexander
Alexander Dolly
Dolly Ben
Dolly Benedict
Benedict Dolly
Alexander Aaron

output:
Ben, Alexander, Dolly, Benedict
Aaron

(给大家三分钟想想有什么解题思路----di----da----di----da----di----

思路讲解:

  既然是放在最短路径来讲,啥也不用说,我啪得一下,就要把每个人当成是图里的一个节点。

  这里遇到了一个问题:我们说的是求最短路径,但是这里并不涉及距离或者时间的消耗吖?是的,但是我们可以把A可以打电话到B这件事情抽象成是有一条路连接着节点A和节点B,形成了通路。大家反过来想,找得到任意两个节点之间的最短路径,不管它是不是实际意义上的最短,不就说明这两个节点有连通吗,放在本题不就是说明两个人可以打电话吗

  而题目要求的电话圈要求A不仅要可以打给B,B也要打给A,才能说明A,B在一个圈里,真就强行入圈了。那我们只要在Floyd运行完之后去判断从A到B和从B到A是否连通即可。

  当然这里我们不需要去最优化路径,我们反而是要利用Floyd算法的多源多点特性。 我们可以给所有节点进行判断,划分电话圈,即可得出答案。比较学术的说法是,寻找闭包 or 强连通分量查找


代码实现细节

  这里题目的节点是以字符串的形式给出,我们要给它们编码,这样才能在代码里进行比较简单的访问,不必修改原来的算法形式。

# 节点数,连接数
N, K = list(map(int, input().split()))
# 存储连通信息的矩阵
ad_mat = [[0 for i in range(N + 1)] for k in range(N + 1)]
# 使用字典来映射字符串到int数字
name_dict = dict()
# 本题默认有向图
for i in range(K):
    u, v = input().split()
    # 以出现的次序来进行编码
    if u not in name_dict.keys():
    	name_dict[u] = len(name_dict)+1
    if v not in name_dict.keys():
    	name_dict[v] = len(name_dict) + 1	
    ad_mat[name_dict[u]][name_dict[v]] = 1

  然后是我们的核心算法——Floyd,这里我们需要加入判断条件以及更改更新操作(松弛过程),因为我们的最终目的是任意判断两个节点能否通过其他节点而形成通路,即可以由起点打电话给终点。

# Floyd 查找强连通分量
for k in range(1, N+1):
    for i in range(1, N + 1):
        # 如果i必须能与k通信,才能构成电话圈
        if ad_mat[i][k]:
            for j in range(1, N+1):
                if ad_mat[k][j]:
                    ad_mat[i][j] = 1
                    # ad_mat[i][j]=ad_mat[i][j]|(ad_mat[i][k]&ad_mat[k][j])

  经过Floyd之后,我们可以知道任意节点间是否连通,接下来只需要判断两个节点 i 和 j 可不可以满足以下条件即可:

(ad_mat[i][j] and ad_mat[j][i]) == True

  然后通过循环一次性把和当前节点的相连通的节点加入list就可以找到所有的电话圈了,有点抽象,我们来看看具体实现:

# 存储各个电话圈
circles = []
# 翻转dict,通过数字找人名
inv_name_dict = dict((v, u) for u, v in name_dict.items())
# 标记数组,连通一旦查找就会找出所有连通分量
vis = [0 for i in range(N+1)]
for i in range(1, N+1):
	if vis[i] == 0:
		vis[i] = 1
		# 只要还没标记,加入该环的第一个节点
		temp = [inv_name_dict[i]]
		for j in range(1, N + 1):
			# 还没被标记过且可以互打
			if vis[j] == 0 and ad_mat[i][j] and ad_mat[j][i]:
				temp.append(inv_name_dict[j])
				vis[j] = 1
		circles.append(temp)

测试样例

  这里我们只测试一组样例,原题的测试样例是由多个组成的,问题不大:

>>> Calling_circles()
5 6
Ben Alexander
Alexander Dolly
Dolly Ben
Dolly Benedict
Benedict Dolly
Alexander Aaron

Calling circles:
Ben, Alexander, Dolly, Benedict
Aaron
Used time: 0.03961s

>>> Calling_circles()
14 34
John Aaron
Aaron Benedict
Betsy John
Betsy Ringo
Ringo Dolly
Benedict Paul
John Betsy
John Aaron
Benedict George
Dolly Ringo
Paul Martha
George Ben
Alexander George
Betsy Ringo
Alexander Stephen
Martha Stephen
Benedict Alexander
Stephen Paul
Betsy Ringo
Quincy Martha
Ben Patrick
Betsy Ringo
Patrick Stephen
Paul Alexander
Patrick Ben
Stephen Quincy
Ringo Betsy
Betsy Benedict
Betsy Benedict
Betsy Benedict
Betsy Benedict
Betsy Benedict
Betsy Benedict
Quincy Martha

Calling circles:
John, Betsy, Ringo, Dolly
Aaron
Benedict
Paul, George, Martha, Ben, Alexander, Stephen, Quincy, Patrick
Used time: 0.04011s

结语

  好啦,至此我们已经把经典的三大最短路径算法讲完啦,从单源单点到多源多点,我们可以解决的问题也逐渐增加。今天也给大家比较细致的讲解了两道省选题目的解答过程,梳理的过程也是我继续进步的机会。
  十分感谢大家的阅读,如有错误,还望指出。深更不易,请勿吝赞~


WeChat

CSDN BLog

快来跟小刀一起头秃~

你可能感兴趣的:(算法小课堂,算法,python,algorithm)