C - スイッチと電球 / Switches and Light Bulbs Editorial by admin Editorial for printing
Claude Opus 5 (High)概要
\(H \times W\) のマス目にある電球を、指定されたマスとその上下左右を反転する操作を \(Q\) 回行い、最終状態を求める問題です。各操作で影響を受けるのは最大 \(5\) マスだけなので、素直にその \(5\) マスだけを反転すれば十分です。
考察
重要な観察
まず、電球の状態は「反転」だけで変化することに注目します。反転は XOR(排他的論理和)そのものなので、
- 同じマスを \(2\) 回反転すると元に戻る
- 反転の順番は結果に影響しない
という性質があります。したがって「操作を順番にシミュレートする」ことと「各マスが何回反転されたかを数えて、偶奇だけ見る」ことは同じ結果になります。
素朴なアプローチとその問題点
一見ありそうな失敗パターンは次の \(2\) つです。
(1) 毎回グリッド全体を走査してしまう
「操作のたびに全マスを見て、対象かどうか判定して反転する」という実装をすると、\(O(QHW)\) = 最大 \(10^5 \times 10^6 = 10^{11}\) となり全く間に合いません。
しかし実際には、\(1\) 回の操作で状態が変わるのは $\((r, c),\ (r, c-1),\ (r, c+1),\ (r-1, c),\ (r+1, c)\)\( の**最大 \)5\( マスだけ**です。ここだけを直接書き換えれば、\)1\( 回の操作は \)O(1)$ で処理できます。
(2) 文字列を毎回作り直してしまう
Python で S[i] = S[i][:j] + ('1' if ... ) + S[i][j+1:] のように文字列を組み替えると、\(1\) 回あたり \(O(W)\) のコピーが発生します。\(W\) は最大 \(10^6\) になり得るので、\(Q = 10^5\) 回では致命的です。Python の文字列はイミュータブル(変更不可)なので、可変なデータ構造を使う必要があります。
解決方針
- グリッドを 1 次元の可変バイト列(
bytearray) として保持する。 - マス \((i, j)\)(\(1\)-indexed)は添字 \(p = (i-1) \times W + (j-1)\) に対応させる。
- 反転は
grid[p] ^= 1の \(1\) 命令で行える。
ここで少し技巧的なのは、bytearray に文字 '0', '1' をそのまま格納する点です。ASCII コードは '0' = \(48 = \texttt{0b110000}\)、'1' = \(49 = \texttt{0b110001}\) なので、最下位ビットだけが違います。よって ^= 1 するだけで '0' ↔ '1' が入れ替わり、数値と文字の変換が一切不要になります。出力時もそのままバイト列として書き出せます。
アルゴリズム
- 入力を
sys.stdin.buffer.read().split()で一括読み込みする。 - \(S_1, \dots, S_H\) を連結して長さ \(HW\) の
bytearrayを作る(これがグリッド)。 - 各操作 \((R_k, C_k)\) について、\(p = (R_k-1)W + (C_k-1)\) とし、
grid[p] ^= 1- \(C_k > 1\) なら
grid[p-1] ^= 1(左) - \(C_k < W\) なら
grid[p+1] ^= 1(右) - \(R_k > 1\) なら
grid[p-W] ^= 1(上) - \(R_k < H\) なら
grid[p+W] ^= 1(下)
- 最後に
gridを \(W\) 文字ずつに区切り、改行を挟んで一括出力する。
具体例
\(H = W = 3\)、初期状態が
000
000
000
のとき、\((2,2)\) のスイッチを押すと \(p = 1\cdot 3 + 1 = 4\) で、\(p, p-1, p+1, p-3, p+3\) = \(4, 3, 5, 1, 7\) が反転し、
010
111
010
となります。さらに \((1,1)\) を押すと \(p = 0\)、左と上は範囲外なので \(0, 1, 3\) が反転し、
100
011
010
です。
計算量
- 時間計算量: \(O(HW + Q)\)
- 空間計算量: \(O(HW)\)
\(HW \le 10^6\)、\(Q \le 10^5\) なので十分高速です。
実装のポイント
入出力を高速化する: \(HW \le 10^6\) かつ \(Q \le 10^5\) なので、
input()を毎行呼ぶのは遅いです。sys.stdin.buffer.read().split()で全入力を一気に読み、出力もbytearrayに溜めてsys.stdout.buffer.writeで一度に書き出します。2 次元リストではなく 1 次元
bytearray: リストのリストにするとメモリと参照コストが増えます。\(p = (i-1)W + (j-1)\) の添字計算で 1 次元化すると高速かつ省メモリです。ASCII の性質を利用した反転:
'0'(48) と'1'(49) は最下位ビットのみ異なるので^= 1で反転できます。int変換やstr生成が不要になり、出力もそのまま行えます。境界判定を忘れない: 端のマスでは隣接マスが存在しません。特に \(1\) 次元化しているため、
c == 1のときにp-1を触ると前の行の末尾を誤って反転してしまいます(範囲外エラーにならず WA になるので危険です)。if c > 1,if c < W,if r > 1,if r < Hの \(4\) つの判定を必ず入れましょう。\(W\) は入力から直接与えられない場合に注意: 制約に \(W\) の上限は明示されていませんが \(HW \le 10^6\) なので、\(W\) 自体は \(10^6\) まであり得ます。\(H = 1\) の細長いケース、\(W = 1\) の縦長ケースの両方が境界判定で正しく動くか確認しましょう。
出力の連結: 行ごとに
printを \(H\) 回(最大 \(2\times10^4\) 回)呼ぶのも遅いので、memoryviewでスライスしながらbytearrayに追記し、最後にまとめて出力するのが安全です。ソースコード
import sys
def main():
data = sys.stdin.buffer.read().split()
H = int(data[0]); W = int(data[1])
grid = bytearray(b''.join(data[2:2 + H]))
Q = int(data[2 + H])
idx = 3 + H
for _ in range(Q):
r = int(data[idx]); c = int(data[idx + 1]); idx += 2
p = (r - 1) * W + (c - 1)
grid[p] ^= 1
if c > 1:
grid[p - 1] ^= 1
if c < W:
grid[p + 1] ^= 1
if r > 1:
grid[p - W] ^= 1
if r < H:
grid[p + W] ^= 1
mv = memoryview(grid)
out = bytearray()
for i in range(H):
out += mv[i * W:(i + 1) * W]
out += b'\n'
sys.stdout.buffer.write(bytes(out))
main()
この解説は claude5opus-high によって生成されました。
posted:
last update: