AtCoder 東京海上日動 プログラミングコンテスト2020 E - O(rand)
atcoder.jp 公式解説がわかりにくかったので自分なりに書いてみます。
問題概要
が与えられます。
ここから重複なく~個選び、下記の条件を満たす方法は何通りあるか求めよ。
・選んだ数の論理積が
・選んだ数の論理和が
制約
方針
- に関する問題の単純化
- 数え上げ方針の検討
- 数え上げの実装
1. に関する問題の単純化
まずについて観察します。
各ビットごとに見たとき、の桁目が、の桁目がだったとします。
すると、下記の場合分けができます。
①
→ 選んだ数は全て桁目が0でなくてはならない。
②
→ 選んだ数の桁目に0,1両方が混ざっている必要がある。
③
→ このような選び方は存在しない。
→ この組み合わせがあった場合は答えが0通りで確定
④
→ 選んだ数は全て桁目が1でなくてはならない。
③はなかったものとして、①と④を考えると、
桁目の数字に着目することで選ぶことのできない数字を取り除くことができます。
例えば入力例2を見ると、これは1桁目が④、2,3桁目が②、4桁目以降が①になっている例です。
よって、1桁目が1ではないは使うことができません。
同様に、4桁目が0ではないも使うことができません。
逆にいうと、それ以外の数字を使えば1,4桁目の条件はいつでも満たすことができます。
以上のことから、使える数字だけを選定し、②以外の桁を右に詰めてしまうことにしましょう。
先の入力例2は下記のように変わります。
このような前処理をすることで、問題を下記のように言い換えることができました。
として、1~個の数を選ぶ。
各桁ごとに選んだ数のビットが0か1かをカウントしたとき、全ての桁で0,1両方が1回以上出現しているような選び方は何通りか
※は②を満たす桁の数
2. 数え上げ方針の検討
ここからが難しいです。
結論から言うと、桁ごとが満たすべき条件に着目し、包除原理を使って解きます。
まず、「全ての桁で0,1両方が1回以上出現しているような選び方」ですが、 これは各桁ごとに独立な事象の重ね合わせです。 つまり、下記で表現できます。
「1桁目で0,1両方が1回以上出現しているような選び方」
「2桁目で0,1両方が1回以上出現しているような選び方」
「桁目で0,1両方が1回以上出現しているような選び方」
また、「0,1両方が1回以上出現しているような選び方」は扱いづらいです。
「0だけ」「1だけ」「両方」の3状態を考えないといけないので状態量がになってしまいます。
ここで 、余事象を考えると扱いやすくなります(後述)。
言語化すると「"0だけが出現している"or"1だけが出現している"ような選び方」になります。
すると求める答えは下記に言い換えられます。
「1桁目で"0だけが出現している"or"1だけが出現している"ような選び方」
「2桁目で"0だけが出現している"or"1だけが出現している"ような選び方」
「桁目で"0だけが出現している"or"1だけが出現している"ような選び方」
の余事象
これで包除原理が使えるような形に持ち込むことができました。
桁目が「"0だけが出現している"or"1だけが出現している"ような選び方」を満たしているとき、、満たしていてもいなくてもどちらでも良いとき とします。
を一つ決めたときに、
そのような条件を満たす数の選び方をで定めます。
の通りの選び方全体をとすると、
(包除原理については詳しい説明は省きますが、)答えは下記で求めることができます。
あとは、が求められれば良いです。
3. 数え上げの実装
余事象を考えると扱いやすくなる、と先ほど述べたポイントです。
を、桁目がの2進数だと考えます。
例えばは1010です。
このケースだと、「2桁目で"0だけが出現している"or"1だけが出現している"ような選び方」 かつ「4桁目で"0だけが出現している"or"1だけが出現している"ような選び方」 の数がになります。
具体例を見てみましょう。
は下記の場合分けの合計になります。
・(2桁目,4桁目) = (0,0)
0000,0001,0100,0101 などからいくつか選ぶ方法
→ となるような数からいくつか選ぶ
・(2桁目,4桁目) = (0,1)
1000,1001,1100,1101 などからいくつか選ぶ方法
→ となるような数からいくつか選ぶ
・(2桁目,4桁目) = (1,0)
0010,0011,0110,0111 などからいくつか選ぶ方法
→ となるような数からいくつか選ぶ
・(2桁目,4桁目) = (1,1)
1010,1011,1110,1111 などからいくつか選ぶ方法
→ となるような数からいくつか選ぶ
このようにみると、をビットマスクと捉えて、
をマスク処理した後に同値になるグループのサイズがわかれば、が計算できることがわかります。
(いくつか選ぶ=1~K個選ぶ方法の数は二項係数の和で計算できます。)
よって、ビットマスクの値を一つ固定し、個のにそれぞれマスク処理をして、同値になるグループごとに選び方を計算する、という処理をすることでこの問題を解くことができます。
ビットマスクの数が、
マスク処理の回数が
1回のマスク処理で桁処理する(※)ので、
これらはで計算できます。
※ この処理はbit演算で速い、かつ桁数も少ないので致命的にはならなそう
※※ 二項係数の和はで事前計算しておきます。(TLE1回)
n,k,s,t = map(int,input().split()) a = list(map(int,input().split())) # 二項係数の事前計算 fac = [1] * (n+1) for i in range(1,n+1): fac[i] = fac[i-1] * i com = [[0] * (n+1) for _ in range(n+1)] for i in range(n+1): for j in range(i+1): com[i][j] = fac[i] // (fac[j] * fac[i-j]) com_n = [0] * (n+1) for i in range(n+1): for j in range(1,k+1): com_n[i] += com[i][j] # 要らない数、桁の圧縮 use = [-1] * 18 for i in range(18): si = (s >> i) & 1 ti = (t >> i) & 1 if si == 0 and ti == 0: use[i] = 0 elif si == 0 and ti == 1: use[i] = 2 elif si == 1 and ti == 0: print(0) exit() else: use[i] = 1 a2 = [] for ai in a: new = 0 for j in range(17,-1,-1): x = (ai >> j) & 1 if use[j] == 0 and x == 1: break if use[j] == 1 and x == 0: break if use[j] == 2: new = new*2 + x else: a2.append(new) # 包除原理で数え上げ l = use.count(2) # 圧縮後の桁数 ans = 0 for mask in range(2**l): group_cnt = dict() for ai in a2: masked = ai & mask if masked in group_cnt: group_cnt[masked] += 1 else: group_cnt[masked] = 1 pc = bin(mask).count('1') for j in group_cnt.values(): ans += (-1) ** pc * com_n[j] print(ans)
感想
余事象に言い換えるところが全く発想になく、の世界で悩み続けていました。
もし言い換えができたとしても、包除原理できることに気づく、ビットマスクを使うとが数え上げられる、なども発想の飛躍があり厳しいです。
ちなみに、公式解説だと計算量となっており、を外せるらしいのですが、そこの工夫はよくわかっていません。
余事象に言い換えるところはmaspyさんのブログを参考にしました。