Official

C - カードの重ね置き / Stacking Cards Editorial 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 値に対応する番号を取得し、その後で辞書の値を現在の操作番号に更新すれば、常に「直近の番号」を取得できます。

アルゴリズム

  1. データの準備:
    • カードの値を管理するスタック(リスト)
    • 現在の XOR 総和を保持する変数 current_xor(初期値 0)
    • XOR 値と LOOK 番号を紐付ける辞書 last_seen
    • LOOK 操作の回数を数える変数 look_count
  2. 各操作の処理:
    • PUT c: スタックに \(c\) を追加し、current_xor ^= c を行う。
    • REMOVE: スタックから値を pop し、その値を current_xor に XOR する。
    • LOOK:
      1. look_count をインクリメントする。
      2. last_seencurrent_xor が存在すればその値を記録し、なければ -1 を記録する。
      3. last_seen[current_xor] を現在の look_count で更新する。
  3. 結果の出力: 記録した結果を順に出力する。

計算量

  • 時間計算量: \(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 によって生成されました。

posted:
last update: