問題
$N$ 頂点 $M$ 辺の有向グラフが与えられる。各辺 $i$ は $u_i \to v_i$ で重み $w_i$(負の値もあり得る整数)を持つ。グラフ中の閉路のうち「閉路の重みの合計 ÷ 閉路の辺数」(平均重み)を最小化する閉路を求め、その最小平均値を既約分数 $p/q$($q>0$)の形式で出力せよ。グラフには少なくとも1つの閉路が存在することが保証される。
制約
| パラメータ | 範囲 | 備考 |
|---|---|---|
| $N$ | $1 \le N \le 300$ | 頂点数 |
| $M$ | $1 \le M \le 3000$ | 辺数 |
| $w_i$ | $-10^6 \le w_i \le 10^6$ | 負の重みあり得る |
| 閉路 | — | 少なくとも1つ存在 |
入出力例
入力例1
3 4
1 2 1
2 1 -4
2 3 1
3 1 1
出力例1
-3/2
入力例2
3 3
1 2 2
2 3 2
3 1 3
出力例2
7/3
例1: 2頂点閉路 $1\to2\to1$ の平均 $(1-4)/2=-3/2$ が最小。例2: 唯一の閉路 $1\to2\to3\to1$ の平均 $7/3$。
概念図: 仮想始点 $d_0(v)=0$ から $k$ 辺ウォークDPで閉路を検出
ヒント
ヒント1(方向性)
すべての単純閉路を列挙して平均を比較するのは指数時間になり得るため不可能。最小平均閉路問題には多項式時間で解ける専用のアルゴリズム(Karp 1978)がある。
ヒント2(アプローチ)
$d_k(v)$ =「全頂点から仮想的にコスト0でスタートできるとして、ちょうど $k$ 本の辺を使って頂点 $v$ に到達する最短ウォークの重み」と定義したDPを $k=0,\dots,N$ まで計算する。
ヒント3(ほぼ答え)
d = [[INF]*(n+1) for _ in range(n+1)]
for v in range(1, n+1):
d[0][v] = 0
for k in range(1, n+1):
for u in range(1, n+1):
if d[k-1][u] == INF: continue
for v, w in adj[u]:
d[k][v] = min(d[k][v], d[k-1][u] + w)
# 最終判定は Fraction(d[n][v]-d[k][v], n-k) の max/min
模範解答
import sys
from fractions import Fraction
def main():
data = sys.stdin.read().split()
idx = 0
n = int(data[idx]); idx += 1
m = int(data[idx]); idx += 1
adj = [[] for _ in range(n + 1)]
for _ in range(m):
u = int(data[idx]); idx += 1
v = int(data[idx]); idx += 1
w = int(data[idx]); idx += 1
adj[u].append((v, w))
INF = float('inf')
d = [[INF] * (n + 1) for _ in range(n + 1)]
for v in range(1, n + 1):
d[0][v] = 0
for k in range(1, n + 1):
for u in range(1, n + 1):
if d[k - 1][u] == INF:
continue
for v, w in adj[u]:
if d[k - 1][u] + w < d[k][v]:
d[k][v] = d[k - 1][u] + w
best = None
for v in range(1, n + 1):
if d[n][v] == INF:
continue
worst = None
for k in range(n):
if d[k][v] == INF:
continue
val = Fraction(d[n][v] - d[k][v], n - k)
if worst is None or val > worst:
worst = val
if worst is not None and (best is None or worst < best):
best = worst
print(f"{best.numerator}/{best.denominator}")
main()
計算量: DPテーブルの計算が $O(NM)$、最終集計が $O(N^2)$。全体で $O(NM)$。
Step-by-Step 解説
Step 1: 「全頂点から無料でスタート」する仮想始点DP
$d_0(v)=0$(全頂点)とすることで、グラフが強連結でなくてもどこかにある最小平均閉路を正しく検出できる。
Step 2: ちょうど $k$ 辺のウォークの最短値を計算
$d_k(v)=\min_{(u,v)\in E} d_{k-1}(u)+w(u,v)$ の緩和を $N$ 回繰り返す。$O(NM)$。
Step 3: Karp の公式を適用
| 対象 | 操作 |
|---|---|
| 各頂点 $v$ | $\max_k \dfrac{d_N(v)-d_k(v)}{N-k}$ |
| 全頂点間 | 最小値を取る |
Step 4: 厳密な分数比較
浮動小数点誤差を避けるため fractions.Fraction で厳密に計算する。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| $d_0$ を単一始点にしか設定しない | 強連結でないグラフで閉路を見逃す | 全頂点に $d_0(v)=0$(仮想始点トリック) |
平均比較を float で行い誤差で誤判定 | 浮動小数点比較 | Fraction で厳密比較 |
maxとminを取り違える | 公式の意味の誤解 | 頂点内はmax、頂点間はmin |
次のステップ
- 発展問題: 最小平均閉路を実際に1つ復元する
- 発展問題: 二分探索 + 負閉路検出(Bellman-Ford)版との比較
自己評価
理解度: / /
自分の回答:
気づき・メモ: