第11回日本情報オリンピック予選(過去問) D - パスタ(Pasta) Python解答例
第11回日本情報オリンピック予選(過去問) D - パスタ(Pasta)をPythonで解きます。
問題文
問題分をAtCoderのページより引用します。
問題
あなたはパスタが大好物であり,毎日,晩御飯にパスタを作って食べている.あなたはトマトソース,クリームソース,バジルソースの
種類のパスタを作ることができる. 3
日間の晩御飯の予定を考えることにした.それぞれの日に N 種類のパスタから 3 種類を選ぶ.ただし,同じパスタが続くと飽きてしまうので, 1 日以上連続して同じパスタを選んではいけない.また, 3 日のうちの N 日分のパスタはすでに決めてある. K 入力として
の値と, N 日分のパスタの情報が与えられたとき,条件をみたす予定が何通りあるかを K で割った余りを求めるプログラムを作成せよ. 10\,000 入力
入力は
行からなる. K + 1
行目には 1 つの整数 2 ( N, K , 3 \leqq N \leqq 100 ) が空白を区切りとして書かれている. 1 \leqq K \leqq N
行目 ( 1 + i ) には 1 \leqq i \leqq K つの整数 2 ( A_i, B_i , 1 \leqq A_i \leqq N ) が空白を区切りとして書かれている.これは, 1 \leqq B_i \leqq 3 日目のパスタはすでに決まっており, A_i のときはトマトソースであり, B_i = 1 のときはクリームソースであり, B_i = 2 のときはバジルソースであることを表す. B_i = 3 ( A_i ) は全て異なる.与えられる入力データにおいて,条件をみたす予定は 1 \leqq i \leqq K 通り以上あることが保証されている. 1
解答例
動的計画法で解きます。
DPテーブルの定義
以下のようにDPテーブルを定義します。ただし、
初期条件は以下のようになります。ここで、すでにパスタの種類が決まっている日の集合を
遷移式は以下のようになります。尚、
考察パート
まず、「3日以上連続して同じパスタを選んではいけない」ルールが無い状態を考えてみます。予めパスタの種類も決まっていないとします。
このときの場合の数の考え方は極めて単純に考えることができます。以下のようなグラフを考え、各ノードに至るパスの本数を数えればよいです。
この図では横軸に
次に、3日以上連続して同じパスタを食べてはいけないルールを課してみます。すると、このままのグラフではルールを考慮することができないことに気づきます。
たとえば、3日目に種類1のパスタを食べる場合の数を数えてみます。上の場合と同じように、2日目に種類1, 2, 3のパスタを食べた場合の数の和を求めれば良いように見えますが、ノード
従ってこれを計上してしまうとルールに反することになってしまいます。一方で、ノード
以上のことから、上の図のようなグラフの形式では問題の制約をうまく考慮できないことがわかりました。
これを解決するために、各ノードに以下の状態を持たせることを考えます。
- 前日に同じ種類のパスタを食べたかどうか。
これを考慮したグラフは以下の図のようになります。
まず、
次に、
さて、
この計算を各日付と各種類に対して実施することで、全ての場合の数を数え上げることができます。
最後に、すでに食べる種類が決まっている日付について考えます。
これを表現するには、単純にノード
上の計算の後、
実装例
実際のコード例を以下に示します。
集合
from typing import List, Dict
import itertools
def main():
# 入力受け取り
N, K = map(int, input().split())
A, B = map(list, zip(*[list(map(int, input().split())) for i in range(K)]))
# 法
mod: int = 10000
# すでに決まっている予定の情報を格納しておくための辞書
D: Dict[int, int] = {a: b - 1 for a, b in zip(A, B)}
# DPテーブル
dp: List[List[List[int]]] = [[[0] * 2 for j in range(3)] for i in range(N + 1)]
# 初日の予定が決まっているかどうかで初期条件が変わるので場合分けして初期化
if 1 in D:
dp[1][D[1]] = [1, 0]
dp[1][(D[1] + 1) % 3] = [0, 0]
dp[1][(D[1] + 2) % 3] = [0, 0]
else:
dp[1] = [[1, 0] for i in range(3)]
# DP遷移
for i in range(2, N + 1):
for j in range(3):
# 前日と違う種類を食べる場合
dp[i][j][0] = sum(dp[i - 1][(j + 1) % 3]) + sum(dp[i - 1][(j + 2) % 3])
# 前日と同じ種類を食べる場合
dp[i][j][1] = dp[i - 1][j][0]
# 余りを取る
dp[i][j][0] %= mod
dp[i][j][1] %= mod
# i日目の予定がすでに決まっていた場合
if i in D:
# 決まっている種類以外のパスタを食べる場合の数を0にする
# TIPS: 0, 1, 2の数字のうちある数字以外の数字は、1、2をそれぞれ
# 足した余りを取ることで求められる。
dp[i][(D[i] + 1) % 3] = [0, 0]
dp[i][(D[i] + 2) % 3] = [0, 0]
# 結果の出力
# dp[N]はリストのリストになっていて、最終結果は各リストの要素の和である。
# itertools.chainは与えられたシーケンスの中身を全て展開して一つのシーケンス
# に纏める働きを持つので、dp[N]の中身を展開して渡している。
print(sum(itertools.chain(*dp[N])) % mod)
if __name__ == "__main__":
main()
実際に提出したコードはこちら。
Discussion
とても分かりやすいです...。ありがとうございます🙇♂
この図では、i が 1, 2, 3 と並んでいますが、文中の表現で言うと i-2, i-1, i という理解で正しいでしょうか?
DP をノードで表現する方法が新鮮に感じたのですが、DP はノードで考える方が一般的なアプローチなのでしょうか?
コメントありがとうございます。
i = 3の場合を考えたときはその理解で正しいです。
尚、iは文中で1以上N以下と定義しているので、図の数値はiの値と対応していると考えて問題無いです。(オフセットみたいなものは無い)
私自身DPに明るくないのでなんともですが、この問題のように状態の数が有限(3×2)で、iだけについて計算していくような問題ならグラフで考察もできるんじゃないかな、と思っています。
逆にナップザック問題のようなDPはグラフで考えるのはちょっと難しい気がするので、問題によりけりかなと思います。