問題
$N$ 頂点 $M$ 辺の有向グラフがある。各頂点 $i$ にはコスト $c_i \ge 0$ が付いており、確率的遷移を行う。頂点 $i$ から確率 $p_{ij}$ で頂点 $j$ へ移動しコスト $c_i$ を支払う。ゴール頂点 $t$ に到達するまでのコストの期待値 $E[s]$ を求めよ。
$$E[i] = c_i + \sum_{j=1}^{N} p_{ij} \cdot E[j] \quad (i \neq t), \quad E[t] = 0$$
制約
| パラメータ | 範囲 | 備考 |
|---|---|---|
| $N$ | $2 \le N \le 200$ | 頂点数 |
| $M$ | $1 \le M \le N^2$ | 辺数 |
| $c_i$ | $0 \le c_i \le 10^6$ | 各頂点のコスト |
| $p_{ij}$ | 有理数(分子/分母で入力) | 各頂点からの確率和=1 |
入出力例
入力例1
4 4 1 4
5 3 2 0
1 2 1 2
1 3 1 2
2 4 1 1
3 4 1 1
出力例1
15/2
$E[4]=0$, $E[2]=3$, $E[3]=2$, $E[1]=5+\frac{1}{2}(3)+\frac{1}{2}(2)=\frac{15}{2}$
概念図: 確率グラフ → ベルマン方程式 → ガウス消去
各頂点で「コスト + 次頂点の期待値の加重平均」というベルマン方程式を立て、移項して線形方程式系に変換。 ガウス消去法で厳密解(有理数)を求める。
ヒント
ヒント1(方向性)
ゴール頂点 $t$ 以外の各頂点 $i$ に対して未知数 $E[i]$ を立てる。$E[i] - \sum_{j \neq t} p_{ij} E[j] = c_i$ という線形方程式系 $AE = b$ をガウス消去法で $O(N^3)$ で解く。
ヒント2(アプローチ)
係数行列 $A$($(N-1) \times (N-1)$)の設定:
対角 $A[i][i] = 1$、非対角 $A[i][j] = -p_{v_i, v_j}$(遷移確率)、定数 $b[i] = c_{v_i}$。
fractions.Fraction を使うと厳密な有理数解が得られる。
ヒント3(ほぼ答え)
from fractions import Fraction
# mat: (n x n+1) 拡大係数行列
for col in range(n):
pivot = next((r for r in range(col, n) if mat[r][col] != 0), -1)
if pivot == -1: continue
mat[col], mat[pivot] = mat[pivot], mat[col]
inv = Fraction(1, mat[col][col])
for k in range(n + 1):
mat[col][k] *= inv
for row in range(n):
if row != col and mat[row][col] != 0:
f = mat[row][col]
for k in range(n + 1):
mat[row][k] -= f * mat[col][k]
# 解: mat[i][n] = E[v_i]
模範解答
import sys
from fractions import Fraction
def solve():
data = sys.stdin.read().split()
ptr = 0
N = int(data[ptr]); ptr += 1
M = int(data[ptr]); ptr += 1
s = int(data[ptr]); ptr += 1
t = int(data[ptr]); ptr += 1
cost = [0] * (N + 1)
for v in range(1, N + 1):
cost[v] = int(data[ptr]); ptr += 1
edges_from = [[] for _ in range(N + 1)]
for _ in range(M):
u = int(data[ptr]); ptr += 1
v = int(data[ptr]); ptr += 1
pn = int(data[ptr]); ptr += 1
pd = int(data[ptr]); ptr += 1
edges_from[u].append((v, Fraction(pn, pd)))
nodes = [v for v in range(1, N + 1) if v != t]
n = len(nodes)
idx_map = {v: i for i, v in enumerate(nodes)}
mat = [[Fraction(0)] * (n + 1) for _ in range(n)]
for i, v in enumerate(nodes):
mat[i][i] = Fraction(1)
mat[i][n] = Fraction(cost[v])
for (w, p) in edges_from[v]:
if w != t:
j = idx_map[w]
mat[i][j] -= p
for col in range(n):
pivot_row = next((r for r in range(col, n) if mat[r][col] != 0), -1)
if pivot_row == -1: continue
mat[col], mat[pivot_row] = mat[pivot_row], mat[col]
inv_p = Fraction(1, mat[col][col])
for k in range(n + 1):
mat[col][k] *= inv_p
mat[col][col] = Fraction(1)
for row in range(n):
if row != col:
f = mat[row][col]
if f != 0:
for k in range(col, n + 1):
mat[row][k] -= f * mat[col][k]
mat[row][col] = Fraction(0)
E = {t: Fraction(0)}
for i, v in enumerate(nodes):
E[v] = mat[i][n]
ans = E[s]
p, q = ans.numerator, ans.denominator
print(p if q == 1 else f"{p}/{q}")
solve()
Step-by-Step 解説
Step 1: ベルマン方程式の導出
頂点 $i$ での期待総コスト $E[i]$ を定義する。ゴール: $E[t] = 0$。その他: $$E[i] = c_i + \sum_j p_{ij} E[j]$$ 直感: まずコスト $c_i$ を支払い、その後 $j$ に移動したときの残り期待コスト $E[j]$ の加重平均を加える。 サイクルがあると後ろ向きDPが使えないため、連立方程式として解く。
Step 2: 線形方程式系への変換
$E[t]=0$ を代入して移項: $E[i] - \sum_{j \neq t} p_{ij} E[j] = c_i$
| $A$ の要素 | 値 | 理由 |
|---|---|---|
| $A[i][i]$ | $1$ | $E[i]$ の係数 |
| $A[i][j]$($j \neq i, j \neq t$) | $-p_{v_i, v_j}$ | $-p_{ij} \cdot E[j]$ の係数 |
| $b[i]$ | $c_{v_i}$ | 定数項(右辺) |
行列 $A$ は対角優位($|A_{ii}|=1 \ge \sum_{j \neq i}|A_{ij}|$)なので数値的に安定。
Step 3: ガウス・ジョルダン消去
拡大係数行列 $[A | b]$ に対して各列をピボットとして前進・後退消去を同時実行(ジョルダン型)。終了時に $[I | E_{\text{vec}}]$ の形になり直接解が得られる。計算量 $O(N^3)$。$N=200$ なら $8 \times 10^6$ 演算で十分高速。
Step 4: fractions.Fraction による厳密解
浮動小数点では累積誤差が生じる。fractions.Fraction を使うと有理数を厳密に扱え、最終的な期待値を真の既約分数として出力できる。
$N=200$ の場合 Fraction の演算量は許容範囲内。出力: $Q=1$ なら整数、そうでなければ P/Q 形式。
Step 5: 計算量まとめ
| ステップ | 計算量 |
|---|---|
| 方程式設定 | $O(NM)$ |
| ガウス消去 | $O(N^3)$ |
| 全体 | $O(N^3) = O(200^3) = 8 \times 10^6$ |
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| ゴール頂点 $t$ も変数に含める | 方程式が $(N \times N)$ になりランク不足 | $t$ を変数から除き $E[t]=0$ を代入してから立式 |
| 対角 $A[i][i]=1$ の初期化漏れ | $E[i]$ の係数が0になり方程式が $-\sum p_{ij}E[j]=c_i$ になる | 必ず mat[i][i] = 1 を設定 |
| float を使う | 分数の累積誤差で整数解が 7.4999... になる |
fractions.Fraction を使う |
| ゴールへの遷移 $p_{it}$ を定数項に加算 | $-p_{it} \cdot E[t] = 0$ なので不要だが加算するとバグ | ゴールへの遷移は何もしない |
| ジョルダン消去せず前進消去のみ | 後退代入のコードが別途必要で複雑 | 全行を対象に消去するジョルダン型が実装簡潔 |
| 到達不能頂点を見落とす | ピボットが0になった列でゼロ除算が起きる | if pivot_row == -1: continue でスキップ |
次のステップ
- 発展問題1: コストが負になり得る場合(MDPの Value Iteration と収束証明)
- 発展問題2: 複数ゴールが存在し、最速で任意ゴールに到達するまでの期待ステップ数
- 発展問題3: 無限ループが存在し得るグラフで $E[s] = \infty$ を検出して報告する(ランク不足の判定)
- 発展問題4: $N=1000$ 以上の大規模版で浮動小数点 Value Iteration を使う場合の収束判定