0. どんな問題?(問題の概要)
$1$ から $N$ までの連番が振られた $N$ 枚のカードの束を、指定されたルールで $K$ 回シャッフルし、最終的なカードの並びを出力する問題です。シャッフルのルールは以下の通りです。
-
上から $M$ 枚ごとにカードを束(セット)に分ける(※一番下の端数が $M$ 枚未満になった場合も、そのまま1つの束とする)
-
分けた束の上下の順番を完全に逆にする(一番上の束が一番下に、一番下の束が一番上になるように積み直す。※束の中のカードの順番は維持する)
-
この操作を $K$ 回繰り返す
※特定の題材を抽象化し、「リストの分割と逆順結合を用いた状態シミュレーション」として整理しています。
1. 最初のアプローチとつまずいたポイント
またエラーを治すために、コードをめちゃくちゃに張り付けていくという方法を取りました。なので、最後のデータの取り出し方の部分、-10としてしまい、すべて罰になってしまいました。
あと、上書きをする方法がパッと思いつかず、ただただ追加していくだけになりました。自分で勝手に間違いやすいポイント作って、案の定その通りになるという。
import sys
N, M, K = list(map(int, sys.stdin.read().split()))
#print(M)
cards = [int(i + 1) for i in range(N)]
#print(cards)
if (N%M > 0):
number = N//M + 1
else:
number = N//M
sorted_list = []
for i in range(number, 0, -1):
sorted_list.extend(cards[M*(i-1):M*i])
#print(sorted_list)
if K >= 2:
for k in range(1, K):
for i in range(number, 0, -1):
sorted_list.extend(sorted_list[M*(i-1):M*i])
data = sorted_list[-10:]
print(*data, sep="\n")
つまずきポイント
-
状態の更新ミス: $K$ 回のループ内で、1回前のシャッフル結果を「次の初期状態」として上書きせず、1つのリストに extend でどんどん追加してしまい、カードが無限増殖してしまいました
-
デバッグコードの消し忘れ: 焦りのあまり、特定の入力例(N=10)でテストしていた際の [-10:] というスライスを消し忘れ、どんな入力が来ても10枚しか出力されないコードを提出してしまいました
2. 発想の転換
提出直後は絶望しましたが、後から見直すと「M枚ごとにスライスして、後ろのセットから追加する」というアルゴリズムの方向性自体は間違っていなかったなと思いました。(Pythonのスライス [M*(i-1):M*i] の性質(範囲外を指定してもエラーにならず、ある分だけ取得してくれる))
発想を転換(というより整理)し、以下の2点を修正します。
-
状態を「追加」ではなく「上書き」する
1回のシャッフルごとに新しい空のリスト(next_cards)を用意し、1回分のシャッフルが終わったら現在の cards を next_cards で丸ごと上書きする。 -
デバッグコードの削除
焦って固定値にしてしまった [-10:] を消し、素直にリスト全体を出力する。
3. 解答コード
自分の書き方に則ったうえで、ちゃんときれいなものに直すとこうなります。
import sys
# 入力の受け取り
N, M, K = list(map(int, sys.stdin.read().split()))
# 初期状態のカード
cards = [i + 1 for i in range(N)]
# セット数の計算(あなたのロジックそのままです!)
if N % M > 0:
number = N // M + 1
else:
number = N // M
# K回シャッフルを繰り返す
for _ in range(K):
next_cards = []
# 下のセットから順に next_cards に積んでいく
for i in range(number, 0, -1):
next_cards.extend(cards[M*(i-1):M*i])
# 1回シャッフルし終わった束を、現在の束として上書きする
cards = next_cards
# 結果を改行区切りで出力
print(*cards, sep="\n")
4. さらに Python らしさを極めるなら
Pythonでは range(start, stop, step) の第3引数(ステップ幅)を使うと、面倒な端数計算(N % M)やインデックス計算を省略して、より直感的に書くことができます。
import sys
N, M, K = map(int, sys.stdin.read().split())
cards = list(range(1, N + 1))
for _ in range(K):
# 1. range(0, N, M) を使って、端数計算なしでM枚ごとのチャンク(塊)に分割する
chunks = [cards[i:i+M] for i in range(0, N, M)]
# 2. chunks[::-1] で束の順番を逆転させる
# 3. sum(2次元リスト, []) を使って、2次元配列を1次元に平坦化(flatten)して上書き
cards = sum(chunks[::-1], [])
print(*cards, sep="\n")
5. 学んだこと・まとめ
-
状態シミュレーションは「丸ごと上書き」が基本
リストを更新し続けるシミュレーション問題では、1つのリストに extend 等で継ぎ足すのではなく、「一時的なリスト(next_cards)を作って最後に丸ごと入れ替える」方が状態管理がシンプルになります。 -
提出前のハードコード(デバッグ用固定値)の指差し確認
テスト用に設定した [-10:] のような固定値は、焦っていると本当に消し忘れます。コードをツギハギした時ほど、出力直前の変数がどうなっているか冷静に確認する癖をつけたいです。 -
range のステップ幅を活用する
リストを一定数ごとに分割したい時は、自力で割り算のインデックス計算をするよりも range(0, N, M) を使うと、コードが短くなり端数バグも防げます。