C - カードの重ね置き / Stacking Cards 解説 by admin
Gemini 3.0 Flash (Thinking)概要
この問題は、スタック(山)の状態に応じて変化する「全要素のビットごとの XOR(排他的論理和)」を管理し、各確認操作において過去に同じ XOR 値が現れた直近のタイミングを特定する問題です。
考察
1. XOR の性質を利用する
「現在の山にあるすべてのカードの XOR 和」を毎回最初から計算すると、山にあるカードの枚数を \(M\) としたとき、1回の LOOK 操作に \(O(M)\) かかってしまいます。最悪の場合 \(M\) は \(N\) に比例するため、全体の計算量が \(O(N^2)\) となり、実行時間制限に間に合いません。
しかし、XOR には 「同じ値を2回 XOR すると元に戻る(\(A \oplus B \oplus B = A\))」 という重要な性質があります。これを利用すると、現在の XOR 和を以下のように効率的に更新できます。
- PUT c: 現在の XOR 和に \(c\) を XOR する。
- REMOVE: 山の一番上の値を \(v\) とすると、現在の XOR 和に \(v\) を XOR する(これにより \(v\) の影響が打ち消される)。
これにより、各操作を \(O(1)\) で処理できるようになります。
2. 直近の同じ値を高速に見つける
各 LOOK 操作で得られた XOR 値について、「過去に同じ値がいつ現れたか」を記録しておく必要があります。これには 連想配列(Python の辞書) を使うのが最適です。
- キー:これまでに現れた XOR 値
- 値:その XOR 値が最後に現れた LOOK 操作の番号
LOOK 操作が行われるたびに、辞書から現在の XOR 値に対応する番号を取得し、その後で辞書の値を現在の操作番号に更新すれば、常に「直近の番号」を取得できます。
アルゴリズム
- データの準備:
- カードの値を管理するスタック(リスト)
- 現在の XOR 総和を保持する変数
current_xor(初期値 0) - XOR 値と
LOOK番号を紐付ける辞書last_seen LOOK操作の回数を数える変数look_count
- 各操作の処理:
PUT c: スタックに \(c\) を追加し、current_xor ^= cを行う。REMOVE: スタックから値をpopし、その値をcurrent_xorに XOR する。LOOK:look_countをインクリメントする。last_seenにcurrent_xorが存在すればその値を記録し、なければ-1を記録する。last_seen[current_xor]を現在のlook_countで更新する。
- 結果の出力: 記録した結果を順に出力する。
計算量
- 時間計算量: \(O(N)\)
- 各操作(PUT, REMOVE, LOOK)は、スタックの操作や辞書の読み書きを含めてすべて平均 \(O(1)\) で処理されます。操作回数は \(N\) なので、全体で \(O(N)\) となります。
- 空間計算量: \(O(N)\)
- スタック、辞書、および結果を格納するリストのサイズは最大で \(N\) に比例します。
実装のポイント
高速な入出力: \(N\) が最大 \(2 \times 10^5\) と大きいため、Python では
input()ではなくsys.stdin.read().split()を使って一括で入力を読み込み、sys.stdout.write()で一括出力することで実行時間を大幅に短縮できます。初期値: XOR の単位元は \(0\) なので、
current_xorの初期値は \(0\) とします。ただし、問題文の条件よりLOOK時には必ずカードが1枚以上あるため、空の状態の XOR 値が参照されることはありません。ソースコード
import sys
def solve():
# 標準入力からすべてのデータを一度に読み込み、空白で分割してリストにする
# これにより、大量の入力に対しても高速に処理が可能
input_data = sys.stdin.read().split()
if not input_data:
return
# 操作の総回数 N
n = int(input_data[0])
# カードの山を管理するスタック
stack = []
# 現在の山にあるすべてのカードの XOR 和
current_xor = 0
# 各 XOR 値が最後にどの確認操作(LOOK)で現れたかを記録する辞書
# キー: XOR 値, 値: 確認操作の番号(1-indexed)
last_seen = {}
# 確認操作の回数をカウント
look_count = 0
# 各確認操作の結果を格納するリスト
results = []
# 入力リストを走査するためのインデックス
idx = 1
for _ in range(n):
op = input_data[idx]
idx += 1
if op == 'PUT':
# カードを追加する操作
val = int(input_data[idx])
idx += 1
stack.append(val)
# XOR の性質: A ^ B ^ B = A なので、追加時はそのまま XOR をとる
current_xor ^= val
elif op == 'REMOVE':
# カードを取り除く操作(山に1枚以上あることが保証されている)
top = stack.pop()
# 取り除くカードの値を XOR することで、その値の影響を消す
current_xor ^= top
elif op == 'LOOK':
# 現在の XOR 和を確認する操作(山に1枚以上あることが保証されている)
look_count += 1
# 同じ XOR 値が過去に現れたか辞書で確認
# 存在すればその番号を、なければ -1 を取得
prev_look_idx = last_seen.get(current_xor, -1)
results.append(prev_look_idx)
# 現在の確認操作の番号で辞書を更新
last_seen[current_xor] = look_count
# すべての確認操作の結果を改行区切りで一括出力
if results:
sys.stdout.write('\n'.join(map(str, results)) + '\n')
if __name__ == '__main__':
solve()
この解説は gemini-3-flash-thinking によって生成されました。
投稿日時:
最終更新: