概要

きのう書いたナップサック問題を Python で解こう!

確かにきのうの記事で、答えを出すための計算を 33554431 回から 225 回へ減らせたけれど、いや、それでも自分でやる気にはなれんわ! だから Python にやってもらおう。

 

動的計画法 練習編を Python で

まずは練習のほう。「数値をいくつか用意したから、その中から好きなだけ数値を選んで、合計が最大になるものを出せ!」という問題だ。

  • たとえば [7, -6, 9] なら最大の組み合わせは 7, 9 で最大値は 16 だ。
  • たとえば [-9, -16] なら何も選ばないのが答えで、最大値は 0 だ。

解説は前の記事のここ(動的計画法 練習編)。

def practice(lis: list) -> int:

    # 表を作ります。
    # 動的計画法を使うときの表は dp って名前にすることが多いみたいです。
    # いちばん左は「リストから 0 個選べるとき」の最大値です。もちろん 0。
    dp = [0]

    # 「リストから[リストの個数]選べるとき」の計算をするので、リストの個数ぶん回します。
    for i in range(len(lis)):

        # 「ひとつ前の値」と「今のところの最大値に lis[i] を足したもの」を比べます。
        # NOTE: dp[i] は「0 個選べるとき」のマスで、 lis[i] は「1 個目の要素」なのがちょっとわかりづらいトコ。
        dp.append(max(dp[i], dp[i] + lis[i]))

    print(dp)

    # for が終わると dp の最後の値が、全部の値を比べた結果の最大値になってるはずだ。
    return dp[-1]


print(practice([7, -6, 9]) == 16)

実行するとこう↓なる! OK!

[0, 7, 7, 16]
True

 

動的計画法 本番を Python で

では本番だ。

  • [(weight, value) = (2, 3), (1, 2), (3, 6), (2, 1), (1, 3), (5, 85)] というような品物リストと、重量制限 W が与えられる。
  • リストから、「weight 合計が W を超えないよう」「好きなだけ」品物を選んだときの value 最大値を出せ。

解説は前の記事のここ(動的計画法 本番編)。

from pprint import pprint


def knapsack(lis: list, W: int) -> int:

    # 表を作ります。
    # dp[0] はリストから何も選んでないときの最大値じゃん? だから一番左の列は全部 0 です。
    dp = [[0] * (W + 1)]

    # リストの個数ぶん回します。
    for i in range(len(lis)):

        # i の周回では lis[i] までの品が選べるときの計算をします。
        weight = lis[i][0]
        value = lis[i][1]

        # lis[i] までの品が選べるときの情報を格納する配列です。もちろん [0]*W+1 です。
        dp.append([0] * (W + 1))

        # lis[i] までの品が選べる列のマスを埋めます。
        for w in range(W + 1):

            # lis[i] の重さが重量制限より上だったら、 lis[i] を追加することはできません。
            # この w 制限での value 最大値は、 lis[i] を追加する前の最大値(dp[i][w])と同じです。
            # NOTE: いま値を追加したいマスは、 dp[i][w] じゃなくて dp[i + 1][w] なのがちょっとわかりづらいトコ。
            if weight > w:
                dp[i + 1][w] = dp[i][w]
                continue

            # lis[i] を追加したときの最大値は、(ここがムズいんだが)
            # lis[i] を追加する前の最大値リストの中で、
            # lis[i] の入るスペースがあるときの最大値に lis[i] の価値を足したものです。
            _ = dp[i][w - weight] + value

            # lis[i] を足した場合の値と、 a[i] を足さなかったときの値(これまでの最大値)、大きいほうが、この条件下での最大値です。
            dp[i + 1][w] = max(_, dp[i][w])

    pprint(dp)

    # 一番はじっこ…… a の個数ぶん選べて、重量制限が W のときの最大値が、答えです。
    return dp[-1][-1]


print(knapsack([(2, 3), (1, 2), (3, 6), (2, 1), (1, 3), (5, 85)], 9) == 94)

実行するとこう↓なる! OK!

[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 3, 3, 3, 3, 3, 3, 3, 3],
 [0, 2, 3, 5, 5, 5, 5, 5, 5, 5],
 [0, 2, 3, 6, 8, 9, 11, 11, 11, 11],
 [0, 2, 3, 6, 8, 9, 11, 11, 12, 12],
 [0, 3, 5, 6, 9, 11, 12, 14, 14, 15],
 [0, 3, 5, 6, 9, 85, 88, 90, 91, 94]]
True

 

おしまい

前回が重たかったので、今回はサラッとここで終わろう。ナップサック問題、習得ー!

クロッキー帳3枚くらい考えてようやくわかったのだ。