はじめに
この記事ではbit全探索というアルゴリズムを3通りの方法で実装します。
具体的には、bit全探索を用いると解ける次の問題を3通りの方法で解いていきます。
C - たくさんの数式
この記事ではbit全探索がどのようなアルゴリズムであるかの説明はしませんので、わからない方は適宜調べてください。個人的には以下の記事がわかりやすいと思います。
これから解く問題「C - たくさんの数式」について
この問題は次のようにして解けます。
- 入力で与えられる
S
の文字と文字の間に、+
を挟む方法は$2^{N-1}$通りある。 - それらすべてを列挙するためにbit全探索を行う。
- for文で回したbitの1が立っている場所を、
+
を挟む場所と対応させることで個々の場合における計算結果を得る。 - それぞれの計算結果を
ans
に足していく -
ans
を出力する。
以下で紹介する3つの実装は、おおむねこの流れに沿ったものとなっています。
違いは列挙の仕方にあります。それでは早速見ていきましょう。
1. 素直なbit全探索
よく他の記事でも紹介されている最も一般的(だと思われる)bit全探索の実装です。
どこにbitが立っているかはシフト演算を使ってチェックしています。
S = input()
N = len(S)
ans = 0
# Sに+を挟む方法は、Sに文字と文字の間(N-1個ある)に
# それぞれ挟むか挟まないかを決めれば決まるから2^(N-1)通りある。
# それを長さN-1のbitに対応させることで全探索することで答えを求める。
# 例: S = 12345 bit = 110 なら bit = 0110 とみなして 12+3+45 を答えに足していく。
for bit in range(1 << (N - 1)): # 1 << (N-1) は 2^(N-1) と同じ
s = S[0]
for i in range(N - 1): # どこにbitが立ってるかを確認していく
if bit & (1 << i): # 下からi番目にbitが立っているとき
ans += int(s)
s = ""
s += S[i + 1]
ans += int(s)
print(ans)
この実装のポイント
- ビット演算の理解が深まる。
- 他言語に応用が効きやすい。
提出 #38119940 - AtCoder Beginner Contest 045(68ms)
2. 整数を二進数に対応させて行うbit全探索
これはあまり他の記事ではみかけない実装です。特徴的なのは次の2行です。
for b in range(2 ** (N - 1)):
bit = bin(b)[2:].zfill(N - 1)
for文で$2^{N-1}$回のループを回しているのは今までと変わりません。
ただ二行目のbit = bin(b)[2:].zfill(N - 1)
で整数b
を二進数に変換し、それがN-1
桁になるよう0埋めをしています。
もう少し詳しく、具体例を用いてみていきましょう。
入力でS = 12345
が与えられたとします。このときN = len(S) = 5
です。
for
文でb
を0
から2^{N-1}-1
まで回します。
今回N = 5
なのでb
は0
から15
までを動くことになりますね。
b = 5
の場合を考えてみます。
bin(b)
ではb
を二進数に変換しています。bin(b) = "0b101"
と、変換した後は文字列の先頭に0b
がついているのでこれを取っ払うためにスライスをして数字の部分だけ切り取ります。すなわち、bin(b)[2:]
です。
しかし、bin(b)[2:]
のままでは101
であり、文字列の長さが足りていないので長さがN-1 = 4
にするために0埋めをします。それをしているのが.zfill(N - 1)
の部分です。
これでようやくbin(b)[2:].zfill(N - 1) = "0101"
となり長さN-1=4
の文字列が得られたのでこれを用いてi
番目の隙間に+
を挟むかどうかの判定が、i
番目が1
かどうかで判定できます。
S = input()
N = len(S)
ans = 0
# Sに+を挟む方法は、Sに文字と文字の間(N-1個ある)に
# それぞれ挟むか挟まないかを決めれば決まるから2^(N-1)通りある。
# それを長さN-1のbitに対応させることで全探索することで答えを求める。
# 例: S = 12345 bit = 110 なら bit = 0110 とみなして 12+3+45 を答えに足していく。
for b in range(2 ** (N - 1)):
bit = bin(b)[2:].zfill(N - 1)
s = S[0]
for i in range(N - 1): # どこにbitが立ってるかを確認していく
if bit[i] == "1": # 下からi番目にbitが立っているとき
ans += int(s)
s = ""
s += S[i + 1]
ans += int(s)
print(ans)
ポイント
- $2^{N-1}$通りの場合の数をチェックしているという実感がもてる。
- シフト操作をどう書くか覚えていなくてもよい。
提出 #38120122 - AtCoder Beginner Contest 045(78ms)
3. itertools.product()をつかったbit全探索
次はitertoolsモジュールのproduct関数を用いた実装です。
このproduct関数は引数に入れたリストの直積を重複なく出力してくれます。
今回はrepeat
という引数にN-1
を入れて、0と1からなる長さN-1
の直積を生成するようにしています。
from itertools import product
S = input()
N = len(S)
ans = 0
# Sに+を挟む方法は、Sに文字と文字の間(N-1個ある)に
# それぞれ挟むか挟まないかを決めれば決まるから2^(N-1)通りある。
# それを長さN-1のbitに対応させることで全探索することで答えを求める。
# 例: S = 12345 bit = 110 なら bit = 0110 とみなして 12+3+45 を答えに足していく。
for bit in product((0, 1), repeat=N - 1):
s = S[0]
for i in range(N - 1): # どこにbitが立ってるかを確認していく
if bit[i] == 1: # 下からi番目にbitが立っているとき
ans += int(s)
s = ""
s += S[i + 1]
ans += int(s)
print(ans)
ポイント
- 実装をバグらせにくい。
- pythonならではの方法なので他言語に応用が効きにくい。
提出 #38120100 - AtCoder Beginner Contest 045(69ms)
おわりに
いかがだったでしょうか、実行速度的には1が68ms
と一番高速ですが、基本的には気に入ったモノを使えば良いと思います。(私は2が好きでよく使ってます、覚えたての頃によく書いてた癖が抜けなくて……)
間違いなどありましたら、お気軽にコメントいただけますと幸いです。
ここまで読んでいただきありがとうございました。