AtCoderのXORを基礎から理解|ABC213 A・ABC171 EをPythonで解く

読了 約8分 たびすけ
AtCoderのXORを基礎から学ぶ。ABC213 AからABC171 Eへ進む図

次に読む記事

関連するテーマの記事を、先に確認できます。

XOR問題でまず使えるようにしたいのは、同じ値を2回XORすると消えるという性質です。この性質が分かると、式から未知の値を取り出す問題と、全体のXORから一つの値を復元する問題を同じ考え方で解けます。

この記事では、入門のABC213 Aで一つの式を変形し、次にABC171 Eで全体XORの再利用へ進みます。どちらも公式の入力形式で動くPython参考コードまで確認します。

XOR問題で最初に覚える3つの性質

XOR(排他的論理和)は、二進数の各桁を比べ、片方だけが1ならその桁を1にする演算です。Pythonでは^を使います。定義と具体例は、ABC213 Aの公式問題文でも確認できます。

xyx ^ y
000
011
101
110

たとえば3は二進数で0115101です。桁ごとにXORを取ると110になり、十進数では6です。

2進数
3011
5101
3 ^ 5110(十進数の6)

問題を解くときは、次の3つを使います。

  • 同じ値同士ではx ^ x = 0になる。
  • 0とのXORではx ^ 0 = xになる。
  • 計算する順序と括り方を変えられる。

この3つを合わせると、同じ値が2回ある部分を隣り合わせにして0へ消せます。以降の2問では、この「同じ値を消す」操作を別の形で使います。

ABC213 A:未知のCをXORで取り出す

ABC213 A – Bitwise Exclusive Orでは、0以上255以下のABが与えられ、A ^ C = Bを満たすCを求めます。

足し算の方程式で両辺から同じ値を引くように、XORでは両辺に同じ値をXORします。この導出はABC213 Aの公式解説にも示されています。

A ^ C = Bの両辺にAをXORすると、左辺はA ^ A ^ Cです。A ^ A = 0なのでAが消え、次の式になります。

C = A xor B

公式の入力例A = 3B = 6を追うと、C011 ^ 110 = 101、つまり5です。元の式へ戻すと3 ^ 5 = 6になり、条件を満たします。

A, B = map(int, input().split())
print(A ^ B)

この参考コードは、公式の2つの入力例3 610 12で、それぞれ56を出力することを確認しています。

正しさ:出力をC = A ^ Bとすると、A ^ C = A ^ A ^ B = 0 ^ B = Bです。したがって、出力したCは問題の条件を満たします。

計算量:制約内の整数1組にXORを1回行うため、時間計算量はO(1)、追加メモリはO(1)です。

ABC171 E:全体XORを一度だけ計算する

ABC171 E – Red Scarfでは、偶数N個の未知の値b1からbNがあります。入力のaiは、biだけを除いた全要素のXORです。この情報から全てのbiを復元します。

Nは最大200000です。各iについて「自分以外」を最初から計算すると、要素を約N回ずつ見るためO(N2)になります。そこで、入力全体のXORを一度だけ計算します。

入力全体のXORをSとします。

S = a1 xor a2 xor … xor aN

この式の中で、未知の値bjが何回現れるかを数えます。bjajには含まれず、それ以外の全てのaiに含まれるため、出現回数はN - 1回です。

問題ではNが偶数なので、N - 1は奇数です。同じ値を偶数回XORした部分は2個ずつ消え、奇数回なら最後に1個だけ残ります。したがって、Sは未知の値全体のXORと一致します。この導出はABC171の公式解説PDF(E: Red Scarf)で確認できます。

S = b1 xor b2 xor … xor bN

さらに、SaiをXORします。aiにはbi以外の全要素が入っています。Sと重ねると、それらは2回ずつ現れて消え、biだけが残ります。

bi = S xor ai

公式サンプルを手で追う

公式サンプルの入力列は20 11 9 24です。まず全体XORを順に計算します。

計算結果
20 ^ 1131
31 ^ 922
22 ^ 2414

S14です。各入力とSをXORすると、次の値を復元できます。

i計算bi
114 ^ 2026
214 ^ 115
314 ^ 97
414 ^ 2422

得られた26 5 7 22は公式の出力例と一致します。たとえば1番目を除いた5 ^ 7 ^ 2220となり、入力のa1へ戻ります。

Python参考コード

N = int(input())
a = list(map(int, input().split()))

s = 0
for x in a:
    s ^= x

print(*(s ^ x for x in a))

最初のループでSを求め、最後に各aiからbiを作って出力します。この参考コードは公式サンプルを無加工で入力し、26 5 7 22を出力することを確認しています。

正しさ:Sには全てのbjが1回ずつ含まれ、aiにはbi以外が1回ずつ含まれます。S ^ a_iではjiでない全てのbjが2回になって消えるため、biだけが残ります。

計算量:N個の入力を1回走査し、N個の答えを出力するため、時間計算量はO(N)です。掲載コードは入力列をリストとして保持するため、メモリはO(N)です。

次のXOR問題で見る2つの形

今回の2問から、次の問題で探す形を二つに整理できます。

  1. 未知の値を取り出す:既知の値 ^ 未知の値 = 結果という式なら、両側に同じ既知の値をXORできないか考える。
  2. 一つを除いた値を求める:各位置について最初から計算せず、全体XORを一度作り、対象の値をもう一度XORして消す、または戻す。

一方、ABC126 F – XOR Matchingは、各値を2回含む列そのものを作り、等しい値の間のXOR条件も満たす問題です。一つの未知数を取り出すだけではなく、解が存在するかと、どの順番に並べるかを分けて考える必要があります。今回の2問を説明なしで実装できるようになった後の発展問題として読むと、難しさの違いが見えます。

コードを見る前に式を1行書く

まずABC213 Aを、参考コードを隠してC = A ^ Bまで自分で導いてみてください。次にABC171 Eの公式サンプルで、Sと各biを紙に書いてから実装します。

式を先に書くと、コードのs ^= xが単なる書き方ではなく「全体XORを作る処理」で、s ^ xが「同じ値を消して必要な値を残す処理」だと対応づけられます。この対応を説明できれば、別のXOR問題でも使う性質を選びやすくなります。