Day 176-Q2 — 2026-10-12 | やり直しのある最速クリア(答え E が遷移の中に現れる期待値 DP を、方策反復=Newton 法で解く)

2026-10-12 赤色 Master (Phase 8+) ★★★★★★★★★ 高度DP(焦点帯 M1)。「始めからやり直す」選択肢があると、やり直した後の期待値は求めたい答え $E$ そのものなので、DP の式に未知数 $E$ が入る。$E$ を仮定して DP を 1 回回すと $f(E)$ が出て、答えは不動点 $E=f(E)$。二分探索なら約 35 回 DP を回すところを、DP の結果を $a+bE$ の 1 次式で持てば方策反復(Newton 法)で 2〜6 回に減る

問題

あるゲームは $N$ 面からなり、1 面から順に遊ぶ。$i$ 面は確率 $P_i\%$ で $F_i$ 秒、残りの確率で $S_i$ 秒かかる($F_i<S_i$)。

1 面から $N$ 面までを1 回の通しで、通しの所要時間の合計 $R$ 秒以内で終えれば達成である。各面を終えた直後に、やり直す(1 面の最初に戻り、通しの所要時間を 0 に戻す)ことを選べる。やり直しに時間はかからないが、それまでに遊んだ時間は総プレイ時間に含まれる。

達成までの総プレイ時間の期待値を最小にするように、いつやり直すかを選ぶ。その最小値を出力せよ。絶対誤差または相対誤差が $10^{-6}$ 以下なら正解とする。

入力形式

N R
F_1 S_1 P_1
...
F_N S_N P_N

制約

#制約
1$1\le N\le50$
2$1\le F_i<S_i\le100$、$80\le P_i\le99$
3$\sum F_i\le R\le5000$

入出力例

入力例1

1 10
3 12 70

出力例1

8.1428571429

(遅いと 12 秒で制限超え。$E=0.7\cdot3+0.3(12+E)$ を解いて $E=57/7$)

入力例2

2 9
2 7 80
3 5 90

出力例2

6.9500000000

(1 面が遅い(7 秒)と 2 面が速くても 10 秒で間に合わない。1 面を終えた時点でやり直せば $139/20=6.95$。最後まで遊んでから気づくと 7.75)

入力例3

2 10
1 4 95
5 8 80

出力例3

6.8105263158

(1 面が遅い(4 秒)とき、2 面が速ければ 9 秒でまだ間に合う。それでもやり直した方が得で、答えは $647/95$。「間に合わなくなったときだけやり直す」と $75/11=6.8182$ で誤り。3 例とも、全方策を分数で厳密に評価した最小値と一致)

背景と出典

  • Codeforces 865C「Gotta Go Fast」が同じ設定の問題だと私は記憶している(入出力例の値で照合したが、問題文は本セッションで未確認)。本問の入力例は自作し、制約は記憶に合わせた
  • 本問の主題は解き方の比較: 不動点 $E=f(E)$ を二分探索で求める方法と、方策(どこでやり直すか)を固定すると $D(0,0)=a+bE$ が 1 次式になることを使う方策反復(Howard 1960 の policy iteration と同じ形。Newton 法とも見られる)
  • これはマルコフ決定過程の「期待総費用の最小化」で、最適方策は定常・決定的なもの(状態だけで決まる)から選べる、という標準的な結果を使っている(Puterman の教科書などにあると記憶、未確認)。検算では小さい入力で全方策を列挙して確かめた

概念図

f(x) = min(方策ごとの直線 a + b·x)と y = x の交点が答え。方策反復は直線の交点へ跳ぶ(模式図) 仮定した E(= x)f(x) y = x 方策 A: 自分からはやり直さない(傾き b 小) 方策 B 方策 C: 早めにやり直す(傾き b 大) 太線 = f(x)(直線の最小 = 上に凸) E₁(A の不動点)E₂E* = E₃ 二分探索は x 軸を半分ずつ(約 35 回)。方策反復は「今の E で最良の方策」の直線と y = x の交点へ直接進み、2〜6 回で止まる(実測)。 E₀ = ∞ から始めるのは、E₀ = 0 だと「毎回やり直す」方策(b = 1)になり a/(1−b) が定義できないため。

ヒント(クリックで段階的に開示)

ヒント1(方向性)

$i$ 面まで終えて通しの経過が $t$ 秒の状態 $(i,t)$ からの「残りの期待時間」を $D(i,t)$ とする。やり直すと残りは最初からと同じ $E=D(0,0)$。つまり $D$ を求めるのに答え $E$ が要る。

ヒント2(アプローチ)

$E$ を仮に $x$ と決めれば、$D$ は普通の DP で後ろから計算でき、$f(x)=D(0,0)$ が出る。

  • $x>E^*$(大きすぎる)なら $f(x)<x$、$x<E^*$ なら $f(x)>x$。だから二分探索できる
  • さらに、どこでやり直すかを固定すると、$D(0,0)$ は $x$ の 1 次式 $a+bx$($b$ はやり直しで最初に戻る確率)。$E=a+bE$ を解けば $E=a/(1-b)$
