01 バックパックについての質問はたくさんしたと思いますが、01 バックパックについて本当に理解していますか? 、私の記事を読んで、01 バックパック [コード + 図 + テキストの説明] をさらに理解してください。

1. 概念的な理解: 01 バックパックとは何ですか

01 バックパックの概念を理解すると、次のようになります。 01 バックパックは、M 個のアイテムからいくつかのアイテムを取り出し、それらを W のスペースを持つバックパックに入れることです。各アイテムの体積は W1、W2 ~ Wn であり、対応する値は P1、P2 ~ Pn です。001 バックパックの制約条件は、複数のアイテムが与えられ、各アイテムが 1 つだけ持ち価値と体積の 2 つの属性を持つことです。01 ナップザック問題では、各項目は 1 つだけであるため、各項目について選択と非選択の 2 つのケースのみを考慮する必要があります。バックパックに入れることを選択しない場合は、対処する必要はありません。バックパックに入れることを選択した場合、以前に置かれたアイテムがどのくらいのスペースを占めるかが明確ではないため、このアイテムをバックパックに入れた後にバックパックのスペースを占める可能性のあるすべての状況を列挙する必要があります。

2. 多くの人が01バックパックをよく理解していない理由

なぜ多くの人が01バックパックを十分に理解していないのかについて、私たちは次の2つの角度から分析します。

①01 ナップザック問題のコードは比較的短いですが、わかりにくいです。時間と労力は節約できますが、01 バックパックの問題を少し変更すると元に戻ってしまいます。早速プロトタイプ〜

②01バックパックの企画戦略は理解できても、01バックパックが作成する配列や配列添字の意味がまだ曖昧で、01バックパックの話題についてACすることもあれば、できないこともある01バックパックのdp配列の具体的な意味を知る

3.01 バックパックの理論戦略(暴力的再帰)

実際、01 バックパックの分析は、バックパックの容量、アイテムの価値、および対応する重量の分析として理解できます。アイテムごとに、バックパックに入れるかどうかの 2 つの選択肢があります。バックパックに入れる, アイテムの順序に従って前から後ろに戦略を選択します. 現在のアイテムがバックパックに入れられているかどうかに関係なく, 結果を保証する必要があります.最後のアイテムを入れるかどうか、アイテムの総重量はバックパックの総容量を超えてはいけません、バックパックに収納できる最大値については、要件を満たす最大値を選択するだけで済みます最後の項目について決定するたびに返却します。戦略にアイテムを追加する場合、アイテムの総重量がバックパックの総容量を超えた場合、この戦略を直接終了できます。この理論上の戦略は、暴力的な再帰的実現のアイデアにすぎません。バックパック問題。鉄は熱いうちに打つことを選択し、01 ナップザック問題に関する暴力的な再帰コード (メモ付き) を直接送信します。
 

public class Bag {
    public static void main(String[] args) {
        int[]w={3,4,5};
        int[]v={6,4,8};
        System.out.println(bestBag(w, v, 10));
    }
    public static int bestBag(int []w,int []v,int weight){
        //检查有效性
        if(w.length==1&&w[0]>weight){
            return 0;
        }
       return process(w,v,0,0,weight);
    }
    //递归过程

    /**
     * 
     * @param w 物品的重量数组
     * @param v 物品的价值数组
     * @param index 遍历的当前物品的序号
     * @param alWeight 当前所遍历过的物品中在选择放与不放之后的重量
     * @param weight 背包的容量
     * @return
     */
    private static int process(int[] w, int[] v, int index, int alWeight, int weight) {
        //判断出口
        if(alWeight>weight){
            return -1;
        }
        if(index==w.length){
            return 0;//成功
        }
        //第一种情况:当前节点上不放值(因为当前物品一旦加入到背包中,背包的总容量就超了)
        int value1=process(w, v, index+1, alWeight, weight);
        //第二种情况:当前结点放入值
        int value2Next=process(w, v, index+1, alWeight+w[index], weight);
        int value2=-1;
        if(value2Next!=-1){
            value2=value2Next+v[index];
        }
        //返回两种结果的最大值
        return Math.max(value1, value2);
    }
}

4. 01 ナップザック実現のための動的計画法戦略

4.1 2次元配列を利用した01ナップザックの動的計画法実現

動的計画法の問題に関して、私の実装と解析戦略は、①再帰配列要素の添字とその要素の意味②再帰方程式の実現③dp配列の初期化④dpの走査順序の5つの部分に分けることです。配列 ⑤エラー発生時、dp配列を出力することで問題箇所を迅速かつ正確に特定し、デバッグの効率を向上させます。

01 ナップザック問題の分析では、次の 5 つの側面から分析します。 ① 2 次元の dp 配列を作成します。たとえば、dp 配列の要素は dp[i][j] であり、i は現在走査する項目を表します。 、 j は現在の dp 配列の容量を表します (この場合、最初に項目を走査し、次にデフォルトで容量を走査します (1 レベルのループは項目の走査用で、2 レベルのループは容量の走査用です)。実際、逆方向のトラバーサルも可能です。後で説明します~)、 dp[i][j] の特定の値は、現在の i 項目の選択における j の容量を満たすことができる最大値を表します。

②再帰式: 01 バックパックの問題を分析します。バックパックの最大容量を達成したい場合は、バックパックの容量を可能な限りいっぱいにする必要があり、それは選択できるすべてのアイテムの中に含まれます。 present 選択をするので、それぞれのアイテムに直面して選択するとき、現在選択できるすべてのアイテムを使用したいとき、dp[i][ と組み合わせて、すべてのバックパックの残りの容量をできるだけ使いたいときj] 上で説明したように、 の意味は、現在の j 個の容量の下で現在の前の i 項目を選択するときに収容できる最大値であるため、dp[i][j] の再帰式は選択に基づいている必要があり、非現在のアイテムの選択戦略 最適解を取得します: dp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+value[i]),再帰的に計算式を説明します: dp[i-1][j] の意味 現在の項目を選択しません。現在の項目の影響を考慮する必要がないため、i-1 個の項目からのみ選択します。バックパックの容量に依存するため、バックパックの容量の選択では、現在選択できる最大のバックパック容量 j を選択します; dp[i][j-weight[i]]+value[i]: これが現在のアイテムです現在のアイテムがバックパックに入れられているため、バックパックに入れることを選択します。このアイテムを入れた後、収容できる最大容量は [j-weight[i]] のみであるため、j-weight[最初の i-1 項目の i] の場合、最適解を選択し、最後の項目の値を加算します。同時に、アイテムを先に走査するのか、バックパックの容量を先に走査するのかに関する上記の疑問についても説明します。結論を先に述べましょう。「大丈夫、なぜ?」再帰式と組み合わせた次の図を観察すると、各要素について、現在の要素の右側の要素と現在の要素の上の要素によって共同で起動されていることがわかります。最初に容量を走査します。要素の右側と上部の両方の要素が要素を満たすことがわかりますが、容量が最初に走査されるか (列による走査と比較して)、項目が最初に走査されます (行による走査)。 、右側と上の要素はすべて走査されるため、これらの走査メソッドは両方とも実行できます。

③初期化: (まだ上の図を結合しています) 容量が 0 の場合、アイテムがいくつあっても、単一のアイテムを収めることはできません。データの最初の列については、それを 0 に初期化します。アイテム 0 の場合、現在のバックパックの容量がアイテムの重量以上であれば、アイテムの値を配列に入れることができますが、現在のバックパックの容量がアイテムの重量未満の場合は、アイテムの重量、このバックパックケース内の値はまだ 0 ですが、関係しない他の行と列の値はどうなるのでしょうか? 再帰式から次のことがわかります。現在の行と列の要素の値は現在の要素とは関係ありませんが上と左の要素によって決定されるため、これらの要素に対して任意の値を初期化できます。 1 つを選択し、0 に初期化します

④走査順序: すべての要素は上と左の要素から一緒に派生するため、要素の走査順序は上から下、左から右に走査する必要があります。

⑤配列を印刷する: 最終結果が正しくないことがわかった場合は、配列を最初から最後まで印刷して、エラーをすばやく見つけることができます。

コード [コード + コメント] は次のとおりです。
 

/**
 * @author tongchen
 * @create 2023-04-13 15:26
 */
