Python
 Computer >> コンピューター >  >> プログラミング >> Python

【Python】各アイテムを何度でも選べるナップサック問題で最大価値を求めるプログラム

問題概要

同じ長さを持つ2つのリスト weights(重さ)と values(価値)、および整数 capacity(容量)が与えられます。weights[i]values[i] は、それぞれ i 番目のアイテムの重さと価値を表します。

ここで特別なルールとして、各アイテムは何個でも(何度でも)選んでよいものとします。合計の重さが capacity を超えない範囲でアイテムを選ぶとき、得られる価値の合計の最大値を求めるのがこの問題です。これは「無制限ナップサック問題(Unbounded Knapsack Problem)」として知られる古典的な動的計画法の応用例です。

たとえば、次の入力を考えてみましょう。

  • weights = [1, 2, 3]
  • values = [1, 5, 3]
  • capacity = 5

この場合の出力は 11 になります。「重さ2・価値5」のアイテムを2つ(重さ4・価値10)と、「重さ1・価値1」のアイテムを1つ(重さ1・価値1)選ぶことで、合計重さちょうど5で価値11を達成できます。

解法のアプローチ

この問題は、再帰的な動的計画法(DP)を使って解くことができます。基本的な考え方は以下の通りです。

  1. dp(i, k) 関数を定義します。 引数 i は現在注目しているアイテムのインデックス、k は残りの容量を表します。
  2. iweights の長さと一致したら、これ以上選べるアイテムがないため 0 を返します
  3. まず、現在のアイテムを選ばないケースとして ans = dp(i + 1, k) を計算します。
  4. もし k >= weights[i](容量に余裕がある)なら、アイテムを選ケースも評価し、ans = max(ans, dp(i, k - weights[i]) + values[i]) でより大きい方を採用します。
  5. ans を返します。
  6. メイン処理では dp(0, capacity) の結果を返します。

ここでの重要なポイントは、アイテムを複数回選べるため、再帰呼び出し時にインデックスを i + 1 に進めず、同じ i のまま dp(i, k - weights[i]) を呼び出すことです。これにより「同じアイテムを繰り返し選ぶ」ことが自然に表現されます。

実装例(Pythonコード)

それでは、実際のコードを見てみましょう。

class Solution:
    def solve(self, weights, values, capacity):
        def dp(i, k):
            if i == len(weights):
                return 0
            ans = dp(i + 1, k)
            if k >= weights[i]:
                ans = max(ans, dp(i, k - weights[i]) + values[i])
            return ans
        return dp(0, capacity)

ob = Solution()
weights = [1, 2, 3]
values = [1, 5, 3]
capacity = 5
print(ob.solve(weights, values, capacity))

入力

[1, 2, 3], [1, 5, 3], 5

出力

11

補足:メモ化による高速化

上記の素朴な再帰実装は、入力が大きくなると同じ状態を何度も計算してしまい、処理時間が爆発的に増える可能性があります。Python標準ライブラリの functools.lru_cache を使ってメモ化を追加すれば、計算量を O(アイテム数 × 容量) まで抑えることができ、実用的な速度になります。

from functools import lru_cache

class Solution:
    def solve(self, weights, values, capacity):
        @lru_cache(maxsize=None)
        def dp(i, k):
            if i == len(weights):
                return 0
            ans = dp(i + 1, k)
            if k >= weights[i]:
                ans = max(ans, dp(i, k - weights[i]) + values[i])
            return ans
        return dp(0, capacity)

まとめ

各アイテムを複数回選べるナップサック問題では、通常の0/1ナップサックとの決定的な違いである「再帰呼び出し後にインデックスを進めない」点を押さえることが鍵となります。再帰的なDPでシンプルに表現でき、メモ化を組み合わせれば大規模な入力にも対応できる、覚えておくと非常に有用なアルゴリズムパターンです。

  1. Pythonで株を1回だけ売買して得られる最大利益を求めるプログラム

    時系列順に並んだ企業の株価リストが与えられたとき、その株を一度だけ買って売ることで得られる最大の利益を求める問題を考えてみましょう。ただし、「売る」ためには必ず先に「買って」おく必要がある点に注意してください。 たとえば、入力が prices = [10, 12, 9, 6, 8, 12] の場合、出力は 6 になります。これは、株価が 6 のときに購入し、12 のときに売却すれば、最大の利益 12 - 6 = 6 が得られるためです。 解法のアプローチ この問題は、リストを一度走査するだけで解くことができます。ポイントは「これまでの最安値」を常に記録しておき、各時点での売却益を計算して最大値

  2. Pythonで辞書から2番目に大きい値を取得する3つの方法

    はじめに この記事では、辞書(ディクショナリ)に格納された値の中から「2番目に大きい値」を取り出す方法を、複数のアプローチに分けてわかりやすく解説します。 問題設定: キーと値を持つ辞書が与えられたとき、その値の中で2番目に大きい値を求めて出力します。 アプローチ1:sorted()関数と負のインデックスを使う方法 まず、sorted()関数で辞書の値を昇順に並べ替え、負のインデックス [-2] を指定することで、後ろから2番目の要素(=2番目に大きい値)を取得します。コードが非常に短くシンプルなのが特徴です。 コード例 # 入力 example_dict = {tutor: 3, tutor