動的計画法の概要

1. ナップサック問題

1 問題の定義

問題がナップザック問題で解けるかどうかを判断する方法
ナップザック問題の共通の特徴:ナップザックの容量ターゲットアイテム配列 nums が与えられた場合、ターゲットを取得するためにnums の要素を特定の方法で選択できますか
? 注:
1. target と nums は数値または文字列にすることができます。
2、target は次のとおりです。明示的 (タイトルが指定されている)、または非明示的 (タイトル情報を掘り出す必要がある) にすることもできます。
3. 一般的な数値選択方法: 各要素は 1 回のみ選択可能 / 各要素は複数回選択可能 / 選択済み要素が配置されている

2 問題の分類

一般的なバックパックの種類は次のとおりです。
1. 0/1 ナップザック問題: 各要素は最大 1 回選択できます。
2.完全なナップザック問題: 各要素は繰り返し選択できます。
3.組み合わせナップザック問題:ナップザック内のアイテム
順序を考慮する必要があります。 各バックパックを反復処理します。

ナップザック問題はそれぞれ要求される内容も異なり、問題の分類により、
1.最大値問題:最大値/最小値を要求する問題
2.存在問題:存在するかどうかに分類できます。 ... ………
3.組み合わせ問題:…を満たすすべての順列と組み合わせを見つけます。

3 問題解決テンプレート

01 最も価値のあるバックパックの問題

この問題は最も単純かつ基本的な問題であり、この問題を理解した後は、少し修正を加えて残りのナップザック問題を学習できます。

  • 最大で BagWeight のアイテムを収納できるバックパックがあります (bagWeight=4)
  • 各項目の重みは配列weight = {1, 3, 4}で表されます。
  • 各項目の値は配列値 = {15, 20, 30} で表現されます。
  • Q: 太っていないことを前提として、バックパックに収納できるアイテムの最大値はどれくらいですか?
  • 問題解決のアイデア:
  1. まず考えやすい境界線ですが、バックパックの容量が0の場合は何も入れられず、最大値も全て0になります。
  2. 次に、最も簡単に解決できる副問題を考えます。アイテム 1 が 1 つだけあり、バックパックの最大容量が 1~bagWeight であると仮定すると、最良の解決策はアイテム 1 のみを選択できます。
  3. アイテム 2 が追加され、バックパックの最大容量が 1~bagWeight であるとします。
    アイテム 2 の重量は 3 であるため、バックパックの最大容量が 1 または 2 の場合、アイテム 2 を入れる余地はまったくないため、アイテム 1 を 1 つだけ選択できます。
    バックパックの最大容量が 2 を超える場合、選択する必要があります。これが動的ルールの本質です。アイテム 2 については、可能性が 2 つしかないため、2 つのスキームを比較する必要があります。次に、2 つのオプションのうち最も価値のあるものを選択してくださいたとえば、バックパックの最大容量が 4 の場合、
    スキーム 1 : アイテム 2 を取ると、バックパックの残り容量は 4-3=1 になります。したがって、バックパックの残り容量が 4 になったときに何が取得できるかを知る必要があるだけです。バックパックは 1 でアイテム i はありません。最大値にアイテム 2 の値を加えたものが計画の合計値です。計画 2 : アイテム 2 を取得しない場合、合計値は実際にはバックパックの最大値になり
    ます。バックパックの残り容量が4で、アイテム1のみの場合。
    常にこの原則に従い、バックパックの最大容量を 1 からバッグの重量とし、アイテム 1 と 2 のみの場合にバックパックに収納できるアイテムの最大値を計算します。
  4. item 配列全体を走査し、サブ配列ごとにバックパックの容量を 0 から BagWeight まで走査し、最終的にバックパックの容量が BagWeight であるときの完全な item 配列の最大値を取得します。これが答えです。
  • コードの実装
    まず、dp 2 次元配列が必要です。これは、行を使用して項目を表し、i を使用してサイクルを表し、列を使用してバックパックの容量を表し(循環には j を使用)、 dp[i][j] はバックパックがいつ使用されるかを表します。容量は i から j です。最大値を得るためにアイテムの中から選択する方法。例えば、2行3列目は、ナップザックの容量が2の場合、項目1と項目2からどのように選べば最大値が得られるかを示しています。
    上記の問題解決のアイデアに従って分析を続けます。
  1. まず考えやすい境界線ですが、バックパックの容量が0の場合は何も入れられず、最大値はオール0、つまりdp配列の1列目はオール0になります。 dp[i][0]=0)
  2. 次に、最も簡単に解決できる部分問題を考えます。アイテム 1 が 1 つだけあり、バックパックの最大容量が 1~bagWeight であると仮定すると、最良の解決策はアイテム 1、つまり dp[0][ を選択することだけです。 j]=値[0]
  3. アイテム 2 が追加され、バックパックの最大容量が 1~bagWeight であるとします。
    アイテム 2 の重さは 3 (weight[i]=3) なので、ナップザックの最大容量が 1、2 (j=1、j=2) の場合、アイテム 2 を 1 つだけ積んでも荷物を積み込むことはできません。ロード済み、つまり dp[i ] [j]=dp[i-1][j]、j<weight[i] の場合
    バックパックの最大容量 > 2 (j>2) の場合、選択です。これがダイナミック ゲージの本質です。2 つを比較する必要があります。項目 2 については、選択するかどうかの 2 つの可能性しかないため、2 つのスキームがあり、その中から最も高い値を持つものを選択します。 2 つのスキーム、つまりmax (スキーム 1、スキーム 2)です。たとえば、バックパックの最大容量が 4 (j=4) の場合:
    スキーム 1 : アイテム 2 を取得すると、バックパックの残りの容量は 4-3=1、つまりj-weight[i]になります。バックパックの残容量を知りたい 1の場合、アイテムiがない場合に取得できる最大値、つまりdp[i-1][j-weight[i]] を加算します。項目 2 の値 (value[i]) が解です 合計値、​​つまり dp[i-1][j-weight[i]]+value[i] ;
    スキーム 2 : 項目 2 は使用しませんとすると、合計値は実際にはバックパックの残量が4のときにアイテム1だけの場合になります 次にナップザックの最大値、つまり dp[ i][j]=dp[i-1][j] ]
    常にこの原則に従い、バックパックの最大容量を 1 からバッグの重量とし、アイテム 1 と 2 のみの場合にバックパックに収納できるアイテムの最大値を計算します。
  4. item 配列全体を走査し、サブ配列ごとにバックパックの容量を 0 から BagWeight まで走査し、最後にバックパックの容量が BagWeight のときの最後の項目の最大値を取得します。これが答えです、つまり dp[totalアイテム数-1][バックパック最大容量]