ヒント3(誘導)
  • 各 $D$ を数 1 つでなく組 $(a,b)$ で持つ。やり直しは $(0,1)$、クリアは $(0,0)$
  • 現在の推定 $E$ で $\min(E,\ \text{続ける})$ を比べて方策を決め、$(a,b)$ を後ろから計算
  • 新しい $E\leftarrow a/(1-b)$。$E$ が減らなくなったら終わり。最初は「自分からはやり直さない」方策($E=\infty$)から始める

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n, lim = int(data[0]), int(data[1])
    F = [int(x) for x in data[2:2 + 3 * n:3]]
    S = [int(x) for x in data[3:3 + 3 * n:3]]
    P = [int(x) / 100 for x in data[4:4 + 3 * n:3]]

    INF = float('inf')

    def evaluate(E):
        """E を「やり直したときの残り期待時間」と仮定して最適方策を選び、
        その方策での D(0,0) を a + b*E の形で返す(b = やり直しで始めに戻る確率)"""
        # nxt_a[t], nxt_b[t]: i+1 面クリア済み・経過 t のときの a, b(t > lim は強制やり直し = 0 + 1*E)
        nxt_a = [0.0] * (lim + 1)
        nxt_b = [0.0] * (lim + 1)
        for i in range(n - 1, -1, -1):
            f, s, p = F[i], S[i], P[i]
            q = 1.0 - p
            cur_a = [0.0] * (lim + 1)
            cur_b = [1.0] * (lim + 1)              # 既定はやり直し(a=0, b=1)
            tmax = lim if i > 0 else 0              # 0 面目は t=0 だけ
            for t in range(tmax + 1):
                tf, ts = t + f, t + s
                if tf > lim:
                    ca = p * f + q * s              # 速くても制限超え → 遊んだ後に必ずやり直し
                    cb = 1.0
                elif ts > lim:
                    ca = p * (f + nxt_a[tf]) + q * s
                    cb = p * nxt_b[tf] + q
                else:
                    ca = p * (f + nxt_a[tf]) + q * (s + nxt_a[ts])
                    cb = p * nxt_b[tf] + q * nxt_b[ts]
                if i == 0 or E == INF or ca + cb * E < E:   # 続けた方が得なら続ける
                    cur_a[t] = ca
                    cur_b[t] = cb
            nxt_a, nxt_b = cur_a, cur_b
        return nxt_a[0], nxt_b[0]

    E = INF
    for _ in range(100):                            # 方策反復(Newton 法)。実際は数回で止まる
        a, b = evaluate(E)
        newE = a / (1.0 - b)
        if newE >= E * (1 - 1e-15):
            break
        E = newE
    print(f"{E:.10f}")


main()

計算量: DP 1 回 $O(NR)$、それを方策反復の回数 $K$ 回。$N=50,R\le5000$ の 12 入力で $K=2\sim6$。起動込みで測った 9 入力は 0.16〜0.38 s(Python 3.14.7、M1 MacBook Pro 8GB、負荷平均 11〜18 での実測、起動込み)。

Step-by-Step 解説

1状態と式

$D(i,t)$: $i$ 面まで終えて経過 $t$。$i=N$ かつ $t\le R$ なら 0。$t>R$ なら必ずやり直すので $E$。それ以外は

$$D(i,t)=\min\Bigl(E,\ p_i\bigl(F_i+D(i+1,t+F_i)\bigr)+(1-p_i)\bigl(S_i+D(i+1,t+S_i)\bigr)\Bigr)$$

ただし $i=0$(まだ何も遊んでいない)ではやり直しは意味がないので $\min$ を取らない。答えは $E=D(0,0)$。

2不動点としての答え

$x$ を仮定して計算した $D(0,0)$ を $f(x)$ とする。$f$ は $x$ について単調増加で、傾きは 1 未満(やり直しで戻る確率 $<1$)。よって $g(x)=f(x)-x$ は減少し、根 $E^*$ がただ 1 つ。$f(x)<x$ なら $x$ は大きすぎる。これで二分探索ができる(相対誤差 $10^{-10}$ まで約 35 回)。

3方策を固定すると 1 次式

「どの状態でやり直すか」(方策)を固定すると、各 $D$ は $x$ の 1 次式 $a+bx$ になる。$\min$ は方策の選択なので、$f(x)=\min_\pi(a_\pi+b_\pi x)$ は1 次式の最小=上に凸な折れ線。

4方策反復(Newton 法)

現在の $E_k$ で最適な方策 $\pi_k$ を選び、その直線と $y=x$ の交点 $E_{k+1}=a/(1-b)$ に進む。これは凸な折れ線 $g$ に対する Newton 法で、$E_{k+1}\le E_k$ を保ちながら、方策が変わらなくなった時点で厳密に止まる(折れ線なので有限回)。実測では $K=2\sim6$ 回、小さい入力 500 個では最大 4 回・平均 2.37 回だった。

