加权图与狄克斯特拉算法|Python实现

丶灬走出姿态 提交于 2020-02-14 20:16:21

在前面,我们讨论了图与广度优先搜索,解决了一个“找朋友”的问题。现在,让我们想一想,我们把问题换成了“找地方”——我们想从一个地方到达另一个地方,也按照最短路径到达——这样的问题中,我们使用图来建立模型时,会发现连接——也就是两个地方之间的路——是有权重,或者说长度的。这种情况下,广度优先搜索就不起作用了——它只能保证我们找到走过最少的连接数,而没有考虑这些连接有长度的情况。下面我们来考虑这个问题。


1. 两个术语

(1).加权图

就像上面所说,广度优先搜索适用于“连接没有长度值”的图,我们称之为非加权图。而连接存在长度值时,我们使用狄克斯特拉算法,此时的图称为加权图,连接的长度值称为权重。比如:

6
1
2
5
3
起点
A
终点
B
(2).有向无环图

之前我们介绍过有向图和无向图,其实无向图就是一个环:

起点
终点

也就是无向意味着两个节点相互指向对方。

当环这个结构存在于图中,并且终点不在环中时,环只会徒劳的增加权重。我们要讨论的狄克斯特拉算法适用于有向无环图(DAG)。


2. 狄克斯特拉算法

前面示例展示的那个图过于简单,无法体现这个算法的所有细节。所以,这里我们搭建一个复杂一点的图,并且解释这个图的现实意义——这会让我们发现狄克斯特拉算法的用处之大。

5
0
15
20
30
35
20
10
乐谱
唱片
海报
吉他
钢琴

这里,节点的意思很明确:某件乐器;而连接上的权重指的是用前者换取后者时我们需要加的钱。比如,用乐谱换唱片时我们需要另外支付5元,而换海报就不需要。狄克斯特拉算法能帮助我们快速找到如何花最少的钱,用乐谱换一架钢琴。

在我们开始之前,我们先建立一张表格:

父节点 节点 开销
乐谱 唱片 5
乐谱 海报 0
吉他
钢琴

这张表记录了从乐谱到其他乐器所花的钱。由于我们刚开始,只能准确的写出乐谱的邻居——海报和唱片的信息,所以表格存在空白。

我们在表格的开销中找到最小值。由于所有的权重都是正数,所以现在表格中的最小值一定就是所有路径中的最小值!这个最小值是换取海报所花的钱:0元。

最小值连接起来的节点是海报,所以我们计算从乐谱到海报的邻居所需的花费。我们的表格变成了

父节点 节点 开销
乐谱 唱片 5
乐谱 海报 0
海报 吉他 30
海报 35
钢琴

由于我们确定了乐谱到海报最少花费0元,所以我们不去管他。也就是说,我们不去考虑处理过的节点。所以,当前最小开销是乐谱到唱片,我们去看看唱片的邻居,然后更新表格:

父节点 节点 开销
乐谱 唱片 5
乐谱 海报 0
唱片 吉他 20
唱片 25
钢琴

重复上面的步骤,现在最小开销是20,我们的表更新为

父节点 节点 开销
乐谱 唱片 5
乐谱 海报 0
唱片 吉他 20
唱片 25
吉他 钢琴 40

现在最小开销是25,继续更新表格:

父节点 节点 开销
乐谱 唱片 5
乐谱 海报 0
唱片 吉他 20
唱片 25
钢琴 35

除了钢琴之外我们没有其他节点了——这意味着我们找到了最便宜的价格,我们只要画35元就能在上面的交易关系中用乐谱获得宜家钢琴,这是狄克斯特拉帮我们找到的最便宜的价格。

负权重

狄克斯特拉算法是不是适用于一切有向无环图呢?其实我们在说明这个算法的过程中做出了一点要求:没有负权重。这是因为我们在表格中检查完一个最小值后,就不在对其进行考虑了。但是负权重的出现要求我们考虑这些内容。

在包含负权重的图张寻找最短路径使用的算法是贝尔曼-福德算法,而我们的狄克斯特拉算法只适用于正权重的有向无环图。

3. 实现

我们先来建立整个加权图的模型,为了表示权重,我们在散列表中建立一些散列表:

trading_networks = {}

trading_networks['乐谱'] = {}
trading_networks['乐谱']['唱片'] = 5
trading_networks['乐谱']['海报'] = 0

trading_networks['唱片'] = {}
trading_networks['唱片']['吉他'] = 15
trading_networks['唱片']['鼓'] = 20 

trading_networks['海报'] = {}
trading_networks['海报']['吉他'] = 30
trading_networks['海报']['鼓'] = 35

trading_networks['吉他'] = {'钢琴':20}

trading_networks['鼓'] = {'钢琴':10}

trading_networks['钢琴'] = {}

这个散列表的具体结构:

{
'乐谱': {'唱片': 5, 
		'海报': 0}, 
		
'唱片': {'吉他': 15, 
		'鼓': 20},
		
'海报': {'吉他': 30, 
		'鼓': 35},
		
'吉他': {'钢琴': 20}, 

'鼓': {'钢琴': 10}},

'钢琴':{}
}

我们可以很快的得到两个节点之间的权重。

然后,我们建立一个开销表并初始化。所谓开销表,就是从起点到图中某个节点所需的权重和。对于无法确定的开销值,我们设定为无穷大:

inf = float('inf')

costs = {}

costs['唱片'] = 5
costs['海报'] = 0
costs['吉他'] = inf
costs['鼓'] = inf
costs['钢琴'] = inf

同时我们还需要一个记录父节点的散列表以及记录处理过的节点的列表:

parents = {}

parents['唱片'] = '乐谱'
parents['海报'] = '乐谱'
parents['吉他'] = None
parents['鼓'] = None
parents['钢琴'] = None

processded = []

完成准备工作后,我们来实现算法:

def get_exchange_path(parents,end):
	'给出交换路径。'

	exchange_path = []

	try:
		while True:
			exchange_path.append(end)
			end = parents[end]
	except:
		pass	

	exchange_path.reverse()

	return exchange_path

def find_lowest_cost_node(costs,processded):
	'寻找当前未处理的最小开销节点。'

	lowest_cost = float('inf')
	lowest_cost_node = None
	for node in costs:
		cost = costs[node]
		if cost < lowest_cost and node not in processded:
			lowest_cost = cost
			lowest_cost_node = node
	
	return lowest_cost_node

def dickstra_algorithm(trading_networks=trading_networks,\
						costs=costs,\
						parents=parents,\
						processded=processded,\
						end='钢琴'):
	
	node = find_lowest_cost_node(costs,processded)
		
	while node is not None:
		cost = costs[node]
		neighbors = trading_networks[node]

		for n in neighbors.keys():
			new_cost = cost + neighbors[n]

			if costs[n] > new_cost:
				costs[n] = new_cost
				parents[n] = node

		processded.append(node)
		node = find_lowest_cost_node(costs,processded)

	print('交换最小花费:{}'.format(cost))

	path = get_exchange_path(parents,end)

	string = '交换顺序:'
	for item in path:
		string = string + str(item) + ' '
	print(string)

在我们的模型上运行这些函数,可以看到结果

交换最小花费:35
交换顺序:乐谱 唱片 鼓 钢琴 

终于,我们可以给出顺序了。


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!