ABC 079 D - Wall
ABC 079 D - Wall に関連する、ワーシャルフロイドについてのメモを残します。
参考
- 「プログラミングコンテスト攻略のためのアルゴリズムとデータ構造」
わからん
ワーシャルフロイドの実装は数行で済むが、どうしてそのコードで最短距離を求められるのか。
この記事では、確かに最短距離を求められそうだな、と納得できることを目標とする。
具体例として、下に示すグラフについて全点対間最短経路問題を考えてみる。
便宜上、丸の中の数字は、頂点番号を表すことにする。
ソースコードは以下の通り。
#include <bits/stdc++.h> using namespace std; int main() { int INF = 1000000000; // A[i, j] := 最終的には始点 i と終点 j の最短経路の距離 // 初期値は、始点 i と終点 j を結ぶ辺の重み (長さ) // 始点 i と終点 j を結ぶ辺がない場合は INF vector<vector<int>> A = { { 0, 5, 100, INF, INF }, { 5, 0, INF, 5, 50 }, { 100, INF, 0, INF, 10 }, { INF, 5, INF, 0, 5 }, { INF, 50, 10, 5, 0 } }; // ワーシャルフロイド // 始点を i、終点を j とする。k は経由する頂点 for (int k = 0; k < 5; k++) { for (int i = 0; i < 5; i++) { for (int j = 0; j < 5; j++) { A[i][j] = min(A[i][j], A[i][k] + A[k][j]); } } } // 出力 for (int i = 0; i < 5; i++) { for (int j = 0; j < 5; j++) { cout << i << " -> " << j << " : " << A[i][j] << endl; } } }
上記ソースコードの のループを追いかけてみる。
のループでは、始点が 、終点が の経路について、
- (0 を経由しない)
- (0 を経由する)
このうち小さい方を新たに の値とする。
前者は → 、後者は → 0 → という経路である。 → と表記するとき、 から へ向かう途中に経由した頂点はないものとする。
たとえば、始点が 1、終点が 2 の経路ならば、
- (0 を経由しない、1 → 2 という経路)
- (0 を経由する、1 → 0 → 2 という経路)
この 2 経路を比べ、小さい方である 105 を の値とする。
後の は、始点 、終点 の最短経路の距離を表している。ただし、端点である 以外に経由してよい頂点は 0 のみである。
遡ると、 前の も始点 、終点 の最短経路の距離を表すが、やはり制約付きで、端点である 以外に経由してよい頂点はない。
以降
のループでは、始点が 、終点が の経路について、
- (1 を経由しない)
- (1 を経由する)
このうち小さい方を新たに の値とする。
各部分路は 0 を経由するかどうかで 2 通り考えられるが、すでに のループで最適な経路が選択されている。
たとえば、始点が 3、終点が 2 の経路ならば、
- (1 を経由しない)
- (1 を経由する)
このうち小さい方である 110 を の値とすればよい。
ここで 、 は以下の通り、 のループで、経由できる頂点は 0 のみの制約の下で最適な経路が選択されている。
- 始点が 3、終点が 1 の経路は、0 を経由しない方が距離は短い (3 → 1)
- 始点が 1、終点が 2 の経路は、0 を経由した方が距離は短い (1 → 0 → 2)
以降、経由してよい頂点を 1 個増やすごとに最短経路の距離を更新していく。
下の表は、各ループ後の頂点間の距離を表し、 後の列が全点対間最短経路問題の答えとなる。
一般化
頂点 0, 1, 2, ..., のみ経由できるとき、始点 、終点 の最短経路の距離を とし、その経路の 1 つを とする。
ここで、 を利用して を求めることを考える。
始点が 、終点が の経路について、
- を経由しないとき、頂点 0, 1, 2, ..., のみ経由できるので、最短経路は と等しく、
- を経由するとき、最短経路は と の 2 つの部分路から構成され、
この 2 経路を比べ、小さい方を の値とすればよい。
このように、 により が決定されることがわかる。
初期値を とすると、確かに のループでは をもとに を求め、 のループでは をもとに を求めていた。
モヤモヤするのは、 のように、最短経路の距離の保持には 3 次元配列 が必要と思うが、上記ソースコードでは 2 次元配列 で実装している点。なぜ問題ないのか。
2 次元で足りる
上記ソースコードについて検討する。
同じ のループ内において、 が最初で が最後に計算されていた。
たとえば を求めるときは、
- (1 を経由しない)
- (1 を経由する)
この 2 つを比べるとよいが、これよりも前に や を求める計算は終わっていて、すでに 、 は上書きされてしまっている。
このように、 を求めるとき、 や がすでに 、 に上書きされていることはありうる。
上書きされていては を求めることができないのではないか、とモヤモヤする。
しかし、
であるため、上書きされることには実は問題がないことがわかる。
そのため、最短経路の距離の保持には 2 次元配列 で足りる。
ABC 079 D - Wall
- 問題 : D - Wall
数字 を数字 に変えるのに必要な魔力が与えられる。数字 を 1 に変えるのに必要な魔力をワーシャルフロイド法により求める。
#include <bits/stdc++.h> using namespace std; int main() { int H, W; cin >> H >> W; vector<vector<int>> c(10, vector<int>(10)); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) cin >> c[i][j]; } vector<vector<int>> A(H, vector<int>(W)); for (int i = 0; i < H; i++) { for (int j = 0; j < W; j++) cin >> A[i][j]; } for (int k = 0; k < 10; k++) { for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { c[i][j] = min(c[i][j], c[i][k] + c[k][j]); } } } int ans = 0; for (int i = 0; i < H; i++) { for (int j = 0; j < W; j++) { if (A[i][j] != -1) ans += c[A[i][j]][1]; } } cout << ans << endl; }