D - 中継都市の重要度 / Importance of Relay Cities Editorial by admin
gemini-3.5-flash-thinking概要
この問題は、与えられた重み付き有向グラフにおいて、各頂点 \(k\) が他の頂点ペア \((i, j)\) の最短経路上の中継地点(内部の頂点)になり得るかを判定し、条件を満たすペアの個数を各 \(k\) について求める問題です。
考察
1. 「最短経路上に存在する」ことの数理的条件
都市 \(i\) から都市 \(j\) への最短コストを \(d(i, j)\) と表すことにします。
都市 \(k\) が \(i\) から \(j\) への最短経路の「内部の頂点」として含まれるとは、「\(i\) から \(k\) を経由して \(j\) に至る経路のコストが、全体の最短コスト \(d(i, j)\) と等しくなる」ということです。 \(i\) から \(k\) への最短経路と、\(k\) から \(j\) への最短経路を繋ぎ合わせることで、全体として \(i \to j\) の最短経路が構成できます。
したがって、都市 \(i, j, k\) がすべて相異なるとき、都市 \(k\) が \(i \to j\) の最短経路に含まれるための必要十分条件は、以下の式が成り立つことです。 $\(d(i, j) = d(i, k) + d(k, j)\)$
2. 制約とアプローチの選択
この条件をすべての \(i, j, k\) の組み合わせに対して判定するためには、すべての頂点ペア間の最短コスト \(d(u, v)\) をあらかじめ求めておく必要があります。
制約に注目すると、都市の数 \(N\) は \(2 \le N \le 400\) と比較的小さいです。 全点対間の最短経路を求める代表的なアルゴリズムであるワーシャル・フロイド(Warshall-Floyd)法を使用すると、時間計算量 \(O(N^3)\) で全点対間の最短コストを求めることができます。
\(N = 400\) のとき \(N^3 = 6.4 \times 10^7\) となり、C++などの高速な言語であれば、実行時間制限内に余裕を持って間に合わせることができます。
アルゴリズム
プログラムは以下のステップで動作します。
隣接行列の初期化: \(N \times N\) の二次元配列
dを用意し、すべての要素を十分に大きな値INF(無限大)で初期化します。ただし、自分自身への距離d[i][i]は0とします。 その後、与えられた道路の情報を元にd[u][v] = wと更新します。ワーシャル・フロイド法による最短経路の計算: 3重のループを回し、すべての頂点ペア間の最短コストを求めます。
for (int k = 0; k < N; ++k) { for (int i = 0; i < N; ++i) { for (int j = 0; j < N; ++j) { d[i][j] = min(d[i][j], d[i][k] + d[k][j]); } } }重要度のカウント: 各都市 \(k\) について、始点 \(i\) と終点 \(j\) を全探索します。 \(i, j, k\) がすべて相異なり、かつ \(i\) から \(k\)、\(k\) から \(j\) への経路が存在する(
INFでない)とき、 $\(d(i, j) == d(i, k) + d(k, j)\)\( が成り立つならば、都市 \)k\( の重要度のカウントを \)1$ 増やします。結果の出力: 各都市 \(k\) について求めたカウントを出力します。
計算量
- 時間計算量: \(O(N^3)\) ワーシャル・フロイド法による最短経路の計算に \(O(N^3)\)、各都市の重要度のカウントに \(O(N^3)\) かかります。\(N \le 400\) であるため、全体の計算量は約 \(1.28 \times 10^8\) 回の基本演算となり、実行時間制限に十分間に合います。
- 空間計算量: \(O(N^2)\) 頂点間の最短距離を保持する \(N \times N\) の二次元配列を使用するため、空間計算量は \(O(N^2)\) となり、メモリ制限に対しても非常に軽量です。
実装のポイント
オーバーフローの防止: 最短経路が存在しないことを表す
INFの値として1e18などの大きな値を使用する場合、d[i][k] + d[k][j]の足し算でオーバーフローが発生しないよう、事前にd[i][k] != INF && d[k][j] != INFであることを確認する条件式を入れています。0-indexed への変換: 問題文では都市の番号が \(1\) から \(N\) で与えられますが、プログラム内では配列のインデックスに合わせて \(0\) から \(N-1\) に変換(デクリメント)して処理を行うことで、実装をシンプルにしています。
ソースコード
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const long long INF = 1e18;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int N, M;
if (!(cin >> N >> M)) return 0;
vector<vector<long long>> d(N, vector<long long>(N, INF));
for (int i = 0; i < N; ++i) {
d[i][i] = 0;
}
for (int i = 0; i < M; ++i) {
int u, v;
long long w;
cin >> u >> v >> w;
--u; --v;
d[u][v] = min(d[u][v], w);
}
for (int k = 0; k < N; ++k) {
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
if (d[i][k] != INF && d[k][j] != INF) {
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
}
for (int k = 0; k < N; ++k) {
int count = 0;
for (int i = 0; i < N; ++i) {
if (i == k) continue;
for (int j = 0; j < N; ++j) {
if (j == k || j == i) continue;
if (d[i][k] != INF && d[k][j] != INF && d[i][j] == d[i][k] + d[k][j]) {
count++;
}
}
}
cout << count << "\n";
}
return 0;
}
この解説は gemini-3.5-flash-thinking によって生成されました。
posted:
last update: