bpeldi2oerkd8の開発日誌

とあるひよっこエンジニアの成長の記録。

ワーシャルフロイド法について

またまたAtCoderの記事です。
Web開発の記事は次回の予定です。

ちなみに前回のABCの結果はこちらです。
developer-bpeldi2oerkd8.hatenablog.com

ワーシャルフロイド法とは

全頂点の最小の重みの和(最短経路など)を求めるときに使います。
ダイクストラ法と違い、重みに負の値があるときも計算可能です。
計算量はO(V^3)なので、時間はかかります。

ただし、負の閉路(重みの和が負となる閉路)があるときは正しい答えを出せません。
しかし、負の閉路があることの検出は可能です。

説明についてはこちらのサイトがわかりやすいかなと思います。
triple-four.hatenablog.com

どのように解くのか(Javaの場合)

ライブラリを作って以下のサイトに上げました。
github.com

使い方は以下のような感じです。

//重さを格納する配列
long[][] w = new long[N][N];

//大きい値の設定
long INF = 10000000L;

//ワーシャルフロイド法用のインスタンスを作成
WF wf = new WF(c, INF);

//各重みを設定
for(int i=0; i<N; i++) {
	for(int j=0; j<N; j++) {
		c[i][j] = sc.nextLong();
	}
}

//すべての頂点間の最小の重みの和(最短距離)を返す
long[][] min_w = wf.getAllDis();

//負の閉路検出(必ずgetAllDisの後で行う)
boolean isNegativeCycle = wf.isNegativeCycle();

作ったライブラリを使って2問ほど解いてみました。

1つ目は、AtCoder Beginner Contest 079 D - Wall です。
atcoder.jp


この問題はワーシャルフロイド法を使うことさえわかってしまえば、あとは1と各数字との最小の重みの和を求めるだけです。

このように、最短経路でないときも使えるんですね。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
	//java11

	public static void main(String[] args) {

		Scanner sc = new Scanner(System.in);

		int H = sc.nextInt();
		int W = sc.nextInt();

		long[][] c = new long[10][10];
		long INF = 10000000;
		WF wf = new WF(c, INF);
		for(int i=0; i<10; i++) {
			for(int j=0; j<10; j++) {
				c[i][j] = sc.nextLong();
			}
		}
		long[][] min_c = wf.getAllDis();

		long min_maryoku = 0;
		int[][] A = new int[H][W];
		for(int i=0; i<H; i++) {
			for(int j=0; j<W; j++) {
				A[i][j] = sc.nextInt();

				if(A[i][j] != -1 && A[i][j] != 1) {
					min_maryoku += min_c[A[i][j]][1];
				}
			}
		}

		System.out.println(min_maryoku);

	}

	//ワーシャルフロイド法
	public static class WF{
		long[][] d;
		long INF;

		public WF(long[][] d, long INF) {
			this.d = d;
			this.INF = INF;

			//初期化
			for(int i=0; i<d.length; i++) {
				Arrays.fill(d[i], INF);
			}

			//自分自身との距離を0に設定
			for(int i=0; i<d.length; i++) {
				d[i][i] = 0;
			}
		}

		//全頂点間の最短距離(重みに負の値あり)(ワーシャルフロイド法)(O(V^3))
		public long[][] getAllDis(){

			long[][] new_d = new long[d.length][d.length];
			System.arraycopy(d, 0, new_d, 0, d.length);
			for(int k=0; k<new_d.length; k++) {
				for(int i=0; i<new_d.length; i++) {
					for(int j=0; j<new_d.length; j++) {
						if(new_d[i][k] != INF && new_d[k][j] != INF) {
							new_d[i][j] = Math.min(new_d[i][j], new_d[i][k]+new_d[k][j]);
						}
					}
				}
			}

			return new_d;
		}

		//負の閉路検出(O(V))
		public boolean isNegativeCycle() {
			for(int i=0; i<d.length; i++) {
				if(d[i][i] < 0) {
					return true;
				}
			}

			return false;
		}
	}
}

2つ目は、AtCoder Beginner Contest 074 D - Restoring Road Network です。
atcoder.jp


この問題は水色diffです。
水色レベルになってくると、考察が必要になってきます。

各頂点間の最短距離がすでに与えられているので、
ワーシャルフロイド法を使って、
他の頂点を経由した方が距離が短くなる場合は-1、
別の経由地を使って最短距離と同じになる場合はその2点間のエッジを切ればよいです。

この解答ではグラフを使わず、あらかじめ与えられる最短距離をすべて加え、
エッジを切る際は代わりにその2点間の最短距離を引きます。

この時注意するのは、エッジを切った情報を記録しておかないと2種類以上最短経路がある場合種類の数だけ引いてしまうので、エッジを切った情報を記録するようにする点です。
(ここでWAを出しました。)

最終的な解答はこちらです。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
	//java11

	public static void main(String[] args) {

		Scanner sc = new Scanner(System.in);

		int N = sc.nextInt();

		long[][] A = new long[N][N];
		long ans = 0;
		for(int i=0; i<N; i++) {
			for(int j=0; j<N; j++) {
				A[i][j] = sc.nextLong();
				ans += A[i][j];
			}
		}

		//2点間がつながっているか
		boolean[][] connected = new boolean[N][N];
		for(int i=0; i<N; i++) {
			Arrays.fill(connected[i], true);
		}
		for(int i=0; i<N; i++) {
			connected[i][i] = false;
		}

		for(int k=0; k<N; k++) {
			for(int i=0; i<N; i++) {
				for(int j=0; j<N; j++) {
					//別の経由地を通ったほうが距離が短くなる場合
					if(A[i][j] > A[i][k] + A[k][j]) {
						System.out.println(-1);
						return;
					}
					//別の経由地を通っても最短距離になる場合
					else if(connected[i][j] && i != k && j != k && A[i][j] == A[i][k] + A[k][j]) {
						ans -= A[i][j];
						connected[i][j] = false;	//2重で引くのを防ぐ
					}
				}
			}
		}

		//無向グラフなので2で割る
		ans /= 2;

		System.out.println(ans);

	}

}


というわけで、ワーシャルフロイド法についての記事でした。
まだ、ベルマンフォード法と最小全域木問題があるので、そこについても勉強したいと思います。
今週末はABCがないみたいなので、次回のABCまでにこの2つを勉強したいと思います。