public class DpBag {
    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        MostWeightDpBag(weight, value, 4);

    }

    /**
     * 五部曲:1.dp[i][j] dp[i]是在0-i内存放东西的策略 j是背包容量为j,则dp[i][j]可以理解为在0-i的物品内和容量为j的背包所能存放的最大容量
     * 2.递推方程:dp[i][j]=math.max(dp[i-1][j],dp[i-1][j-w[i]])3.初始化当背包容量为0时,每一个i都是0,当物品为i时,每个背包能存放的最大价值为i
     * @param
     * @return
     */
    public static void MostWeightDpBag(int[]weight,int []value,int maxSize){
        //创建数组
        int[][]dp=new int[weight.length][maxSize+1];
        //初始化
        for(int i=weight[0];i<=maxSize;++i){
            dp[0][i]=value[0];
        }
        //循环
        //一层循环物品
        for(int i=1;i< weight.length;++i){
            //二层循环背包
            for(int j=1;j<=maxSize;++j){
                dp[i][j]=dp[i-1][j];
                if(weight[i]<=j){
                    dp[i][j]=Math.max(dp[i][j],dp[i][j-weight[i]]+value[i]);
                }
            }
        }
        for (int i = 0; i < weight.length; i++) {
            for (int j = 0; j <= maxSize; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
    }
}

4.2 1次元配列を利用して01ナップザックの動的計画法を実現する(ローリング配列)

4.2.1 2次元配列から1次元配列への変更

2 次元配列から 1 次元配列への実際の劣化を説明する前に、2 次元配列から 1 次元配列への変化を見てみましょう。2 次元配列から 1 次元配列へ, 重要な変更は、項目 i をキャンセルすることです。違いは、1 次元配列である j の添字のみが、j の容量内に収容できる最大値を表すことです。最も本質的な理由は、要素 dp が[i-1][j] は dp[i][ j] に直接コピーされます; さらに、他の変更を分析してみましょう: ① 走査順序は許可されなくなりましたが、最初に項目を走査し、次に容量を走査する必要があります。容量トラバーサルでは、大容量から小容量へのトラバーサル方法を採用する必要があります。 ②初期化について: 初期化要素の定義は、前の行の要素からコピーされなくなりましたが、選択 (dp[j]) され、配置するために選択されません。バックパック内の現在のアイテム (dp[j-weight[i]]+value [i]) が考慮され、その最大値が取得されるため、2 つの間の比較プロセスでは、初期化がそれに影響を与えることはできません。 、そして dp[j] の意味はまだ j の容量を下回っており、ナップザックは負うことができる最大値であるため、初期化を負にすることはできず、比較に影響を与えることはできないため、0 に初期化します (負でない最小の数)

4.2.2 問題を再分析し、2 次元配列から 1 次元配列に変更する難しさを分析する

まず問題を分析します。
やはり、①再帰配列要素の添字と要素の意味から開始します。 ②再帰方程式の実現 ③dp 配列の初期化 ④ dp 配列の走査順序 ⑤ エラーが発生した場合問題が発生した場合は、dp 配列をすばやく出力します 問題が発生した場所を正確に特定し、デバッグの効率を向上します これら 5 つの部分が分析されます。

①dp[j]はj容量以下でナップザックに収納できるアイテムの最大値を表し、jはナップザックの容量を表します

②現在の項目は選択されない(i-1要素が直接iにドロップされる)ため、再帰式は依然として現在の項目の選択と非選択から最大値を選択します: dp[ j -weight[i] ] は、 j -weight[i] の容量を持つナップザックが運ぶ最大値を表します。dp[j - 重量[i]] + 値[i] は、j - アイテム i の重量 + アイテム i の値の容量を持つバックパックを意味します。(つまり、アイテム i を dp[j] に入れた後の、容量 j のバックパックの値。この時点で、dp[j] には 2 つのオプションがあります。1 つは、独自の dp[j] を取得することです。二次元 dp 配列に相当します。 dp[i-1][j]、つまり項目 i を置かず、dp[j -weight[i]] + value[i] を取ることです。項目 i を入力し、指定された値は最大のものを取得します。結局のところ、それが最大値なので、再帰式は次のようになります。 dp[j]=max(dp[j],dp[j-weight[i]] +値[i]);

③初期化: dp[j] の意味: 容量が j のバックパックの場合、運ぶアイテムの値は dp[j] までであり、その場合は dp[0] は 0 でなければなりません。容量0のバックパックは0です。次に、dp 配列は添字 0 の位置を除いて 0 に初期化されますが、他にいくつの添字を初期化する必要がありますか? 再帰式を見てください: dp[j] = max(dp[j], dp[j -weight[i]] + value[i]); dp 配列は、導出時に最大値を持つ数値である必要があります。および初期化された値 これはバックパックが耐えることができる最大値であるため、初期化を負の数にすることはできず、比較に影響を与えることはできないため、0 (負ではない最小の数) に初期化します。

④走査順序: 2次元配列とは異なり、最初に1ビット配列内の項目を走査し、次に容量を走査する必要があり、容量は後ろから前に走査する必要があります(容量は大きいものから順に走査します)。小): おそらく現時点では、同志の皆さん、私は疑問を抱いています。なぜ最初に定員を越えることができないのでしょうか? なぜ容量を前から後ろ (大容量から小容量へ) に走査できないのかについては、順番に答えます。 ① 最初に容量を走査することが、配列を列ごとに走査する (列の順序で走査する) ことと同等である場合列ごとに)、確認する必要があるのは、j の現在の容量に対応できる最大値が正しいことです。その後、バックパックへの現在のアイテムの選択と非選択に対応する値が必要です。こちらも正解です。次の写真を使用してバックパックを分析します。

この列に従って移動すると、現在の容量が最大値ポリシーに対応できるという原則に実際に違反します。なぜそう言えるのですか? 列ごとにトラバースする場合、シーンを 2 次元配列に戻すことになります。これは、前の行の要素を直接コピーすることと同等であり、各要素が実際に収容可能な最大値を表すという事実に違反します。現在の容量項目の下にあります。

まだ少し抽象的だと思われる場合は、配列内の要素を出力し、結果とアイデアを確認できます

容量の移動がなぜ大容量から小容量まで移動する必要があるのか​​について話しましょう。小さいものから大きいものに移動すると、同じアイテムが繰り返し配置されることになるためです。

逆順トラバーサルは、項目 i が 1 回だけ挿入されることを保証するものです。ただし、正の順序でたどると、項目 0 が複数回追加されます。

例を挙げると、アイテム 0 の重量 Weight[0] = 1、値 value[0] = 15

この時点で dp[1] = dp[1 -weight[0]] + value[0] = 15dp[2] = dp[2 -weight[0]] + value[0] = 30 をトラバースする場合 dp[2] ] はすでに 30 になっており、これは項目 0 が 2 回挿入されていることを意味するため、順方向にたどることはできません。なぜ、逆順にたどることによって、項目が 1 回だけ挿入されることが保証されるのでしょうか? 逆の順序で計算すると、 dp[2] dp[2] = dp[2 - Weight[0]] + value[0] = 15 (dp 配列は 0 に初期化されています) dp[1] = dp[1 - Weight[0] ]] + value[0] = 15 なので、後ろから前にループし、毎回取得される状態は以前に取得された状態と一致しないため、各項目は 1 回だけ取得されます。それでも疑問がある場合は、配列を一度出力して、走査プロセス全体を整理することをお勧めします。

これに従って、コードを与えます:


/**
 * @author tongchen
 * @create 2023-04-14 18:00
 */
public class RollingDpBag {
    public static void main(String[] args) {

    }

    /**
     * 1.j代表容量为j2.dp[j]代表j容量下存放的最大值2.dp[j]=max(dp[j],dp[j-weight[i]]+value[i])3.dp[i]=0;4.第一层是物品,从上到下,第二层是容量,从左到右
     */
    public static void dpBag(int[]weight,int []value,int maxSize){
        int[]dp=new int[maxSize+1];
        for(int i=0;i< weight.length;++i){
            for(int j=maxSize;j>=weight[i];--j){
                dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
            }
        }


    }
}

おすすめ

転載: blog.csdn.net/m0_65431718/article/details/130177137