AtCoder Beginner Contenst 275 E 解答までの思考
AtCoder Beginner Contest(ABC) 275のE問題について、
コンテスト中の考察の流れを書き残しておきます。
前提
筆者のレートは緑〜水をフラフラとしているくらいです。
想定解をスムーズに導くより、コンテスト中にどのような流れで解答に行きついたかの考えを
参考に残しているものです。
筆者がGo言語で競技プログラミングに参加しているため、サンプルのコードはGo言語で記載しています。
問題
下記のページからの引用です。
問題
問題文
高橋君は双六で遊んでいます。
この双六には
から 0 の番号がついた N 個のマスがあります。 高橋君はマス N+1 からスタートし、マス 0 を目指します。 N この双六では、
から 1 までの M 種類の目が等確率で出るルーレットを使います。 高橋君はルーレットを回して出た目の数だけ進みます。もし、マス M を超えて進むことになる場合、マス N を超えた分だけ引き返します。 N 例えば、
で高橋君がマス N=4 にいるとき、ルーレットを回して出た目が 3 の場合は、マス 4 を 4 マス超えてしまいます。そのため、 3+4−4=3 マスだけマス 3 から引き返し、マス 4 に移動します。 1 高橋君がマス
に到達するとゴールとなり、双六を終了します。 N 高橋君がルーレットを
回まで回す時、ゴールできる確率を K で求めてください。 \mod 998244353 確率
の定義 \mod 998244353
この問題で求める確率は必ず有理数になることが証明できます。 また、この問題の制約下では、求める確率を既約分数で表したときに frac{y}{x} が x で割り切れないことが保証されます。 998244353 このとき
を満たすような xz≡y \pmod {998244353} 以上 0 以下の整数 998244352 が一意に定まります。この z を答えてください。 z 制約
M≤N≤1000 1≤M≤10 1≤K≤1000 - 入力は全て整数
問題理解
まずは入力例1をもとに、
ルーレットを
このような絵を描いて、問題文入力例1の「ゴールできる確率は
さらに
漸化式から、この問題は動的計画法(DP)で解く方針で進めました。
DPの詳細検討
計算量の見積もり
漸化式をもとに、
とします。計算量は
実際にコード化すると下記のようなループになる見込みでした。
for i := 1; i <= k; i++ {
for j := 0; j < n; j++ {
for x := 1; x <= m; x++ {
//ここにdp[i][j] += dp[i-1][j']のような処理を書いていく
}
}
}
フェルマーの小定理を適用する
DPを具体的にしていく段階で、 確率を
漸化式の中で、
これはAtCoderの過去問で学んだフェルマーの小定理を使用しました。
が成立するという定理です。
となり、
func Pow(x, y, p int) int {
ret := 1
for y > 0 {
if y%2 == 1 {
ret = ret * x % p
}
y >>= 1
x = x * x % p
}
return ret
}
func Inv(x, p int) int {
return Pow(x, p-2, p)
}
貰う、配るの再検討
次に
問題の制約から、ルーレットで大きな数字が出ても、マス
これに合わせて、DPの書き方も漸化式通り貰うDPの書き方をしていたところを、配るDPの書き方に変更して下記のようにDPの遷移も書けてきました。
dp[0][0] = 1
//pは別のところで998244353と定義
im := Inv(m, p)
//今jにいてルーレットでxが出た時、次の位置
computeNext := func(j, x int) int {
if j+x > n {
over := j + x - n
return n - over
} else {
return j + x
}
}
for i := 0; i < k; i++ {
for j := 0; j < n; j++ {
for x := 1; x <= m; x++ {
next := computeNext(j, x)
dp[i+1][next] += dp[i][j] * im
dp[i+1][next] %= p
}
}
}
実装
ここまで書けたところで問題の入力例を試してみましたら、入力例2だけ想定の出力が得られませんでした。
$ go1.14.1 run main.go
10 5 6
453091133
これは解答として
答えはルーレットを
var ans int
for i := 1; i <= k; i++ {
ans += dp[i][n]
ans %= p
}
return ans
これを適用して入力例2も想定の出力が得られました。
$ go1.14.1 run main.go
10 5 6
184124175
最終的なACコードは下記のようになります。
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
var sc = bufio.NewScanner(os.Stdin)
var out = bufio.NewWriter(os.Stdout)
func main() {
buf := make([]byte, 1024*1024)
sc.Buffer(buf, bufio.MaxScanTokenSize)
sc.Split(bufio.ScanWords)
n, m, k := nextInt(), nextInt(), nextInt()
ans := solve(n, m, k)
PrintInt(ans)
}
func solve(n, m, k int) int {
const p = 998244353
dp := make([][]int, k+1)
for i := 0; i <= k; i++ {
dp[i] = make([]int, n+1)
}
dp[0][0] = 1
im := Inv(m, p)
//今jにいてルーレットでxが出た時、次の位置
computeNext := func(j, x int) int {
if j+x > n {
over := j + x - n
return n - over
} else {
return j + x
}
}
for i := 0; i < k; i++ {
for j := 0; j < n; j++ {
for x := 1; x <= m; x++ {
next := computeNext(j, x)
dp[i+1][next] += dp[i][j] * im
dp[i+1][next] %= p
}
}
}
var ans int
for i := 1; i <= k; i++ {
ans += dp[i][n]
ans %= p
}
return ans
}
func nextInt() int {
sc.Scan()
i, _ := strconv.Atoi(sc.Text())
return i
}
func PrintInt(x int) {
defer out.Flush()
fmt.Fprintln(out, x)
}
func Pow(x, y, p int) int {
ret := 1
for y > 0 {
if y%2 == 1 {
ret = ret * x % p
}
y >>= 1
x = x * x % p
}
return ret
}
func Inv(x, p int) int {
return Pow(x, p-2, p)
}
最後に
今回解法に早く気がつけたのは、今年の夏頃に期待値を求める問題に苦手と感じることがあり、
確率、期待値に関して集中して学び直していたことがきっかけかもしれません。
苦手を感じたのが、まさに今回のSugoroku 4の前のナンバリング問題であるSugoroku 3でした。
楽しみながら、手を動かしながら学べたのが、書籍数学ガールや、アルゴ式のプログラミングで学ぶ数学のコンテンツでした。最後に参考に紹介させていただきます。
Discussion