5始点

$E_0=\infty$ は「自分からはやり直さない」方策に当たる。制限 $R\ge\sum F_i$ があるので全部速ければ必ず達成でき、$b<1$ で $a/(1-b)$ が有限になる。逆に $E_0=0$ から始めると「毎回やり直す」方策になり $b=1$ で割り算できない。

迷った他案と採らなかった理由

案DP の回数採否理由
全方策の列挙(分数で厳密評価)$2^{\text{状態数}}$検算用のみ判断する状態が 14 個以下の入力に限る
$E$ の二分探索約 35 回不採用(遅い)正しいが、$N=50,R=5000$ で 1.18〜3.73 s(実測、12 入力)。同じ入力の方策反復(関数呼び出しだけで 0.09〜0.20 s)の 8〜32 倍
自分からはやり直さない($E_0=\infty$ の 1 回目で止める)1 回不採用(誤り)500 入力中 183 で誤答(実測)。入力例 2 で 7.75
間に合わなくなった時点でだけやり直す($t+\sum_{j>i}F_j>R$)1 回不採用(誤り)500 入力中 29 で誤答(実測)。入力例 3 で 6.8182
値反復($E$ を $f(E)$ で置き換え続ける)多数不採用収束率は $b$(やり直し確率)で、$b$ が 1 に近いと非常に遅い。$P_i=80,N=50$ なら達成率は $0.8^{50}\approx1.4\times10^{-5}$ もありうる
方策反復(本解)2〜6 回採用0.16〜0.38 s

前提が変わったら

  • $R$ が $10^5$ 以上なら DP 1 回が $O(NR)$ で重くなる。各面で $t$ についての「やり直す閾値」が単調だと示せれば、閾値だけを持つ形に縮められる(本セッションでは単調性を確かめていない)
  • $P_i$ が小さい(例: 50%)と達成率が $2^{-50}$ 程度になり、$1-b$ が浮動小数で桁落ちする。分数や対数で持つ必要がある
  • やり直しにも時間がかかる($c$ 秒)なら、やり直しの組を $(c,1)$ にするだけで同じ方法が使える

よくあるミス

ミス原因正しい書き方
メモ化再帰で $D(0,0)$ を直接呼ぶやり直しで自分自身を呼ぶ$E$ を未知数として外に出す
方策反復を $E_0=0$ から始める二分探索の下端の習慣毎回やり直す方策になり $1-b=0$ で割れない。$E_0=\infty$(やり直さない)から
「間に合わないときだけやり直す」実行可能性だけを見る間に合っても、遅れが大きければやり直した方が得(入力例 3、29/500 で誤答)
$i=0$ でもやり直しを選ばせる全状態に同じ式を使う$D(0,0)=\min(E,\cdot)$ だと $E=E$ が常に成り立ち、値が決まらない
二分探索の判定を $f(x)\le x$ と $f(x)<x$ で取り違える単調性の向きの混同$f(x)<x$ なら $x$ は大きすぎる(上端を下げる)

次のステップ

  • 発展1: 各面の「やり直す閾値 $t$」が単調かどうかを全探索で調べ、単調なら閾値だけで DP を書く
  • 発展2: 確率が 3 通り以上($F,M,S$)でも同じ方法が使えることを確かめる
  • 発展3: Dinkelbach 法(Day131)と方策反復が、どちらも「比や不動点を 1 次式の交点で更新する Newton 法」であることを式で対応づける

検証ログ(本セッションで実行した数値)

  • 全方策の列挙との照合(実測): $N=1\sim4$、$F\le4$、$S\le F+4$、$P\in[50,99]$、$R$ をランダムにした 500 入力(判断する状態 14 個以下)。各方策を fractions.Fraction で $E=a/(1-b)$ と厳密評価した最小値と、相対誤差 $10^{-6}$ 以内で全件一致
  • 誤った方法(実測、同じ 500 入力): やり直さない 183、間に合わないときだけやり直す 29 で誤答。二分探索は 0 件
  • 方策反復の回数(実測): 小さい 500 入力で最大 4・平均 2.37。$N=50$ の 12 入力で 2〜6
  • 二分探索との比較(実測、$N=50$): $R=2094$($=\sum F$、答え 201126.99)で方策反復 3 回 0.09 s・二分探索 37 回 1.18 s。$R=5000$ で 2 回 0.10〜0.12 s・35 回 2.99〜3.73 s
  • Codeforces 865C の入力例と記憶している 3 つ(1 8 / 2 8 81 → 3.14、2 30 / 20 30 80 / 3 9 85 → 31.4、4 面の例 → 314.1592653585)を本解で再現した
  • 扱っていない範囲: 閾値の単調性、$P_i$ が小さい場合の数値安定性、問題文の原典確認

自己評価

自分の回答

気づき・メモ