コードの実装 (Java)

    public static void main(String[] args) {
    
    
        Scanner scanner = new Scanner(System.in);
        int bagWeight = scanner.nextInt(); // 背包最大容量
        int n = scanner.nextInt(); // 物品数量
        // 物品重量数组
        int[] weight = new int[n];
        for (int i = 0; i < n; i++) {
    
    
            weight[i] = scanner.nextInt();
        }
        // 物品价值数组
        int[] value = new int[n];
        for (int i = 0; i < n; i++) {
    
    
            value[i] = scanner.nextInt();
        }
        // 调用方法求解不超出最大容量的前提下,背包最多能背多大价值的物品
        System.out.println(bags(bagWeight, weight, value));

    }

    public static int bags(int bagWeight, int[] weight, int[] value) {
    
    
        int n = weight.length; // 物品数量
        int[][] dp = new int[n][bagWeight+1]; // dp数组,行表示物品,列表示从0到最大容量
        // 第一列表示背包容量为0时的情况,第一列应该全为0。
        // 由于建dp数组时,java会默认为数组赋0,所以保持第一列为0,更新第二列及以后的即可
        // 从上到下从左到右计算dp,右下角即答案
        for (int i = 0; i < n; i++) {
    
    
            for (int j = 1; j <= bagWeight; j++) {
    
    
                // 第一行表示只能选第一个物品
                if (i == 0) {
    
    
                    dp[i][j] = value[i];
                }
                // 剩余行表示有多个物品可选,需要考虑两种情况
                else {
    
    
                    // 情况1:背包容量就算只装一个物品i也装不下
                    if (j < weight[i]) {
    
    
                        dp[i][j] = dp[i-1][j];
                    }
                    // 情况2:背包容量可以装下物品i,需要考虑两种方案,然后取最大
                    else {
    
    
                        // 方案1:不装物品i
                        // 方案2:装物品i,最大价值为 物品i的价值 加上 去掉物品i的重量后背包剩余容量的最大价值
                        dp[i][j] = Math.max(dp[i-1][j], value[i] + dp[i-1][j-weight[i]]);
                    }
                }
            }
        }
        return dp[n-1][bagWeight]; // 答案是数组的右下角
    }

