イチからわかる!競技プログラミング初心者向けのbit全探索入門ガイド【Python】

munich プログラミング

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進数ですべての選び方を列挙することができます。

全探索を解説したこちらの記事も参考にしてください。

参考

AC – 3.05.ビット演算 – AtCoder

bit全探索 – けんちょんの競プロ精進記録

タイトルとURLをコピーしました