ビット全探索を理解する!練習問題(7~9問目)【Haskellで競技プログラミングがしたい! Part4】
はじめに
少しずつ進んできましたね。この調子で頑張ります!
さて今回は7~9問目について回答してみましょう。
7問目
問題
N 以下の正の整数の中で、X の倍数または Y の倍数であるものの個数はいくつありますか?
解答
isMultiple :: Int -> Int -> Int -> Int
isMultiple x y n
| n `mod` x == 0 || n `mod` y == 0 = 1
| otherwise = 0
solve :: Int -> Int -> Int -> Int
solve x y n = sum $ map (isMultiple x y) [1..n]
main:: IO ()
main = do
input <- getLine
let [n, x, y] = map read (words input) :: [Int]
print $ solve x y n
解説
どのように計算するか迷ったのですが、今回思いついたのはガード条件を使った倍数判定です。
これを使うと関数のパターンマッチングにおいて条件を指定することができます。
isMultiple
は、今回だとx
またはy
で割ったときのあまりが0の時に1を返し、それ以外の時は0を返すように実装しました。
次にこの関数を使ってsolve
を実装します。
1
からn
までに対してisMultiple
を適用して1
になったものの合計をとると個数が数えられそうと思いつき、
-
map
で[1..n]
までの値にisMultiple
を適用する - この処理の返り値のリストに
sum
して計算
としました。
最後に標準入力ですが、変数に値をBindするときもパターンマッチングが使えます。
なので入力が3個であることが事前にわかっているので、[n, x, y]
として値を受け取れました。
8問目
問題
赤・青のカードが各1 枚ずつあり、あなたはそれぞれのカードに 1 以上 N 以下の整数を 1 つ書き込みます。
カードに書かれた整数の合計が S 以下となる書き方は、いくつありますか?
解答
solve :: Int -> Int -> Int
solve n s = sum [1 | i <- [1..n], j <- [1..n], i + j <= s]
main:: IO ()
main = do
input <- getLine
let [n, s] = map read (words input) :: [Int]
print $ solve n s
解説
総当たりで計算したかったのでリスト内包表記を使いました。
内包表記とはもともと数学の集合に関する仕組みで、既存の集合から新たな集合を作るために使われるそうです。[1]
今回は2種類のカードがあり、それぞれ最大n
までの値をとるので、それぞれのカードがとる数字の集合、つまりリストは[1..n]
で表現することができます。
またこの内包表記には条件を設定することができ、この条件がTrue
になったものだけ新しいリストに追加されます。
今回はs
以下が条件なので、2種類のカードの数字の合計i+j
がs
以下ならTrue
となりリストに1
が追加されます。
最後にこの処理によって新しいリストが生成されるので、sum
して組み合わせの数を合計しています。
標準入力は前のものと同様にパターンマッチングで変数にBindしました。
9問目
問題文にあった注意書き
全探索で解いても 1000 点中 500 点しか得られず、満点(AC)にならないことに注意してください。(本に記されている通り、一部の大きいケースでは現実的な時間で答えが求まらないからです)
問題
N 枚のカードが横一列に並べられています。左からi 番目
カードの中からいくつかを選んで、合計がちょうど S となるようにする方法はありますか。
解答
intToBits :: Int -> [Bool]
intToBits 0 = []
intToBits n = (n `mod` 2 == 1) : intToBits (n `div` 2)
genBits :: Int -> [[Bool]]
genBits n = [intToBits i | i <- [1..2^n]]
selectedSum :: [Int] -> [Bool] -> Int
selectedSum nums bits = sum $ zipWith (\b x -> if b then x else 0) bits nums
check :: Int -> [Int] -> [Bool] -> Bool
check s nums bits = s == selectedSum nums bits
output :: Bool -> String
output bool = if bool then "Yes" else "No"
main:: IO ()
main = do
input <- getLine
let [n, s] = map read (words input) :: [Int]
input <- getLine
let nums = map read (words input) :: [Int]
let allBits = genBits n
putStrLn $ output $ any (==True) $ map (check s nums) allBits
解説
最初どのように実装すればわからず、だいぶ苦戦しました・・・
この問題で設定されたn
の値で計算完了するには動的計画法(DP)が必要になるのですが、自分はまだこの実装方法がよくわかっていません。
ただ全探索でも部分点があるということなので、まずは全探索で実装することを考えました。
この本のコラムにビット全探索で求めることができるとのことなので、この方法で実装を進めました。
(ビット全探索でより良い・または楽な実装方法がある場合は教えていただけると嬉しいです)
ビット全探索とは、ビット演算を使って可能な組み合わせを表現する方法です。
今回だとn
枚あるカードの中からどれを選び、どれを選ばないかについてビットを使って表現します。
この手法を用いて最終的な計算結果(Yes
or No
)を求めるには以下の処理が必要になってきます。
- 特定の数をビット表現に変換する
- ビットとカードの情報を組み合わせて、選ばれたカードのみを抽出する
- 選ばれたカードのリストから合計値を計算する
- 合計値が指定する値と一致するかチェックする
- カードが
n
枚あり、それぞれを選ぶ・選ばないという条件の時パターンは2^n
個あるので、そのすべてのパターンについてビット表現に変換する - すべてのパターンにおいて、指定する値と一致するものが1つ以上あれば
Yes
を返し、そうでない場合はNo
を返す
それぞれの機能を実装したのが解答になります。
簡単にそれぞれ実装した関数についての説明です。
intToBits
-
Int型の引数を受け取って、その余りが1なら
True
、0ならFalse
になり、それをconsでつないでいく。 - 再帰的に関数を定義して
div
で2ずつ割っていく。 - リストの最初は一番最初の2のあまりなので
2^0
の項が出てきて、次再帰で実行されるときは2^1
の項が出てくる - これで引数が0になったら空のリストを返し、今まで出力してきた
True
、False
が入ったリストが返ってくる - 最終的な返り値は通常のビット表現を左右反転したものとなるが、組み合わせを全列挙できれば良いので問題ない
genBits
- 組み合わせが
2^n
パターンあり、このパターン数分だけビット表現に変換する必要がある - リスト内包表記を使ってIntのリストからビット表現のリストに変換した
selectedNum
- ここでは
zipWith
を使って、2つのリストを組み合わせて関数を適用した新たなリストを生成する - ラムダ関数を使い、ビットの値が
True
の時はカードの値を返し、False
の時は0を返す - これによって選ばれたカードの数値だけが入ったリストが完成する
- 最後にこのリストの値を
sum
で合計する
check
- 指定の値
s
を引数にとって、先ほどのselectedNum
の値と一致しているかどうか判定する
output
- もしリストに
True
があればYes
を返し、そうでない場合はNo
を返す
print
するときの処理
最後の-
map (check s nums) allBits
として、check
に部分適用してBoolを受け取って値を返す形にしてmap
を使う - これによって作られたリストは、選んだ組み合わせが指定の値と一致しているかどうかの結果が入っているもの
- このなかにどれか1つでも
True
があるかどうかチェックしたいので、any (==True)
とする - 最後にこの値を
output
の引数に渡すことで無事目的の文字列が取得できる
という結果で、これで全探索することができました。
難しかったですがなんとかやり切れてうれしいです。
終わりに
なかなかこのような処理を日常考えていないので実装するのに結構時間がかかり大変でしたが、何とかなったのは少し自信になりました。
しかしこれはまだ入門でここからこの問題を時間内に計算を終わらせるアルゴリズムがあるので、そこも引き続き学んでいきたいです!
少しずつ重量が増えてきたので毎回3個ずつ解答を上げるのが難しくなるかもしれませんが、継続して解いていきたいと思っているので今後もこつこつ進めていきます。
今回も見てくれてありがとうございました!
参考
内包表記で参考になった記事
Discussion
9問目は、(containersパッケージにあるData.IntSetモジュールの集合演算を使えばACになりそうですが)
ビットパターンを使うなら、type Bit = Bool ではなくて、type Bit = Int としておく方がすっきりするかもしれませんね。
8問目は、
などでどうでしょう。
7問目は、リストを生成しなくてもよい気がします。
それぞれの問題についてコメントありがとうございます!
たしかに集合の演算すれば配列使わなくてよいので早くなりますね!
この計算も自分は愚直にリスト2個つかって内包表記していましたが、カードの数字を片側決めれば、そのパターン数は指定の数Sから引いた数に等しくなるということですかね。
確かにオーダーがO(N)になって早くなりますねーありがとうございます!
使ったことない関数が多くまだ理解できていませんが、確かに
type Bit = Int
とすることで掛け算をするだけでシンプルに選ばれたカードの数を抽出できますね、ありがとうございます!後は色々な関数の合成が使われていたり
interact
を使っていたりとあるので、ここらへんは少しずつ使いながら学んでいきます。また、
Data.IntSet
でACできるらしいということについてもありがとうございます。集合を使ってどのように解くかについても考えてみようと思います!