8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[競プロ用]ダイクストラ法とベルマンフォード法まとめ

Last updated at Posted at 2021-02-21

ダイクストラ法

概略

幅優先探索ではキューを用いたところを優先度付キュー(heapq)を用いた高速化version。
考え方はこの記事がとてもよくわかる。
https://qiita.com/zk_phi/items/d93f670544e4b9816ed0
結局、辺に重みがあってもその分勝手に一本道の頂点を増やして全ての辺の重みを1に揃えてあげれば幅優先探索が使えるので、重み付きでも考え方は大きく変わらない。
BFSで頑張ってもいいが、重み次第では時間がかかるので一本道を一気に進める様にヒープを用いたダイクストラ法を使って解くといった具合。

テンプレート

dijkstra
import heapq
INF = 10 ** 9
# ----------------------------------------------------------------
# Input
#   1. タプル(重み, 行先)の二次元配列(隣接リスト)
#   2. 探索開始ノード(番号)
# Output
#   スタートから各ノードへの最小コスト
# ----------------------------------------------------------------
def dijkstra(edges: "List[List[(cost, to)]]", start_node: int) -> list:
    hq = []
    heapq.heapify(hq)
    # Set start info
    dist = [INF] * len(edges)
    heapq.heappush(hq, (0, start_node))
    dist[start_node] = 0
    # dijkstra
    while hq:
        # ------------------------------------------- タイミング1
        min_cost, now = heapq.heappop(hq)
        # ------------------------------------------- タイミング2
        if min_cost > dist[now]:
            continue
        for cost, next in edges[now]:
            if dist[next] > dist[now] + cost:
                dist[next] = dist[now] + cost
                heapq.heappush(hq, (dist[next], next))
            # --------------------------------------- タイミング3
    return dist

実際の動き

テンプレートの上記1~3の各タイミングでのグラフとキューの状態を以下グラフを題材にダイクストラ法の動きを見ていく。始点を0、終点を5として最短経路を導き出す。

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

計算量

heapqの出し入れ : 1件につきlog(n)
→ popは頂点分で最大n回、pushは辺の分で最大e回なのでO((n+e)logn)
このため、e= n^2の完全グラフでは効率が悪くなる。

https://www-ui.is.s.u-tokyo.ac.jp/~takeo/course/2007/algorithm/dijkstra.pdf
https://www.ioi-jp.org/joi/2007/2008-yo-prob_and_sol/2008-yo-t6/review/2008-yo-t6-review.html

ベルマンフォード法

概略

ダイクストラは負の重みがある辺が存在する場合には利用できない。
ベルマンフォード法では、その時点で確定しているノードのコストを正としてそれぞれのノードから移動可能なノードのコストを決めていく。
当然、既に評価したノードのコストを更新したことでそのノードを正として算出したノードのコストを再計算しないといけないケースが出てくる。
そのため、ベルマンフォード法では全頂点からスタートを除いた(V-1)回まで、更新がある限りこの一連の処理を繰り返す。
ただし、負閉路を含む場合、この更新は無限に終わらない。そのため、(V-1)回の更新に到達しても更新が終わらない場合は負閉路が含まれると判断できる。

テンプレート

bellmanFord
INF = 10 ** 16
# ----------------------------------------------------------------
# Input
#   1. 辺のリスト: タプル(始点, 終点, 重み)
#   2. 頂点数
#   3. 探索開始ノード(番号)
# Output
#   リスト(スタートから各ノードへの最小コスト。ただし負閉路から到達可能なノードは-INF)
# Note
#   始点から到達不可能な負閉路の有無: × (ベルマンフォード法では発見されない)
#   始点から到達可能だが終点に影響ない負閉路の有無: ○ (出力のリストに-INFが含まれるかどうか)
#   始点から到達可能で終点に到達する負閉路の有無: ○ (出力のリストの終点へのコストが-INFかどうか)
# ----------------------------------------------------------------
def bellmanFord(edges: "List[(from, to, to)]", vertex: int, start_node: int) -> list:
    # Initialize
    costs = [INF] * vertex
    costs[start_node] = 0
    for i in range(vertex * 2):         # ---- 外側のLoop
        for f, t, c in edges:           # ---- 内側のLoop
            # ------------------------------------------- タイミング1
            if costs[f] != INF and costs[t] > costs[f] + c:
                if i >= vertex - 1:
                    costs[t] = -INF
                else:
                    costs[t] = costs[f] + c
    return costs

実際の動き

■ 負閉路なしケース

同じく以下の題材において、上記テンプレートの外側ループと内側ループそれぞれのタイミング1のグラフの状態を示す。なお、edgesには辺のコストが小さいものから順に入っていたと仮定する(この順序はなんでもよい)。

image.png

image.png

image.png

image.png

最大でも頂点数分ループを回せば全ての頂点のコストが確定する。

■ 負閉路あり(終点に到達する)ケース

負の辺を含む場合にベルマンフォード法が有効なため、題材を以下に変えてみる。

image.png

image.png

image.png

image.png

image.png

ただし、負閉路があると答えは必ず-∞かというとそうでもない。次の例をみる。

■ 負閉路あり(終点に到達しない)ケース

image.png

この場合、負閉路があるもののゴールに繋がらないため影響しない。即ち、ゴールの値はループを回し続けても変化しない。このことを利用して、以下のように頂点数を超えても終点の更新があるかで判断できそうである。

{頂点数}回更新した後にゴールが更新される    = 負閉路がある
            〃               されない  = 負閉路があったとしても影響しない

しかし、これは次のような例では成立しない。次の例をみる。

■ 負閉路あり(コストが莫大な辺がある)ケース

image.png

負閉路を1周回る毎にコストが -3 になるので、いつかは終点の値を更新するだろうが、それは頂点数6回だけ回しても訪れないため判断を誤る。

そのため、前述のコードは頂点数の2倍分ループを回し、以下の処理が入っている。

if i >= vertex - 1:
    costs[t] = -INF

{頂点数}回を超えてからのループでは更新があったらそこを-∞に書き換える。
この負閉路から到達可能な箇所を-∞で書き換える処理を同じく{頂点数}回行うと正しい影響範囲がわかる

image.png

image.png

image.png

計算量

O(VE)。

例題

ダイクストラ法

  1. Single Source Shortest Path
  2. 第7回日本情報オリンピック 予選 F - 船旅
  3. ABC079 D - Wall

1. Single Source Shortest Path

import heapq
INF = 10 ** 9

def main():
    V, E, r = map(int, input().split())
    edge = [[] for _ in range(V)]
    costs = []
    for _ in range(E):
        s, t, d = map(int, input().split())
        edge[s].append((d, t))

    dist = dijkstra(edge, r)
    for c in dist:
        print("INF" if c == INF else c)

if __name__ == '__main__':
    main()

2. 第7回日本情報オリンピック 予選 F - 船旅

import heapq
INF = 10 ** 9

def main():
    n, k = map(int, input().split())
    edge = [[] for _ in range(n)]
    for _ in range(k):
        i = list(map(int, input().split()))
        if i[0] == 1:
            edge[i[1] - 1].append((i[3], i[2] - 1))
            edge[i[2] - 1].append((i[3], i[1] - 1))
        else:
            costs = dijkstra(edge ,i[1] - 1)
            print(-1 if costs[i[2] - 1] == INF else costs[i[2] - 1])
    return
    
if __name__ == '__main__':
    main()

3. ABC079 D - Wall

import heapq
INF = 10 ** 9
def main():
    H, W = map(int, input().split())
    edges = [[] for i in range(10)]
    for i in range(10):
        c = list(map(int, input().split()))
        for j in range(10):
            if i == j:
                continue
            edges[i].append((c[j], j))
    nums = [0] * 10
    for _ in range(H):
        A = list(map(int, input().split()))
        for a in A:
            if a == -1:
                continue
            nums[a] += 1
    ans = 0
    for i, s in enumerate(nums):
        if i == 1:
            continue
        dist = dijkstra(edges, i)
        ans += dist[1] * s
    print(ans)

if __name__ == '__main__':
    main()

ベルマンフォード法

  1. Single Source Shortest Path (Negative Edges)
  2. ABC061 D - Score Attack
  3. ABC137 E - Coins Respawn

1. Single Source Shortest Path (Negative Edges)

INF = 10 ** 9

def main():    
    V, E, r = map(int, input().split())
    edges = []
    for _ in range(E):
        s, t, d = map(int, input().split())
        edges.append((s, t, d))
    costs = bellmanFord(edges, V, r)
    
    if -INF in costs:
        print("NEGATIVE CYCLE")
        return
    
    for c in costs:
        print("INF" if c == INF else c)

if __name__ == '__main__':
    main()

2. ABC061 D - Score Attack

INF = 10 ** 16

def main():
    N, M = map(int, input().split())
    edges = []
    costs = []
    for _ in range(M):
        a, b, c = map(int, input().split())
        edges.append((a - 1, b - 1, -c))
 
    costs = bellmanFord(edges, N, 0)
 
    if costs[-1] == -INF:
        print("inf")
        return
 
    print(-costs[N - 1])
    return
 
if __name__ == '__main__':
    main()

3. ABC137 E - Coins Respawn

最大値を求める問題でもコストを全てマイナスにして実施することで対応できる。

def main():
    N, M, P = map(int, input().split())
    edges = []
    for _ in range(M):
        a, b, c = map(int, input().split())
        edges.append((a - 1, b - 1, -(c - P)))
 
    costs = bellmanFord(edges, N, 0)
    if costs is None or costs[-1] == -INF:
        print(-1)
        return
 
    print(0 if costs[N - 1] > 0 else -costs[N - 1])
    return
 
if __name__ == '__main__':
    main()

参考

ダイクストラ法

ベルマンフォード法

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?