あるエンジニアのAtCoder奮闘記

東京都港区にあるアミフィアブル株式会社のエンジニアが、AtCoderで解いた問題について振り返ったりしていく会社公認のブログです。

AtCoder ABC 142 E - Get Everything 解説AC (Go)

irisruneです。解説ACですが記事にすることはできそうでした。

問題

atcoder.jp

これも一つのbitDPではありそうですが、特殊なケースになると思います。

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
)

var sc *bufio.Scanner

func nextStr() string {
    sc.Scan()
    return sc.Text()
}

func nextInt() int {
    sc.Scan()
    i, e := strconv.Atoi(sc.Text())
    if e != nil {
        panic(e)
    }
    return i
}

const INF = 1000000000

type key struct {
    cost int
    open int
}

func main() {
    sc = bufio.NewScanner(os.Stdin)
    sc.Buffer(make([]byte, 0), 1000000001*3)
    sc.Split(bufio.ScanWords)
    n, m := nextInt(), nextInt()
    pattern := 1 << uint(n)
    keys := make([]key, m)
    for mi := 0; mi < m; mi++ {
        a, b := nextInt(), nextInt()
        open := 0
        for bi := 0; bi < b; bi++ {
            c := nextInt()
            open = open | (1 << uint(c-1))
        }
        keys[mi] = key{a, open}
    }

    dp := make([][]int, m+1)
    dp[0] = make([]int, pattern)
    // dp[0][0] = 0
    for pi := 1; pi < pattern; pi++ {
        dp[0][pi] = INF
    }

    for mi := 0; mi < m; mi++ {
        dp[mi+1] = make([]int, pattern)
        copy(dp[mi+1], dp[mi])
        for pi := 0; pi < pattern; pi++ {
            pnext := pi | keys[mi].open
            if dp[mi][pi]+keys[mi].cost < dp[mi+1][pnext] {
                dp[mi+1][pnext] = dp[mi][pi] + keys[mi].cost
            }
        }
    }

    switch {
    case dp[m][pattern-1] < INF:
        fmt.Println(dp[m][pattern-1])
    default:
        fmt.Println(-1)
    }
}

まず計算量の見通しを立てる必要があります。まずNについて考えると、それぞれの宝箱を開けられるかどうかを考えることになるので、O(2^N)またはO(2^NN)辺りになりそうです。なので全体の計算量はO(2^NM(\times N))辺りになると考えられます。

ここでbitDPで解くことを考えてみます。この問題をbitDPで解くということは、以下のように箱に対するDPに帰着されるということになります。

  1. N_0個の宝箱を開ける場合の組み合わせそれぞれに対する最小費用を求める。
  2. 1.の結果を利用してN_0+1個の宝箱を開ける場合の組み合わせそれぞれに対する最小費用を求めることができる。

しかし、このようなアルゴリズムに帰着させることができないことが示せます。例えば入力例1をアレンジした以下の入力に対する解を考えます。

2 3
10 1
1
15 1
2
20 2
1 2

この場合、1個の宝箱を開ける場合の最小費用は箱1,2それぞれに対して10,15となりますが、2個の宝箱を開ける場合の最小費用は10+15=25ではなく、3番目の鍵のみ購入する20となります。

つまり、箱に対してDPを適用することはできません。今回はここで詰まってしまい解説ACすることとなりました。

結論としては、箱ではなく鍵に対してDPを適用することでこの問題を解くことができます。ナップサック問題の変形として考えると以下のような問題になります。

  1. 荷物の価値を鍵の価格、荷物の重さ(容量)を鍵に対応する宝箱の(bitによる)組み合わせに置き換える。
  2. 1,2,\ldots,M_0番目の鍵を用いることができる場合の、開けることができる宝箱の組み合わせとそのときの最小費用を求める。
  3. 2.の宝箱の組み合わせとM_0+1番目の鍵に対応する宝箱の組み合わせの論理和を求めることによって、1,2,\ldots,M_0,M_0+1番目の鍵を用いることができる場合の、開けることができる宝箱の組み合わせとそのときの最小費用を求めることができる。
  4. M個の鍵全てを用いることができる場合の、N個全ての宝箱を開けることができる最小費用が最終的な解となる。

端的に言えば、箱の状態をbitで表しつつ鍵の本数を増やしていくDPとなります。DPテーブルのサイズは箱の状態が2^N、鍵の本数がMのためO(2^NM)となります。

荷物の価値を宝箱の組み合わせ、荷物の重さを鍵の価格に置き換えるとDPとしてわかりやすいと思いますが、鍵の価格の合計は最大10^5Nとなるため、DPテーブルのサイズが10^9をやや上回り、計算が間に合わない可能性が高いです。頑張れば通せそうなスケールなので試してみたい気もします。

雑記

  • DPテーブルの要素にコスト、軸に価値が入るのがbitDPの特徴であると言えるかもしれません。適当言ってるかもしれません。