情報の坩堝的な

メモがわりに記事を書きます。たまにQiitaにも同じ内容を投稿します。

AGC 045 A Xor Battle 解説

はじめに

  • 当日
    • 20:55 よーし、AGCがんばるぞー
    • 21:00 コンテスト開始
    • 21:30 なんもわからん。コンテスト終わったら解説読むか
  • 翌日以降: 誰も解説してくれないんだが...?

その後、おおよそ理解したのでメモ。

事前知識

XOR関連

XOR(排他的論理和)とは

詳細はWikiで。 要点は以下の真理値表(ここで⊕は問題文にならってXORに対応)

ABA⊕B
000
011
101
110

特に、

  • ABが同じ: 0
  • ABが異なる: 1
  • XORを偶数回操作すると元に戻る: $A \oplus B \oplus B=A$

です。

自分自身が逆元である

単位元を0とすると、二進数表記のAに対する逆元はA自身です。 例えば、$A=00101$とします。自分自身とのXORをとってみましょう。

$$A \oplus A=00000$$

詳しい証明などはこちらを参照してください。

ベクトルの線型結合

線形従属・線形独立

線形代数の基礎知識、線形従属・線形独立の理解が必要です。 Wikiさんとか理数アラカルトさんに譲ります。

ここでは、具体例を交えながら簡単に解説します。 3次元空間を考えます。 ベクトル${\bf v}_{1}, {\bf v}_{2}$があるとしましょう。ただし、${\bf v}_{1}=(0, 0, 1)^\top, {\bf v}_{2}=(0, 1, 0)^\top$とします。 このとき 、${\bf v}_{3}$が${\bf v}_{1}, {\bf v}_{2}$の線形和で表せるかを考えます。

  • case 1: ${\bf v}_{3}=(0, 1, 1)^\top$

→単純に${\bf v}_{1}+{\bf v}_{2}$で${\bf v}_{3}$で表すことができます。 このとき、${\bf v}_{3}$は${\bf v}_{1}, {\bf v}_{2}$と線形従属な関係にあります。

  • case 2: ${\bf v}_{3}=(1, 0, 0)^\top$

→このケースはいくら${\bf v}_{1}, {\bf v}_{2}$をこねくりまわしても、${\bf v}_{3}$を表現することができません。 このとき、${\bf v}_{3}$は${\bf v}_{1}, {\bf v}_{2}$と線形独立な関係にあります。

続いて、XOR演算でこの特性を確認していきましょう。

XORにおける従属関係の確認方法

よく見ると、先程3次元で定義していたベクトル${\bf v}$というのは、二進数表記の整数に見えますね(見えてください)。

つまり、${\bf v}_{1}=(0, 0, 1) \rightarrow e_1=001, {\bf v}_{1}=(0, 1, 0) \rightarrow e_2=010$に見えます。 このとき 、ある整数の二進数表記$v$が、$e_1, e_2$のXORのみで表現できるかはどのように確認できるでしょうか?

  • case 1: $v=011$

→単純に$e_1 \oplus e_2$で表すことができます。すなわち、$v$は既存の基底と従属な関係にある。

  • case 1: $v=110$

→$e_1, e_2$をこねくり回しても表現できません。すなわち、$v$は既存の基底と独立な関係にある。ただし、追加で$e_3=100$という基底があれば表現可能です。

また、値の大きい基底から順番に、基底の最上位bitが$v$で立っているかを確認することで、従属関係を確認できます*1

具体的なコードは以下。

base = [1, 2]
v = int(input())

for e in sorted(base, reverse=True):
    # print('v  : {:2d} {:05b}'.format(v, v))
    # print('e  : {:2d} {:05b}'.format(e, e))
    # print('e^v: {:2d} {:05b}'.format(v ^ e, v ^ e))
    # print('-'*16)
    v = min(v, e ^ v)
# print('v  : {:2d} {:05b}'.format(v, v))

if v == 0:
    print('従属!')
if v > 0:
    print('独立!')

※print前のコメントアウトを消すと変数の細かな動きを確認できます。

ここで、minが掃き出し処理に相当します*2

動作例

3
v  :  3 00011
e  :  2 00010
e^v:  1 00001
----------------
v  :  1 00001
e  :  1 00001
e^v:  0 00000
----------------
v  :  0 00000
従属!

また、重要な知見として、計算結果の変数vは、入力値vを表現するために追加で必要な基底にあたります

解説

発想と方針

  • 発想
    • $A_{1:i-1}$に関係なく、$A_i$を$A_{i+1:N}$のXORで表現できなければ、0さんは詰み
    • $A_i$を$A_{i+1:N}$で表現できなくする動作をしたら、1さんの勝ち
  • 方針
    • 後ろから$A_i$を$A_{i+1:N}$で表現できるかチェック
      • $A_{i+1:N}$の基底で貼る空間に$A_i$が収まっている(従属)か?
    • 表現できるなら、0さん1さん関係なくpass
    • 表現できない場合
      • 0さん: $A_i$を使用(=基底を追加)
      • 1さん: $A_i$を使わないことで勝利が確定

ACコード

前項の発想と方針を実装すると以下になります。

def solve():
    base = []
    for a, s in zip(A[::-1], S[::-1]):
        # print(f'a: {a}, s: {s}')
        # for i, y in enumerate(base):
        #     print('base{:d} {:3d}: {:05b}'.format(i, y, y))
        x = a
        for y in sorted(base, reverse=True):
            x = min(x, x ^ y)
        if x:
            if s == "0":
                # print('=>add {:3d}: {:05b}'.format(x, x))
                base.append(x)
            else:
                return 1
    return 0


t = int(input())
for _ in range(t):
    n = int(input())
    *A, = map(int, input().split())
    S = input()
    # print(f'A: {A}, S: {S}')
    print(solve())
    # print('-'*32)

※print前のコメントアウトを消すと変数の細かな動きを確認できます。

こんな感じ。baseで表現できなければ、baseにaddされていきます。

A: [2, 3, 4, 5, 6, 7], S: 111000
a: 7, s: 0
=>add   7: 00111
a: 6, s: 0
base0   7: 00111
=>add   1: 00001
a: 5, s: 0
base0   7: 00111
base1   1: 00001
=>add   2: 00010
a: 4, s: 1
base0   7: 00111
base1   1: 00001
base2   2: 00010
a: 3, s: 1
base0   7: 00111
base1   1: 00001
base2   2: 00010
a: 2, s: 1
base0   7: 00111
base1   1: 00001
base2   2: 00010
0

わからないこと

従属関係を確認する際に、appendした順に比較を行うこと(つまり、sorted(base, reverse_True) -> base)でもACとなるのはなぜか?

おわりに

AGC難しい...。 間違っている箇所などあればご指摘ください。

参考資料

*1:こちらなどを参考にしました

*2:XORをとったときに$v$が小さくなる→ある基底$e$の最上位bitが$v$上で1であるため