結果の dp 配列と答えは次のとおりです。

dp = 
[0, 15, 15, 15, 15]
[0, 15, 15, 20, 35]
[0, 15, 15, 20, 35]
answer = 35
  • 上級: 計算プロセスを観察します。dp は行ごとに計算されます。スペースを節約するために、データは 1 行のみ保存できます。
    public static int bags(int bagWeight, int[] weight, int[] value) {
    
    
        int n = weight.length; // 物品数量
        int[] dp = new int[bagWeight+1]; // dp数组,表示从0到最大容量可以装的最大价值
        // 第一个元素表示背包容量为0时的情况。
        // 由于建dp数组时,java会默认为数组赋0,所以保持第一个元素为0,更新第二个元素及以后的即可
        // 从左到右计算dp,最后一个元素即答案
        for (int i = 0; i < n; i++) {
    
    
            // 注意!!!在计算转移方程的过程中,我们需要用到上一次循环得到的dp数组,所以内层循环必须倒序,否则转移方程的dp[j-weight[i]]会被覆盖掉,二维数组不存在这个问题
            for (int j = bagWeight; j > 0; j--) {
    
    
                // 当背包容量可以装下物品i时
                if (j >= weight[i]) {
    
    
                    // 如果只有一个物品可选
                    if (i == 0) {
    
    
                        dp[j] = value[i];
                    }
                    // 如果有多个物品可选
                    else {
    
    
                        // 方案1:不装物品i
                        // 方案2:装物品i,最大价值为 物品i的价值 加上 去掉物品i的重量后背包剩余容量的最大价值
                        dp[j] = Math.max(dp[j], value[i] + dp[j-weight[i]]);
                    }
                }
            }
            System.out.println(Arrays.toString(dp));
        }

        return dp[bagWeight]; // 答案是数组的最后一个元素
    }

        return dp[bagWeight]; // 答案是数组的最后一个元素
    }

dp 配列を取得し、各ループの答えを取得します。

dp = [0, 15, 15, 15, 15]
dp = [0, 15, 15, 20, 35]
dp = [0, 15, 15, 20, 35]
answer = 35

思考の本質は計算プロセスと同じですが、スペースを節約するだけであることがわかります

残りのナップザックの問題

分析ルーチンは基本的に 01 の最値ナップザック問題と同じですが、次の点が異なります。

  • サイクル
  1. 0/1 ナップザック: 外側のループ項目配列、内側のループのナップザック容量。ローリング 1 次元配列が使用される場合、内側のループは最大容量から逆になります。
  2. バックパック全体: 外側ループのアイテム配列、内側ループのバックパック容量、内側ループの正のシーケンス
  3. 結合されたバックパック: 外側ループのバックパック容量、内側ループのアイテム配列、外側ループの正のシーケンス
  4. グループ化されたバックパック: これは非常に特殊で、3 つのループが必要です。外側のループ内のバックパックの数はバッグであり、内側の 2 層ループは、トピックの要件に従って 1、2、および 3 つのバックパック タイプのテンプレートに変換されます。
  • 状態遷移方程式
  1. 最値問題: dp[i] = max/min(dp[i], dp[i-nums]+1) または dp[i] = max/min(dp[i], dp[i-num]+nums) );
  2. 問題があります (ブール値): dp[i] = dp[i]||dp[i-num];
  3. 組み合わせ問題: dp[i] += dp[i-num];

4 分析例

