2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

bitDPがなかなか理解できなかった話

Posted at

1. はじめに

bitDPの解説読んだけど、「全体集合」「状態」「[S]」「O(N2^N)」とかむずかしい言葉ばかりで理解できないよ~:disappointed_relieved:
となった方、いませんか?私がそうです。

私の理解力が足りないからです。数学の知識も足りていません。
これを書いている現在も、分かっていません。

ただ、なんとかbitDPの使い方は分かりましたので、記事を投稿します。

bitDPについて学びたかったけれど、難しい用語ばかりで理解できなかった…:disappointed_relieved:
という人の助けになれば幸いです。

DP(動的計画法)は理解していることが前提です

2. bitで状態を表す

たいていのbitDP解説記事はまずこれから始まっていると思います。
で、これは多分つまづきポイントではないかと思いますが、一応。

「bitで状態を表す」際には、値を2進数で考えます。
そして、各ビットにそれぞれなんらかの意味を持たせます。

#define MAHI   0b00000000000000000000000000001000 // まひ
#define DOKU   0b00000000000000000000000000000100 // どく
#define YAKEDO 0b00000000000000000000000000000010 // やけど
#define NEMURI 0b00000000000000000000000000000001 // ねむり

int main() {
    int status;
    cin >> status;
    if (status & MAHI) { // まひ判定
        // まひ時処理

上記のように各ビットに意味を持たせることで、複数の状態をひとつの値に持たせることができます。
もちろん、それぞれの状態をbool値で管理することもできます。
ですが、『複数の状態をひとつの値に持たせる』ことがDPを行ううえで重要になります。

3. 『状態集合を添え字にとして持つDP』

これは、私がなんとかbitDPの理解に至った言葉です。
まあ、いまだ『状態集合』がいまいち分かっていませんが…

DP(動的計画法)の典型的な問題であるナップサック問題では、重さを添え字にします。

ナップサック問題では、例えば下記のケースで
image.png
総重量 i の時にとり得る最大の価値 dp[i] を求めるには、
・ dp[i-1] + 1
・ dp[i-2] + 3
・ dp[i-3] + 5
のうち最大の価値になるものを選びます。

以下、4桁の数字は2進数です。0bってつけると見にくいので…

一方、bitDPでは『複数の状態をもたせた値』を添え字にします。
たとえば状態 1101 の時にとり得る最適値 dp[1101]を求めるには、
・ dp[0101] に 1000 を追加したもの
・ dp[1001] に 0100 を追加したもの
・ dp[1100] に 0001 を追加したもの
のうち最適になるものを選びます。

4. 例題

4つの食料があります。1日1つずつ食べます。
食料は食べた日によって満足度が異なります。
(一晩寝かせたカレーがおいしいとか、新鮮な魚はおいしいとか、そういう感じです)
全て食べ終わった時の最大合計満足度を求めなさい。
image.png

まず、それぞれの食料を食べたかどうかを、各ビットにアサインします。
食料0: 0001
食料1: 0010
食料2: 0100
食料3: 1000

食料0,1,2を食べ終わった状態は 0111 で表すことができます。
3日目までに食料0,1,2を食べ終わった場合の合計満足度の最大値 dp[0111] は

・dp[0110] + 3 (3日目に食料0を食べた)
・dp[0101] + 1 (3日目に食料1を食べた)
・dp[0011] + 2 (3日目に食料2を食べた)

のうち、最大のものになります。

実装はこんなかんじです。

#include <iostream>
#include <vector>
#include <bitset>
using namespace std;

int main() {
    int N = 4;
    // 食料の満足度[食料i][j日目]
    vector<vector<int>> food{{1,3,3,4},{2,1,1,1},{1,3,2,2},{3,2,1,1}};

    vector<int> dp((1 << N), 0);

    // 0001 から 1111 までまわす
    for (int i = 1; i < (1 << N); i++) {
        for (int j = 0; j < N; j++) {
            int x = 1 << j;
            // 0001, 0010, 0100, 1000 のうち、i のビットが立っているものについて
            if (i & x) {
                // 何日目かを知るために、立っているビットを数える
                int day = bitset<16>(i ^ x).count();

                // dp[0111] は、下記のうち最大のもの
                // ・dp[0110] + 食料[0][3日目] 
                // ・dp[0101] + 食料[1][3日目] 
                // ・dp[0011] + 食料[2][3日目] 
                dp[i] = max(dp[i], dp[i ^ x] + food[j][day]);
            }
        }
    }
    cout << dp.back() << endl;

    // debug
    for (int i = 1; i < (1 << N); i++) {
        cout << bitset<4>(i) << ":" << dp[i] << endl;
    }
    return 0;
}

(1 << N) というのが、普段あまり見ない表記ですね。
上記のケースでは0001から1111までのdpを埋めたいので、forの上限を10000にしている、ということです。
(i ^ x) も普段あまり見ない表記ですね。
^ は排他的論理和で、意図としては i から x のビットを落とす、というものです。
i = 0111, x = 0001 なら i ^ x は 0110 になります。

実行結果は下記の通りです。
食料を3, 2, 1, 0 の順で食べると、合計満足度の最大値は11になります。
また、デバッグ情報としてdpの中身をすべて出してみました。
例えば 1001:6 は、2日目までに食料0と食料3を食べた時の合計満足度の最大値が6であることを示します。

11
0001:1
0010:2
0011:5
0100:1
0101:4
0110:5
0111:8
1000:3
1001:6
1010:4
1011:7
1100:6
1101:9
1110:7
1111:11

5. 計算量

例題のようにN=4であれば4!=24通りの組み合わせすべてを求めてることもできますが、
N=16となると16!=20,922,789,888,000通りと膨大な量になってしまいます。
そこで上記のようにbitDPを使用すると、
N=4なら(2^4)×4 = 64 回のループで、N=16でも(2^16)×16 = 1,048,576 回のループで済みます。

ここまで書いて、ようやくO(N2^N) の意味が分かりました。

    // 0001 から 1111 までまわす
    for (int i = 1; i < (1 << N); i++) { // ここが 2^Nで
        for (int j = 0; j < N; j++) { // ここがN

だったんですね。

6. 応用

巡回セールスマン問題

巡回セールスマン問題とは、N個の地点をすべて1回ずつ巡回する場合の最短経路を求める問題です。
巡回済みの場所をビット管理するのですが、追加情報として、直前に巡回した場所が必要になるので、
2次元の動的計画法ということになります。

例えば『地点0,1,2が巡回済み、最後に巡回した場所が2』の dp[0111][2] の最適値は、
・dp[0011][0] + 地点0->2の距離
・dp[0011][1] + 地点1->2の距離
のうち、最適な方となります。

巡回セールスマン問題は、巡回する場所の順列の問題ではありますが、
1つビットを立てるためには「1つ前に立てたビットがなにか」を知る必要がある、
ちょっとした応用問題になるかと思います。

n個の中から2個のペアを作れるだけ作って最適を求める

n個のアイテム A, B, C, D, ... があり、AとBのペアなら1ポイント、AとCのペアなら2ポイント、
のように各組合せにポイントが設定されており、最大値をめざすというものです。
(AtCoder ABC318 D問題)

DCBAの順にビットをアサインし、ペアとして選択済みのビットを立てることにすると、
dp[1111] は
・dp[1100] + A-Bペアのポイント
・dp[1010] + A-Cペアのポイント
・dp[0110] + A-Dペアのポイント
・dp[1001] + B-Cペアのポイント
・dp[0101] + B-Dペアのポイント
・dp[0011] + C-Dペアのポイント
のうち最大のものになります。

ただ、A-B選択済みのところにC-Dのポイントを足すことと、
C-D選択済みのところにA-Bのポイントを足すことは、結果的に同じになります。
よって、実は下記の3通りだけ検討すればよいということになります。
・dp[1100] + A-Bペアのポイント
・dp[1010] + A-Cペアのポイント
・dp[0110] + A-Dペアのポイント

これを端的に言うと
「追加する2つのビットのうち1つを、最下位ビットに固定できる」
ということになるようです。

dp[]が加算されるのが2個おき、追加するビットを固定できる、というのが応用ポイントですね。

7. 最後に

私がbitDPをなかなか理解できなかったのは、最初にbitDPに触れたのが
「n個の中から2個のペアを作れるだけ作って最適を求める」
だったためです。。。

いきなり応用から入ってはダメですね…。

あとは
dp[0110]はどうやって求めるのか?
というのがなかなか理解できなかった、というのがあります。

この投稿ではできるだけ簡単な例で、dp[]の求め方が分かりやすいように、
難しい言葉をできるだけ使わずに、書いてみました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?