スタート位置とゴール位置があり、そこに向かうルートが複数あるものとします。この中で、一番短い距離でゴールに到達する道のりを見つけ出す問題を、最短経路問題と呼びます。ダイクストラ法とは、それを見つけ出す手法の一つです。ルート検索アプリなどを作成する場合に必要となる、大変重要な手法です。
おまじないを書いておきます。
import numpy as np
import itertools as it
from graphviz import Graph
import random as rn
import time as ti
まずは、どのような問題を扱うのかを明確にするため、隣接行列を定義します。対角成分を0にすること、対称行列になることを間違えないようにしましょう。
# 隣接行列の定義
g_matrix = np.array([
[0, 1, 9, 2, 0, 0, 0, 12], # 0行目
[1, 0, 0, 0, 5, 8, 0, 0], # 1行目
[9, 0, 0, 0, 0, 2, 3, 10], # 2行目
[2, 0, 0, 0, 0, 0, 5, 0], # 3行目
[0, 5, 0, 0, 0, 1, 0, 0], # 4行目
[0, 8, 2, 0, 1, 0, 0, 6], # 5行目
[0, 0, 3, 5, 0, 0, 0, 2], # 6行目
[12, 0, 10, 0, 0, 6, 2, 0] # 7行目
])
er_flag = 0
# 対角成分チェック
for i in range(len(g_matrix)):
if g_matrix[i, i] != 0:
print("対角成分がすべて0になっていません。")
er_flag = 1
# 対称性チェック
if np.sum(g_matrix == g_matrix.T) != len(g_matrix)**2:
print("対称行列ではありません。")
er_flag = 1
# 最終評価
if er_flag == 0:
print("適切な隣接行列です。")
else:
print("不適切な隣接行列です。作り直してください。")
ノード名が数字だとわかりにくいので、下記のようにします。
ここで、$n+1$はノードの最大数です(0スタートなので、1を足します)。また、
とします。一番最初のノードからスタートし、最後のノードに到達することを目的とすることを意味します。この条件のもと、隣接行列をグラフで表現します。可視化を行う関数は以下の通りです。
def graph_view(matrix):
# ノード名生成
node_name = []
size = len(matrix)
for i in range(size):
node_name.append("s"+str(i))
# エッジを生成するノードの組み合わせ
comb_seed = np.arange(0, size)
comb = list(it.combinations(comb_seed, 2))
#print("エッジ生成ノードのリスト:", comb)
# グラフ作成
nG = Graph(format="pdf", engine="dot") # レイアウト: dot, fdp, osage, sfdp など
nG.attr("node", shape="circle", color="black", size="100")
for i in range(len(comb)):
if matrix[comb[i][0], comb[i][1]] > 0: # 距離が0以上のエッジのみ描画
nG.edge(node_name[comb[i][0]], node_name[comb[i][1]], label=str(int(g_matrix[comb[i][0], comb[i][1]])))
nG.node(node_name[0], shape="circle", color="blue", penwidth="2") # 始点
nG.node(node_name[-1], shape="circle", style="dashed", color="red", penwidth="2") # 終点
# 描画
nG.render("ngraphs")
display(nG)
関数を使用するには、隣接行列を与えればokです。
graph_view(g_matrix)
ノード(円)が場所で、エッジ(線)が移動可能であること、エッジに付与された数字が移動距離を表します。
続いて、上のグラフにおいて、ダイクストラ法により最短距離を求める方法を説明します。ダイクストラ法とは、始点に近いノードから最短距離を逐次求めていくアルゴリズムとなります。これは以下の手続きにより構成されます。用語がわからない場合は、本ページの冒頭を見直してください。
まずは、以下の表を用意します。$s_n$は終点ですから、ノードの数だけ列数があることになります。
ノード | $s_0$ | $s_1$ | ... | $s_n$ |
---|---|---|---|---|
最短距離 | xxx | xxx | ... | xxx |
暫定距離 | xxx | xxx | ... | xxx |
経路情報 | xxx | xxx | ... | xxx |
はじめに行う処理:
その後の処理:
経路を得る方法:
これを関数化したものが以下となります。
# 隣接行列、printフラグを入力 → 最短経路を出力
def dijkstra(matrix, print_flag):
t1 = ti.time()
# ノード数とノード名取得
node_name = []
size = len(g_matrix)
for i in range(size):
node_name.append("s"+str(i))
# はじめの処理
m = 1000 # 無限大的な数字
dist_temp = np.zeros(size) + m # 暫定距離
dist_opt = np.zeros(size) + m # 最短距離
dist_temp[0] = 0 # 始点のノードの暫定距離を0とする
pre_node_info = np.zeros(size) - 1
for t in range(size):
if print_flag == 1:
print("step: ", t)
# 手続き1
best_node = np.argmin(dist_temp) # 暫定距離が最も小さいものを s' に
dist_opt[best_node] = dist_temp[best_node] # s' の暫定距離を s' の最短距離に
if print_flag == 1:
print("・", node_name[best_node], "が暫定距離の中で最も小さいノードでしたので、この値を最短距離として採択しました。")
print("・(変更後)最短距離", dist_opt)
# 手続き2
if node_name[best_node] == node_name[-1]: # s' が 終点ならば終了
if print_flag == 1:
print("\n ### 最短経路が求まったので、処理を終了します ### ")
print("・最短距離", dist_opt)
print("・経路情報", pre_node_info)
### 出力 ###
route =["s"+str(size-1)]
pre_node_id = -1
while(pre_node_id != 0): # 終点から始点まで逆順に辿る
pre_node_id = int(pre_node_info[pre_node_id])
route.append("s" + str(pre_node_id))
tx = route[-1]
for i in range(2, len(route)+1):
tx = tx + " → " + route[-(i)]
print("・最短経路", tx, ",(距離 =", dist_opt[-1], ")")
t2 = ti.time()
elaps = t2-t1
print("・所要時間", np.round(elaps, 5), "秒")
break
if t == size - 1: # 反復が最大の場合も終了(何かのミスにより、無限ループを回避するため)
if print_flag == 1:
print("\n ### 最大反復回数になったので、処理を終了します ###")
break
# 手続き3
for i in range(size):
if matrix[best_node][i] > 0: # 直接接続されているノードであり、
if dist_opt[i] == m: # かつ、最短距離が無限大ならば、
# 手続き4
dist_check = dist_opt[best_node] + matrix[best_node, i] # 始点からs' への最短距離 + s'からs'と直接接続にあるノードへの距離
if dist_check < dist_temp[i]:
if print_flag == 1:
print("・", node_name[best_node],
"と直接接続されており、最短距離が記入されておらず、書き換え対象になったノードは",
node_name[i], "でした。")
dist_temp[i] = dist_check # 暫定距離を更新
pre_node_info[i] = best_node # 経路情報に s' を記載
if print_flag == 1:
print("・(変更後)暫定距離", dist_temp)
print("・(変更後)経路情報", pre_node_info, "\n")
dist_temp[best_node] = m
return [tx, elaps]
続いて、この関数を利用します。隣接行列とprint_flagを入力して使用します。計算過程を見たい場合はprint_flagを1に、見たくない場合は0にしてください。
res = dijkstra(g_matrix, print_flag = 1)
最短距離は、始点から$s_0$〜$s_7$までの最短経路の距離を意味します。最短距離となる経路は、「最短経路」に記載しています。 また、とても高速に解けることも確認できます。
ランダムにひたすら隣接行列を作って、最短経路を出すプログラムです。いくつか単純すぎる問題もありますが、自動生成しているためですので、気にしないでください。
for i in range(10):
print("\n ### Graph: ", i ," ###")
rn.seed(i)
# 隣接行列のランダム生成
size = rn.randint(10, 20)
g_matrix = np.zeros([size, size])
comb_seed = np.arange(0, size)
comb = list(it.combinations(comb_seed, 2))
for i in range(len(comb)):
sic = rn.random()
if sic < 0.20: # 20%の確率で経路生成
val = rn.randint(1, 10)
g_matrix[comb[i][0], comb[i][1]] = val # 対称行列に
g_matrix[comb[i][1], comb[i][0]] = val
else:
g_matrix[comb[i][0], comb[i][1]] = 0 # 対称行列に
g_matrix[comb[i][1], comb[i][0]] = 0
g_matrix[0, size-1], g_matrix[size-1, 0] = 10**2, 10**2 # 解なしを回避(無限大m=1000よりも小さく)
graph_view(g_matrix)
res = dijkstra(g_matrix, print_flag = 0)
ランダムにひたすら隣接行列を作って、最短経路を出すプログラムです。こちらも単純すぎる問題がいくつか含まれますが、気にせずお願いします。
for i in range(10):
print("\n ### Graph: ", i ," ###")
rn.seed(i)
# 隣接行列のランダム生成
size = rn.randint(30, 40)
g_matrix = np.zeros([size, size])
comb_seed = np.arange(0, size)
comb = list(it.combinations(comb_seed, 2))
for i in range(len(comb)):
sic = rn.random()
if sic < 0.10: # 10%の確率で経路生成
val = rn.randint(1, 10)
g_matrix[comb[i][0], comb[i][1]] = val # 対称行列に
g_matrix[comb[i][1], comb[i][0]] = val
else:
g_matrix[comb[i][0], comb[i][1]] = 0 # 対称行列に
g_matrix[comb[i][1], comb[i][0]] = 0
g_matrix[0, size-1], g_matrix[size-1, 0] = 10**2, 10**2 # 解なしを回避(無限大m=1000よりも小さく)
graph_view(g_matrix)
res = dijkstra(g_matrix, print_flag = 0)