LeetCode1049. 最後の石の重さ II

  • 問題をナップザック問題に抽象化します (ここが難しい点です)
  1. タイトル説明: 石の山から、毎回重さ x と y の石を 2 つ取り出します。x=y の場合は両方の石が粉砕されます。x<y の場合、2 つの石は 1 つの重さの yx 石になります。最小値を求めてください。最後に残った石の重さ。最小値は非負の数であると考えるのは簡単で、最小値は 0 です。
  2. 問題の変換: 石の山を 2 つの山に分割し、2 つの石の山の重さの差の最小値を求めます (詳しく説明: 2 つの石が得られるたびに、1 つを同時に投げると、最終的に 2 つの石の山が得られます) 2 つの石の山の重さをそれぞれ x、y とし、x=y の場合、2 つの石の山は粉砕されます。x < y の場合、2 つの石の山は重さ yx の石になります。 2 つの石の山の重さの差の最小値)
  3. 変換を続行します: この石の山の合計重量の合計は変更されません。最も完璧な状況は、2 つの石の山の重量が同じで、オフセットが 0 であることです。2 つの石の山の重さをできるだけ等しくするには、最初の石の山の重さを総重量の半分 (和/2) にできるだけ近づける必要があります。この和/2がバックパックの最大容量となり、選択したアイテムが全ての石となり、各石の重量がアイテムの価値となります。
  4. 01に変換し続ける ナップザック最大値問題:最大重量が和/2のナップザックがあり、重さの異なる石の束が与えられた場合、最大重量を超えずに積める石の最大重量を求めよナップザックの。
  5. 答えを計算します。 4 を仮定します。得られた答えは maxWeight です。その後、元の質問に戻ります。最初の石の山の重量は maxWeight、2 番目の山の石の重量は sum-maxWeight です。2 番目の石の重さは最初の石の重さ以上である必要があるため、2 つの石の重さの差は (sum-maxWeight)-maxWeight となります。
    単純化すると、答えは sum-2*maxWeight であり、maxWeight は 01 の最も価値のあるナップザック問題に抽象化して解くことができます。
  • コードの実装 (Java)
    public static void main(String[] args) {
    
    
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int[] stones = new int[n];
        for (int i = 0; i < stones.length; i++) {
    
    
            stones[i] = scanner.nextInt();
        }
        System.out.println(lastStoneWeightII(stones));
    }

    // 动规
    public static int lastStoneWeightII(int[] stones) {
    
    
        // 非显式的背包最大容量,需要计算
        int sum = 0;
        for (int i = 0; i < stones.length; i++) {
    
    
            sum += stones[i];
        }
        int maxWeight = sum/2; // 背包可以承受的最大重量
        
        // 把石头分成两堆,计算第一堆石头不超出sum/2的最大重量
        // dp表示i个石头时,最大容量为j时,背包最多可以装的重量
        int[] dp = new int[maxWeight+1];
        for (int i = 0; i < stones.length; i++) {
    
    
            // 注意!!用一维数组时,内循环必须倒序,否则状态转移方程用到的dp[j-stones[i]]已经被覆盖掉了
            for (int j = maxWeight; j > 0; j--) {
    
    
                // 边界,第一行,只有一个石头
                if (i == 0 && j >= stones[i]) {
    
    
                    dp[j] = stones[i];
                }
                // 有两个及以上石头
                else {
    
    
                    if (j >= stones[i]) {
    
    
               			// 两种方案(拿石头i或者不拿石头i)取最大重量
                        dp[j] = Math.max(dp[j], stones[i] + dp[j-stones[i]]);
                    }
                }
            }
        }
        // 计算两堆石头的差值,即答案
        return sum-2*dp[maxWeight];
    }

2. 区間動的計画法

1 つの問題解決テンプレート

区間 DP は実際には区間内の最適値を求めるもので
、一般にこの種の問題の状態を設定する場合、区間 ij と
f[i][j ] の最適値として f[i][j] を設定できます。これは 2 つの小さな間隔を結合することで得られます。これら 2 つの小さな間隔を分割するには、ループ変数 k を使用して列挙する必要があり、一般的な状態遷移方程式は次のとおりです。 f [i][j] = max/min (
f [i][j], f[i][k]+f[k][j]+something)
このトピックの実際の意味に従って適応する必要があります。間隔 dp のおおよその
テンプレートは次のとおりです。

for (int len=2;len<=n;len++)
    for (int i=1;i+len-1<=n;i++)
    {
    
    
        int j=i+len-1;
        for (int k=i;k<=j;k++)
            f[i][j]=max/min(f[i][j],f[i][k]+f[k][j]+something)
    }

len は間隔の長さを列挙し、i と j は間隔の開始点と終了点であり、k は間隔を分割するために使用されます。

2 分析例

Niuke.Shizi 合併

  • トピックの説明

画像の説明を追加してください

  • 問題を間隔 dp に変換する
  1. 5 つの砂の山があり、砂の重さが配列 nums=[1, 3, 4, 2, 5] で表されるとします。ここで、これら 5 つの砂の山の最小合計コストが必要になります (答えは次のとおりです)。 34)。比較のために次の 2 つの結合スキームを示し
    ここに画像の説明を挿入
    、最終的な総コストの計算プロセスを逆算して、法則を見つけます:
    5 つの砂の山の最小結合コスト = 5 つの砂の山の重量の合計 +前回マージされた 2 つのサブヒープの最小マージ コスト
  2. 法則を説明する例: 最初のスキームでは、2 と 5 の間に分割線があり、5 つの砂の山が 2 つの部分に分割されると想像できます。次に、5 つの砂の山の最小マージ コスト (nums=[1, 3, 4, 2, 5]) = (1+3+4+2+5) + 4 つの砂の山 (nums=[1, 3, 4, 2 ]) + 砂の山 1 つの最小合計コスト (nums=[5])
    しかし、実際には、2 と 5 (つまり、最初のスキーム) を半分に分割することも、4 と 2 を分割することもできます。 2 つの半分に分割する (つまり 2 番目のスキーム) だけでなく、3 と 4 を 2 つの半分に分割することもできます。したがって、5 つの砂の山を 2 つの半分に分割できるすべての状況を列挙し、要約したルールを使用して各状況の合計コストを計算し、最小値を取得する必要があります。(ここでは間隔の概念が反映されており、ポインタを使用して配列全体を 2 つの間隔に分割しています)
  3. 再帰を想像してください。このルールでは、「最後にマージされた 2 つのサブヒープの最小マージ コスト」を再帰によって計算できます。これは再帰のエントリに相当します。再帰出口が境界であり、ここには 2 つの境界があります:
    境界 1: 砂の山は 1 つだけあり、マージ コストは 0 です。
    境界 2: 砂の山は 2 つだけあり、マージ コストは合計です。 2 つの砂の山の重さ。
  4. 再帰を説明する例: 2. の例では、4 つの砂の山の最小合計コストを再帰的に入力し、同じ方法で計算できます。ポインタが 4 つの砂の山を 1、3 の山と 4、2 の山に分割する場合、4 つの砂の山の最小マージ コスト (nums=[1, 3, 4, 2]) = (1+3+4+) 2) + 2 つの砂の山の最小マージ コスト (nums=[1, 3]) + 2 つの砂の山の最小マージ コスト (nums=[4, 2])、2 つの砂の山は再帰的出口です。数値を使用して直接計算できます
  5. 再帰を動的ルールに変更します。再帰計算は外側から内側に向​​かって行われ、層ごとに再帰の深さに入り、出口に到達した場合にのみ、最外側の層まで層ごとに計算されます。何度も計算を繰り返すことになります。動的ルールとは、実際には時間を犠牲にして、裏から計算し、計算しながらフォームに記入し、多くの二重計算を避けるというアイデアです。
    したがって、動的ゲージの計算は境界から始まります。つまり、最初に砂山 1 山と砂山 2 山の最小合計コストを入力し、砂山 1 山と砂山 2 山に基づいて砂山の 3 山の最小合計コストを計算します。山、次に4山、そして5山...
  6. 動的ゲージの完全なプロセス:
    事前に合計を計算します。sum はサブパイルサンドの重量の合計を表します。たとえば、sum[2][4] はサブパイルサンドの重量の合計を表します。 =[4,2,5]
0 1 2 3 4
0 0 4 8 10 15
1 - 0 7 9 14
2 - - 0 6 11
3 - - - 0 7
4 - - - - 0

dp の初期化: ダイナミック ゲージの 2 次元配列 dp。サブパイル サンドの最小マージ コストを表します。たとえば、 dp[2][4] はサブパイル サンドの最小マージ コストを表します。 [4,2,5]。
対角線が砂山1つ、副対角線が砂山2つだとすると、残りの数はすべて超大きな数になります(最小コストを求めるので、最大コストを求める場合、余りはすべて超小さい数値なので比較すると便利です)

