最短路算法算是基礎算法, 我還是總是忘。。維基有個動圖很好,比較直觀,可是還不夠友好,于是自己做了點筆記,僅供參考。網上關于Dijkstra的文章也不少,適合的才是最好的。
動態圖 - 來自wiki
-
頂點為城市,頂點間連線表示城市相連通,線上數字為城市間距離。
城市間距離 目標為找出從序號為1的城市到序號為5的城市的最短路徑。
該路徑可以由 已知最短路徑 迭代求出。
每次從已知最短路徑中選出沒探索過且最短的,從該路徑終點出發,探索新的最短路徑。
-
一開始,從出發點到達它之外的所有頂點的已知最短路徑為無窮大,到出發點自己的最短路徑為0。
-
現在,出發點的已知最短路徑最小,則從出發點出發,探索與其直連的所有頂點,如果路徑長度比到該頂點的已知最短路徑小,則刷新該頂點的已知最短路徑。
-
接著,出發點已經探索過了,從未出發探索過的已知最短路徑中選出最小的一個,即從城市2出發,探索與其直連的城市,如果到達該城市的路徑長度比已知最短路徑小,則刷新最短路徑。可以看到,從城市2到3的路徑總長17>城市3目前的最短路徑9,不滿足條件,不刷新城市3的最短路徑,而到城市4的已知最短路徑刷新為7+15=21。(已知最短路徑的計算都是從出發點開始)
依次類推,直到我們遇到目的地是已知最短路徑里未探索的所有頂點中最短的一個時,終止探索。
值得注意的是,我們每一步探索過程中的當前出發點的最短路徑是確定的了,不會再變了,因為它是所有未探索過的已知最短路徑中的最小的了,所以不存在從其它地方再加一段路程到達它還會比它更小的情況。
剛看動態圖可能跟不上,我們用工具將gif轉為png一幀幀過一遍就好理解了, 圖在后面,先看代碼。
簡單代碼
- 由維基百科里的偽代碼,可以比較簡單地轉換為python代碼。
V = [ # 頂點列表
1, 2, 3, 4, 5, 6]
w = [ # 城市間距離, -1表示無窮大,與圖中一致
[ 0, 7, 9, -1, -1, 14],
[ 7, 0, 10, 15, -1, -1],
[ 9, 10, 0, 11, -1, 2],
[-1, 15, 11, 0, 6, -1],
[-1, -1, -1, 6, 0, 9],
[14, -1, 2, -1, 9, 0]]
def Dijkstra(V=V, w=w, s=1, dest=5):
d = [-1 if v != s else 0 for v in V] # 出發點到各點的已知最短路徑的初始值, 到自身為0
N = len(V) # 頂點數
p = [None] * N # 出發點到各點的已知最短路徑的前一個點
S = set() # 這個集合暫時沒什么用處
Q = set(range(N)) # 生成所有頂點的虛擬序號的集合, 以0起始
def Extract_Min(Q): # 將頂點集合Q中有最小d[u]值的頂點u從Q中刪除并返回u,可先不管
u_min = None
for u in Q:
if d[u] != -1: # 無窮大則跳過
u_min = u
for u in Q:
if d[u] == -1: # 無窮大則跳過
continue
if d[u_min] > d[u]:
u_min = u
Q.remove(u_min)
return u_min
v_d = V.index(dest) # 目的地的虛擬序號
while Q : # 當Q非空
u = Extract_Min(Q) # 將頂點集合Q中有最小d[u]值的頂點u從Q中刪除并返回u
if u == v_d : break # 可以在找到目的地之后立即終止,也可繼續查找完所有最短路徑
S.add(u)
v_reached_from_u = [i for i in range(N) if w[u][i] != -1] # u能到達的頂點, 這里沒有去除在S中的頂點
for v in v_reached_from_u:
if d[v] > d[u] + w[u][v] or d[v] == -1: # 如果經過u再到v的距離更短
d[v] = d[u] + w[u][v] # 用該值更新已知最短路徑
p[v] = u # 則已知到v的最短路徑上的倒數第二個點為u
print('d=', d)
v = v_d # 由p列表可以回溯出最短路徑
final_route = []
while v != V.index(s):
final_route.insert(0, str(V[v]))
v = p[v]
final_route.insert(0, str(V[v]))
print('最終路徑', '-->'.join(final_route))
Dijkstra()
#d= [0, 7, 9, -1, -1, 14]
#d= [0, 7, 9, 22, -1, 14]
#d= [0, 7, 9, 20, -1, 11]
#d= [0, 7, 9, 20, 20, 11]
#最終路徑 1-->3-->6-->5
使用堆
關鍵思路中從已知最短路徑中選擇最小的一個,還要增加(更新)新的值,讓人想到堆結構。
-
利用python的堆結構heapq,可以很簡單地獲取到最小的已知最短路徑。
我們每次從堆中取出已知最短路徑中最短的,將從該點探索下的所有新路徑直接放入堆中,不用管重復,堆會返回有最小路徑的,只要我們額外使用一個集合來記錄已經探索過的頂點即可,使每個頂點只探索一次。
先看沒有記錄路徑只算長度的,代碼相對上面簡潔了不少。
from heapq import heappop, heappush
V = [ # 頂點列表
1, 2, 3, 4, 5, 6]
w = [ # 城市間距離, -1表示無窮大
[ 0, 7, 9, -1, -1, 14],
[ 7, 0, 10, 15, -1, -1],
[ 9, 10, 0, 11, -1, 2],
[-1, 15, 11, 0, 6, -1],
[-1, -1, -1, 6, 0, 9],
[14, -1, 2, -1, 9, 0]]
def Dijkstra(V=V, w=w, s=1, dest=5):
N = len(V) # 頂點數
S = set()
Q = [(0, V.index(s))] # 路徑長,城市虛擬序號
v_d = V.index(dest)
while Q : # 當Q非空
d, u = heappop(Q)
if u == v_d :
print(d)
break # 可以在找到目的地之后立即終止,也可繼續查找完所有最短路徑
# 如果到目的地的距離已經是當前所有距離中最短的,那不可能會有更短的,所以退出
if u not in S:
S.add(u)
v_reached_from_u = [i for i in range(N) if w[u][i] != -1] # u能到達的頂點
for v in v_reached_from_u:
if v not in S:
heappush(Q, ((d + w[u][v]), v)) # 到頂點v的某條路徑的距離
Dijkstra() # 20
- 要記錄路徑,只需要在放入堆中的每個小元組尾部再加上個記號,改動很少
from heapq import heappop, heappush
V = [ # 頂點列表
1, 2, 3, 4, 5, 6]
w = [ # 城市間距離, -1表示無窮大
[ 0, 7, 9, -1, -1, 14],
[ 7, 0, 10, 15, -1, -1],
[ 9, 10, 0, 11, -1, 2],
[-1, 15, 11, 0, 6, -1],
[-1, -1, -1, 6, 0, 9],
[14, -1, 2, -1, 9, 0]]
def Dijkstra(V=V, w=w, s=1, dest=5):
N = len(V) # 頂點數
S = set()
Q = [(0, V.index(s), str(s))] # 路徑長, 序號, 路徑
v_d = V.index(dest)
while Q : # 當Q非空
d, u, p = heappop(Q)
if u == v_d :
print(d, p)
break # 可以在找到目的地之后立即終止,也可繼續查找完所有最短路徑
# 如果到目的地的距離已經是當前所有距離中最短的,那不可能會有更短的,所以退出
if u not in S:
S.add(u)
v_reached_from_u = [i for i in range(N) if w[u][i] != -1] # u能到達的頂點
for v in v_reached_from_u:
if v not in S:
heappush(Q,(
(d + w[u][v]), v, ''.join((p,'->',str(V[v])))
)) # 到頂點v的某條路徑的距離
Dijkstra() # 20 1->3->6->5
分解圖 - 使用文末工具
工具
- 在網上找到的效果最好的從gif到png的轉化代碼
# https://gist.github.com/BigglesZX/4016539
import os
from PIL import Image
'''
I searched high and low for solutions to the "extract animated GIF frames in Python"
problem, and after much trial and error came up with the following solution based
on several partial examples around the web (mostly Stack Overflow).
There are two pitfalls that aren't often mentioned when dealing with animated GIFs -
firstly that some files feature per-frame local palettes while some have one global
palette for all frames, and secondly that some GIFs replace the entire image with
each new frame ('full' mode in the code below), and some only update a specific
region ('partial').
This code deals with both those cases by examining the palette and redraw
instructions of each frame. In the latter case this requires a preliminary (usually
partial) iteration of the frames before processing, since the redraw mode needs to
be consistently applied across all frames. I found a couple of examples of
partial-mode GIFs containing the occasional full-frame redraw, which would result
in bad renders of those frames if the mode assessment was only done on a
single-frame basis.
Nov 2012
'''
def analyseImage(path):
'''
Pre-process pass over the image to determine the mode (full or additive).
Necessary as assessing single frames isn't reliable. Need to know the mode
before processing all frames.
'''
im = Image.open(path)
results = {
'size': im.size,
'mode': 'full',
}
try:
while True:
if im.tile:
tile = im.tile[0]
update_region = tile[1]
update_region_dimensions = update_region[2:]
if update_region_dimensions != im.size:
results['mode'] = 'partial'
break
im.seek(im.tell() + 1)
except EOFError:
pass
return results
def processImage(path):
'''
Iterate the GIF, extracting each frame.
'''
mode = analyseImage(path)['mode']
im = Image.open(path)
i = 0
p = im.getpalette()
last_frame = im.convert('RGBA')
try:
while True:
print("saving %s (%s) frame %d, %s %s" % (path, mode, i, im.size, im.tile))
'''
If the GIF uses local colour tables, each frame will have its own palette.
If not, we need to apply the global palette to the new frame.
'''
if not im.getpalette():
im.putpalette(p)
new_frame = Image.new('RGBA', im.size)
'''
Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
If so, we need to construct the new frame by pasting it on top of the preceding frames.
'''
if mode == 'partial':
new_frame.paste(last_frame)
new_frame.paste(im, (0,0), im.convert('RGBA'))
new_frame.save('%s-%d.png' % (''.join(os.path.basename(path).split('.')[:-1]), i), 'PNG')
i += 1
last_frame = new_frame
im.seek(im.tell() + 1)
except EOFError:
pass
# 使用
import os
os.chdir(r'E:\python\2017_3_1\dijkst\png')
processImage('E:/python/2017_3_1/dijkst/Dijkstra_Animation.gif')