bit 全探索は、競技プログラミングにおいて非常に重要なテクニックの一つです。
「0」と「1」で表す2進数の考え方を使って、すべての組み合わせを試します。
このテクニックを使うことで、難しい問題を効率的に解決することができます。
この記事では、bit 全探索の基礎知識から具体例なコードも使ってわかりやすく解説します。
C++ のコードで bit 全探索について解説した記事もありますので、ぜひ参考にしてください。
bit とは
bit は、「0」と「1」で表す情報の最小単位です。
コンピュータでは、bit が電気信号の「オン」と「オフ」のように2つの状態で情報を管理しています。
たとえば、4ビットは連続した4桁の bit で「1011」のようなものです。
「1011」は、左から2番目の bit が 0 で、残りの bit が 1 であることを表しています。
このような bit の集合を使えば、複数のフラグの状態を表すことができます。
ビットでフラグを管理する
3ビットの「101」について考えてみます。一番左と、一番右が「1」で、真ん中が「0」です。
たとえば、「赤、青、黄」の3つのボールから何個か選ぶとき、3ビットで表す bit の「1」が選んだとき、「0」が選ばなかったときと考えることができます。
- bit が 1:選ぶ
- bit が 0:選ばない
「101」だと「赤、青、黄」なので、一番左の赤のボールと、一番右の黄のボールを選んだと表現できます。
「001」だと、一番右の黄のボールだけ選んだと表現できます。
bit 全探索とは
bit の集まりを使って、選ぶか選ばないかを管理することができました。
このように bit の集まりを使って、すべての組み合わせについて探索するのが bit 全探索です。
3つのボールからいくつか選ぶ組み合わせ
「赤、青、黄」の3つのボールから、いくつかのボールを選ぶときの組み合わせについて考えてみます。
この組み合わせは全部で8通りあります。そのときの bit の集まりで「選ぶ(1)」か「選ばない(0)」かの対応を表してみます。
- 選 ば な い:0 0 0
- 黄:0 0 1
- 青 :0 1 0
- 青 黄:0 1 1
- 赤 :1 0 0
- 赤 黄:1 0 1
- 赤 青 :1 1 0
- 赤 青 黄:1 1 1
1つのボールについて「選ぶ」、「選ばない」の2通りの選び方があります。
- 赤のボール:「選ぶ」、「選ばない」の2通り
- 青のボール:「選ぶ」、「選ばない」の2通り
- 黄のボール:「選ぶ」、「選ばない」の2通り
全体の選び方としては、「2通り * 2通り * 2通り = 8通り」→「23 = 8」になります。
bit で考えると、すべて選ばないときの「000」からすべて選ぶときの「111」までを試せば、すべての組み合わせについて列挙できます。
2進数
2進数は、「0」と「1」の2つの数字だけを使って数を表現します。
今回の「000」や「111」などは2進数になります。私たちが普段使っているのは10進数で、0から9までの数字を使って数を表現します。
さきほどの bit の集まり(2進数)を10進数に変換すれば、次のようになります。
- 選 ば な い:0 0 0 → 0
- 黄:0 0 1 → 1
- 青 :0 1 0 → 2
- 青 黄:0 1 1 → 3
- 赤 :1 0 0 → 4
- 赤 黄:1 0 1 → 5
- 赤 青 :1 1 0 → 6
- 赤 青 黄:1 1 1 → 7
「000」から「111」までのすべての組み合わせは、10進数の「0」から「7」までを2進数に変換したものになります。
つまり、「000」から「111」までを試したいときは、10進数の「0」から「7」を2進数に変換すれば試すことができるわけです。
もし、ボールの数が4つになれば、選び方は「2通り * 2通り * 2通り * 2通り= 16通り」→「24 = 16」です。bit では、「0000」から「1111」(10進数だと0から15)までの組み合わせです。
ボールの数が5つだと「2 * 2 * 2 * 2 * 2 = 32通り」→「25 = 32」。bit では、「00000」から「11111」(10進数だと0から31)まで。
ボールの数が5つだと「2 * 2 * 2 * 2 * 2 *2 = 64通り」→「26 = 64」。bit では、「000000」から「111111」(10進数だと0から63)まで。
このように、N 個の中からいくつか選ぶときの組み合わせは「2N 通り」の選び方があります。
コード
実際の bit 全探索のコードについて、見ていきます。
3つのボールの組み合わせについて、すべて列挙する Python のコードです。
# 3つのボール
ball = ['黄', '青', '赤']
# 選ぶボールの数
n = len(ball)
# 0 から 2 の n 乗までのループ ( 2**3 = 8 )
for i in range(2**n):
# 表示用(何番目の組み合わせか)
print(f'{i} : ',end='')
# n 個の中からどれが選ばれているか(どの bit が 1 なのか)
for j in range(n):
# 選ばれているとき
if i & (1 << j):
# 表示用(選ばれているボール)
print(ball[j], end=' ')
# 表示用(次の組み合わせのための改行)
print()
"""実行結果
0 :
1 : 黄
2 : 青
3 : 黄 青
4 : 赤
5 : 黄 赤
6 : 青 赤
7 : 黄 青 赤
"""
3つのボールから選ぶ組み合わせは「000」から「111」まで試せばよかったですね。
10進数だと「0」から「7」になります。
0 から 7 まで繰り返すには次のように書きます。
# 0 から 7 まで繰り返す
for i in range(8):
print(i)
"""実行結果
0
1
2
3
4
5
6
7
"""
3つのボールを選ぶ組み合わせは、23 = 8 通りです。Python で累乗は 「**」 を使うので、先ほどのコードは次のように書き換えられます。
# 0 から 7 まで繰り返す
for i in range(2 ** 3):
print(i)
"""実行結果
0
1
2
3
4
5
6
7
"""
どの bit が 1 なのか
「000」から「111」までは、試すことができるようになりした。
次は、どの bit が「1」なのか判定する必要があります。
「110」(10進数だと「6」)のときについて考えてみます。
この bit について右から1bit ずつ判定するのが次のコードです。
# ボールの個数
n = 3
# 110 のとき
bit = 6
for j in range(n):
if bit & (1 << j): # 選ばれているとき
print(f'{j} : 1 です')
else: # 選ばれていないとき
print(f'{j} : 0 です')
"""実行結果
0 : 0 です
1 : 1 です
2 : 1 です
"""
if bit & (1 << j)
の部分で、各桁のビットが「1」かを判定しています。
(1 << j)は、左シフトです。「1」を左に「j」だけずらします。
ここでは、jは0から2まで変化するのでjに対する(1 << j)は次のようになります。
- j=0のとき、1 << 0 は「001」
- j=1のとき、1 << 1 は「010」
- j=2のとき、1 << 2 は「100」
あとは「&演算子」を使って、その桁が「1」かを判定します。
&演算子
&は、ビット演算子になります。
2つのビットについて、各ビットが両方とも「1」のときに「1」になります。
- 0 & 0 = 0
- 0 & 1 = 0
- 1 & 0 = 0
- 1 & 1 = 1
たとえば、「110」&「001」は「000」になります。
「110」&「010」は「010」になります。
110
& 001
-----
= 000
110
& 010
-----
= 010
つまり、選ばれたボール(bit が1)のときは、値は0以外になります。
Python では、if 文は「0」だと「False」と判定され、「0」以外の数値だと「True」になります。
そのため、選ばれているときは、if 文が True になるので処理が実行されます。
# ボールの個数
n = 3
# 110 のとき
bit = 6
for j in range(n):
if bit & (1 << j): # 選ばれているとき
print(f'{j} : 1 です')
else: # 選ばれていないとき
print(f'{j} : 0 です')
"""実行結果
0 : 0 です
1 : 1 です
2 : 1 です
"""
ビット全探索のテンプレート
ビット全探索の基本的なコードは次のようになります。
# bit 全探索
for i in range(2**n):
for j in range(n):
if i & (1 << j):
選ばれているときの処理
これを使って、次のような問題を解いてみましょう。
問題
A1,A2,・・・ANのN個の整数が与えられます。 これらの整数からいくつかを選んで、その総和がKとなるような選び方が存在するかを求めてください。
次の bit 全探索を使ったコードで、この問題を解くことができます。
# 入力
N, K = map(int, input().split())
A = list(map(int, input().split()))
# bit 全探索
answer = 'No'
for i in range(2**N):
sum = 0
for j in range(N):
if i & (1 << j):
sum += A[j]
if sum == K:
answer = 'Yes'
print(answer)
AtCoder Beginner Contest 079 C – Train Ticket
ABC079 C – Train Ticket を bit 全探索で解いてみます。
問題概要
4 つの 0 以上 9 以下の整数 A, B, C, D が整理番号としてこの順に書かれています。
A op1 B op2 C op3 D = 7 となるように、op1, op2, op3 に + か – を入れて式を作って下さい。
考え方
4 つの数字の間に + か – を入れて 7 になる式を出力すればいいです。
bit 全探索で、op1, op2, op3 の中から + を入れる位置をすべて試します。
次のようなコードで、3カ所から + を入れる位置をすべて試せます。
# + を入れる位置の個数
N = 3
# bit 全探索
for i in range(2**N):
for j in range(N):
if i & (1 << j):
print('+', end='') # 選んだとき
else:
print('-', end='') # 選んでないとき
print() # 表示用の改行
"""実行結果
---
+--
-+-
++-
--+
+-+
-++
+++
"""
これで、すべての + と – のパターンが試せるので、あとは、計算するだけです。
# 入力
S = input()
# + を入れる位置の個数
N = 3
# bit 全探索
for i in range(2**N):
answer = S[0] # 出力用の文字列
sum = int(S[0]) # 計算した結果の変数
for j in range(N):
# + 演算子のとき
if i & (1 << j):
answer += '+' + S[j+1]
sum += int(S[j+1])
# - 演算子のとき
else:
answer += '-' + S[j+1]
sum -= int(S[j+1])
# 計算結果の確認
if sum == 7:
answer += '=7'
print(answer)
exit()
まとめ
bit 全探索を使えば、「選ぶ」、「選ばない」のような2種類の選択をする探索が簡単にできます。
N 個の中からいくつか選ぶときの選び方は、2N 通りあります。
選ばないを「0」、選ぶを「1」で考えれば、2進数ですべての選び方を列挙することができます。
全探索を解説したこちらの記事も参考にしてください。