0 1 2 3 4
0 0 4 最大 最大 最大
1 - 0 8 最大 最大
2 - - 0 7 最大
3 - - - 0 6
4 - - - - 0

状態遷移方程式をテーブルに記入します
テーブルを下から上、左から右に記入します。dp 配列の右上隅の dp[0][4] は、nums=[1] の最小マージ コストを示します。 ,3,4,2,5], これが私たちが望む答えです。i
と j を使用して部分配列の 2 つの辺を表し、k を使用して部分配列を 2 つの区間に分割できるポインターを表します。列挙します。 k のすべてのケースを組み合わせたコストを計算し、最小値を取得します。状態遷移方程式は次のとおりです:
dp[i][j] = min (dp[i][j], sum[i][j] + dp[i][k] + dp[k+1][j])

0 1 2 3 4
0 0 4 12 20 34
1 - 0 7 15 28
2 - - 0 6 17
3 - - - 0 7
4 - - - - 0
  • コードの実装 (Java)
    public static void main(String[] args) {
    
    
        Scanner scanner = new Scanner(System.in);
        int N = scanner.nextInt();
        int[] nums = new int[N];
        for (int i = 0; i < nums.length; i++) {
    
    
            nums[i] = scanner.nextInt();
        }
        System.out.println(stonesCombine(N, nums));

    }

    static int stonesCombine(int N,int[] nums) {
    
    
        if (N == 0) {
    
    
            return -1; // 边界,0堆沙子
        }
        int[][] dp = new int[N][N]; // 从i到j的子数组的最小代价
        int[][] sum = new int[N][N]; // 从i到j的子数组的总代价
        // 初始化dp全为最大值,斜对角线全为0,副对角线全为两堆沙子之和
        for (int i = 0; i < N; i++) {
    
    
            for (int j = 0; j < N; j++) {
    
    
                if (i == j) {
    
    
                    dp[i][j] = 0; // 边界1,只有1堆沙子
                }
                else if (i+1 == j) {
    
    
                    dp[i][j] = nums[i] + nums[j]; // 边界2,只有2堆沙子
                }
                else {
    
    
                    dp[i][j] = Integer.MAX_VALUE; // 求最小值,初始化为最大值
                }
            }
        }
        // 计算sum
        for (int i = 0; i < N; i++) {
    
    
            for (int j = i+1; j < N; j++) {
    
    
                if (j == i+1) {
    
    
                    sum[i][j] = nums[i] + nums[j]; // 特殊情况,2堆沙子,1堆沙子总代价为0
                }
                else {
    
    
                    sum[i][j] = sum[i][j-1] + nums[j];
                }
            }
        }
        // 计算dp剩余部分,从下到上,从左到右
        for (int i = N-3; i >= 0; i--) {
    
    
            for (int j = i+2; j < N; j++) {
    
    
                // 枚举所有指针分割成两个区间的情况,取最小
                for (int k = i; k < j; k++) {
    
    
                    dp[i][j] = Math.min(dp[i][j], sum[i][j]+dp[i][k]+dp[k+1][j]);
                }
            }
        }
        // dp右上角即答案
        return dp[0][N-1];
    }
  • 高度
    空間の複雑さはさらに最適化でき、サブ配列の重みの合計を表す合計配列を 1 次元にすることができます。

要約と分析

  1. 動的プログラミングは、実際には時間のために空間を犠牲にするというアイデアであり、これは再帰の結果を記録し、繰り返しの計算を避けることと同じです。
  2. ナップサック問題の難しさは、問題がナップサック問題の特徴を持っているかどうかを確認し、それをナップサック問題に抽象化して解決できることです。
  3. 動的ゲージ グループを埋める最初のステップは、最初に境界を埋め、境界と状態遷移方程式に従って残りを計算することです。ここでの境界は実際には再帰の出口に相当し、状態遷移方程式は等価です再帰の入り口へ。
  4. 再帰の計算は外側から内側へ、動的ルールの計算は内側から外側へ

参考URL

おすすめ

転載: blog.csdn.net/weixin_46838605/article/details/130422867