動態規劃篇——揹包問題

秋落雨微涼發表於2022-11-23

動態規劃篇——揹包問題

本次我們介紹動態規劃篇的揹包問題,我們會從下面幾個角度來介紹:

  • 揹包問題概述
  • 零一揹包問題
  • 完全揹包問題
  • 多重揹包問題
  • 分組揹包問題

揹包問題概述

揹包問題算是很經典的動態規劃問題,我們在面試中也經常出現

首先我們給出動態規劃的思想:

然後我們簡單介紹一下揹包問題:

/*揹包問題*/

有 N 件物品和一個容量是 V 的揹包。

第 i 件物品的體積是 vi,價值是 wi。

求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大,輸出最大價值。

/*輸入格式*/
    
第一行兩個整數,N,V,用空格隔開,分別表示物品數量和揹包容積。

接下來有 N 行,每行兩個整數 vi,wi,用空格隔開,分別表示第 i 件物品的體積和價值。

/*輸出格式*/
    
輸出一個整數,表示最大價值。

最後我們介紹我們下列將要講述了揹包問題的前提:

/*01揹包問題*/

每件物品只能使用一次
    
/*完全揹包問題*/
    
每件物品無次數限制使用
    
/*多重揹包問題*/
    
每件物品有不同的使用次數
    
/*分組揹包問題*/
    
每組物品有若干個,同一組內的物品最多隻能選一個

零一揹包問題

我們首先介紹一下01揹包規則:

/*揹包問題*/

有 N 件物品和一個容量是 V 的揹包。

第 i 件物品的體積是 vi,價值是 wi。

求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大,輸出最大價值。

/*輸入格式*/
    
第一行兩個整數,N,V,用空格隔開,分別表示物品數量和揹包容積。

接下來有 N 行,每行兩個整數 vi,wi,用空格隔開,分別表示第 i 件物品的體積和價值。

/*輸出格式*/
    
輸出一個整數,表示最大價值。
    
/*限制條件*/
    
每件物品只能使用一次

然後我們對其進行分析:

/*內容分析*/

首先我們有 N 件物品,總容量為 V
    
如果我們想要求得最大 W 的情況,我們就需要計算所有的 N 和 V 情況
    
/*暴力求解方法分析*/
    
我們這裡首先採用最暴力的方法(二維):
    
我們採用f[i][j]來表示前i件物品中進行選擇,其體積不超過j,儲存值為W最優解
    
我們會發現,f[i][j]無非就兩種情況:
    在i比前一位增加一位後,如果我們當前的i沒有包含最後一位,那麼一切都和上一位i的結果相同(f[i][j] = f[i-1][j])
    那麼我們就只需要判斷是否需要加上第i位,且前提是j >= v[i](f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]))
    	
/*最佳化方法分析*/
 
下面我們介紹的最佳化方法來自於滾動陣列:
    滾動陣列是指當我們只需要兩行資料時,我們可以丟擲二維的概念,採用層級差來覆蓋掉之前的資料資訊,從而轉換為一維
    
我們對上述暴力求解進行分析:
    我們會發現其實我們所採用的無非只有兩行:f[i]和f[i-1]
    那麼我們只需要將f[i]所使用的f[i-1]的資訊在使用前保留下來,我們就可以將其簡化為一行,也就是一維

我們給出實際程式碼以及程式碼中的解析:

/*暴力求解方法*/

import java.util.Scanner;

public class Packsack01 {

    final static int N = 1010;

    static int n,m;

    // 存放f[][],v[],w[]
    static int[][] f = new int[N][N];
    static int[] v = new int[N];
    static int[] w = new int[N];

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);
        
        n = scanner.nextInt();
        m = scanner.nextInt();
        
        // 首先我們應該初始化f[][],但是由於需要初始化為0,陣列預設為0,所以我們不需要書寫
        
        // 然後我們放入v,w
        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }
        
        // 然後我們就可以逐層更新(第0層為前0個物品,肯定都是0,不用更新)
        for (int i = 1; i <= n; i++) {
            // 我們對前i個物品的體積v也進行遞增
            for (int j = 0; j <= m; j++) {
                
                // 如果我們不加入最後一個數,那麼當前i層的值和i-1層的值相同
                f[i][j] = f[i-1][j];
                
                // 注意:由於加入第i個數不一定是最優解,所以我們需要進行w權重比較
                // 我們比較的資料分別是上一層的不加i的w
                // 注意這裡由於上面f[i][j] = f[i-1][j],所以下面的f[i][j]實際上是上一層的f[i-1][j]
                // 以及我們該層加上i之後的w,我們加上i之後v就需要去掉v[i]
                // 同時我們選取前i-1個數的v為j-[v[i]]的w最優解加上w[i]來進行比較
                if (j >= v[i]) f[i][j] = Math.max(f[i][j],f[i-1][j-v[i]]+w[i]);
            }
        }

        // 最後輸出即可
        System.out.println(f[n][m]);
    }

}

/*最佳化求解方法*/

import java.util.Scanner;

public class Packsack01 {

    final static int N = 1010;

    static int n,m;

    // 存放f[],v[],w[]
    static int[] f = new int[N];
    static int[] v = new int[N];
    static int[] w = new int[N];


    // 建議和暴力求解對比觀看
    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);
        
        n = scanner.nextInt();
        m = scanner.nextInt();
        
        // 首先我們應該初始化f[],但f[0]最開始都是0,就不需要初始化了

        // 然後我們放入v,w
        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }
        
        // 然後我們就可以逐層更新
        for (int i = 1; i <= n; i++) {
            // 我們對前i個物品的體積v也進行遞增
            // 注意:由於下面判斷條件需要保證j>=v[i],所以我們這裡可以直接從v[i]開始,畢竟前面的條件都不滿足
            // 注意:我們這裡需要倒敘書寫, 因為我們下面要使用f[i-1][j-v[i]]這裡的i-1就是上一層,我們需要注意我們不能覆蓋掉這一層!!!
            for (int j = m; j >= v[i]; j--) {

                // 這裡簡化i之後,為f[j] = f[j],恆等式,我們就直接省略了

                // 注意:由於加入第i個數不一定是最優解,所以我們需要進行w權重比較
                // 我們比較的資料分別是上一個不加i的w
                // 以及我們該層加上i之後的w,我們加上i之後v就需要去掉v[i]
                // 同時我們選取前i-1個數的v為j-[v[i]]的最優解來進行比較,記得加上w[i]
                
                // 這裡我們需要注意,我們後面比較的值是上一層的f
                // 所以我們前面的for迴圈的方向需要轉換一下,防止上一層的資料被覆蓋掉
                f[j] = Math.max(f[j],f[j-v[i]]+w[i]);
            }
        }

        // 最後輸出即可
        System.out.println(f[m]);
    }

}

完全揹包問題

我們首先介紹一下完全揹包規則:

/*揹包問題*/

有 N 件物品和一個容量是 V 的揹包。

第 i 件物品的體積是 vi,價值是 wi。

求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大,輸出最大價值。

/*輸入格式*/
    
第一行兩個整數,N,V,用空格隔開,分別表示物品數量和揹包容積。

接下來有 N 行,每行兩個整數 vi,wi,用空格隔開,分別表示第 i 件物品的體積和價值。

/*輸出格式*/
    
輸出一個整數,表示最大價值。
    
/*限制條件*/
    
每件物品沒有使用次數限制

然後我們對其進行分析:

/*內容分析*/

首先我們有 N 件物品,總容量為 V
    
如果我們想要求得最大 W 的情況,我們就需要計算所有的 N 和 V 情況
    
/*暴力求解方法分析*/
    
我們首先介紹暴力求解方法:

我們其實所有步驟和01揹包的步驟相似,但不同的是對於第i個物品的數量的大小的決定
    
我們在不能承載第i個物品前:f[i][j] = f[i-1][j]
 
在我們能承載第i個物品後:f[i][j] = Math.max(f[i-1][j],f[i-1][j - k*v[i]] + k * w[i])
    
所以我們只需要在01揹包基礎上加上一個fork迴圈來控制第i個物品的數量保證最優解即可
    	
/*最佳化方法1分析*/
    
我們在上述暴力求解中直接採用了fork迴圈,這時我們的時間複雜度在 O(n^3),所以我們想要減少時間複雜度
    
我們可以發現,我們上述承載第i個物品後:f[i][j] = Math.max(f[i-1][j],f[i-1][j - k*v[i]] + k * w[i])
    
那麼相當於:f[i][j] = Math.max(f[i-1][j],f[i-1][j-v[i]]+w[i],f[i-1][j-2*v[i]]+2*w[i],...)
    
相當於我們直接在上一個f[i][j]的基礎上判斷是否能夠新增i物品,也就是:f[i][j] = Math.max(f[i][j],f[i][j-v[i]]+w[i])

/*最佳化方法2分析*/
    
最後一重最佳化其實就是01揹包的最佳化,我們轉化為滾動陣列即可    
    
下面我們介紹的最佳化方法來自於滾動陣列:
    滾動陣列是指當我們只需要兩行資料時,我們可以丟擲二維的概念,採用層級差來覆蓋掉之前的資料資訊,從而轉換為一維
    
我們對上述暴力求解進行分析:
    我們會發現其實我們所採用的無非只有兩行:f[i]和f[i-1]
    那麼我們只需要將f[i]所使用的f[i-1]的資訊在使用前保留下來,我們就可以將其簡化為一行,也就是一維

我們給出實際程式碼以及程式碼中的解析:

/*暴力求解演算法*/

import java.util.Scanner;

public class PacksackFull {

    final static int N = 1010;

    static int n,m;

    static int[][] f = new int[N][N];
    static int[] v = new int[N];
    static int[] w = new int[N];

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        n = scanner.nextInt();
        m = scanner.nextInt();

        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }


        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                // 重點在這裡!!!
                // 我們之前的f[i][j] = f[i-1][j]也融入到下面的max判斷裡去了
                // 我們由於需要判斷第i個物品的數量,我們需要從0開始,判斷應該增加幾個i物品
                for (int k = 0; k * v[i] <= j; k++) {
                    f[i][j] = Math.max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
                }
            }
        }

        System.out.println(f[n][m]);
    }

}

/*最佳化演算法1*/

import java.util.Scanner;

public class PacksackFull {

    final static int N = 1010;

    static int n,m;

    static int[][] f = new int[N][N];
    static int[] v = new int[N];
    static int[] w = new int[N];

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        n = scanner.nextInt();
        m = scanner.nextInt();

        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }


        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                // 我們為了減少一層迴圈,我們直接將f[i][j]與前面的f[i][j]比較即可

                // 注意:記得先給當前f[i][j]賦值
                f[i][j] = f[i-1][j];

                // 然後我們才能進行比較,我們將f[i-1][j]與f[i]層的比較(記得判斷是否可以加v[i]!)
                if(j >= v[i]) f[i][j] = Math.max(f[i][j],f[i][j-v[i]]+w[i]);
            }
        }

        System.out.println(f[n][m]);
    }

}

/*最佳化演算法2*/

import java.util.Scanner;

public class PacksackFull {

    final static int N = 1010;

    static int n,m;

    static int[] f = new int[N];
    static int[] v = new int[N];
    static int[] w = new int[N];

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        n = scanner.nextInt();
        m = scanner.nextInt();

        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }


        for (int i = 1; i <= n; i++) {
            // 注意:由於這次我們使用的是第i層的資料,所以我們需要從前往後遍歷提前更新第i層資料,防止使用第i-1層資料
            for (int j = v[i]; j <= m; j++) {
                if(j >= v[i]) f[j] = Math.max(f[j],f[j-v[i]]+w[i]);
            }
        }

        System.out.println(f[m]);
    }

}

多重揹包問題

我們首先介紹一下多重揹包規則:

/*揹包問題*/

有 N 件物品和一個容量是 V 的揹包。

第 i 種物品最多有 si 件,每件體積是 vi,價值是 wi。

求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大,輸出最大價值。

/*輸入格式*/
    
第一行兩個整數,N,V,用空格隔開,分別表示物品數量和揹包容積。

接下來有 N 行,每行三個整數 vi,wi,si,用空格隔開,分別表示第 i 種物品的體積、價值和數量。

/*輸出格式*/
    
輸出一個整數,表示最大價值。
    
/*限制條件*/
    
每個物品有一定的使用次數限制

然後我們對其進行分析:

/*內容分析*/

首先我們有 N 件物品,總容量為 V
    
如果我們想要求得最大 W 的情況,我們就需要計算所有的 N 和 V 情況
    
/*暴力求解方法分析*/
    
其實暴力求解方法和完全揹包問題暴力求解方法完全相同
    
只不過是在k的限制條件上多加了一個k < s[i]的限制而已
    	
/*最佳化方法分析*/
    
我們需要注意多重揹包最佳化由於有數量限制的原因,無法使用完全揹包最佳化!
    
我們因為多重揹包有數量限制,當數量較少時,我們採用暴力求解是沒有問題的,但是當s數量過多,高達一兩千就會導致問題
    
我們的最佳化思路是
    透過將該物品打包分類為多個新的物品,重新定義這些物品的v和w,s固定為1
    我們選擇2的n次冪來打包物品,因為2的n次冪相加可以組成2的n+1次冪內的所有數!

我們給出實際程式碼以及程式碼中的解析:

/*暴力求解演算法*/

import java.util.Scanner;

public class PacksackNumber {

    final static int N = 1010;

    static int n,m;

    // 多新增一個s陣列存放個數
    static int[][] f = new int[N][N];
    static int[] v = new int[N];
    static int[] w = new int[N];
    static int[] s = new int[N];

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        n = scanner.nextInt();
        m = scanner.nextInt();

        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
            s[i] = scanner.nextInt();
        }


        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                // 重點在這裡!!!
                // 我們只需要多一個條件 k <= s[i]即可
                for (int k = 0; k <= s[i] && k * v[i] <= j; k++) {
                    f[i][j] = Math.max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
                }
            }
        }

        System.out.println(f[n][m]);
    }

}

/*最佳化演算法*/

import java.util.Scanner;

public class PacksackNumber {

    // 因為是二進位制,一個數最多就是2的12次方就會超過題目給的2000,所以給個將限制範圍1000*12
    final static int N = 12000;

    static int n,m;

    // 這裡的f採用一維即可,因為最後我們會轉變為01問題,可以採用滾動陣列最佳化
    static int[] f = new int[N];
    
    // 我們這裡只需要記錄v,w即可,因為我們會根據輸入的資料重新更新v,w,不再存在s的概念
    static int[] v = new int[N];
    static int[] w = new int[N];

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        n = scanner.nextInt();
        m = scanner.nextInt();

        // 表示定義到第幾個資料
        int cnt = 0;
        
        // 我們根據輸入的資料重新定義v,w
        for (int i = 1; i <= n; i++) {
            // a是v,b是w,s是數量
            int a = scanner.nextInt();
            int b = scanner.nextInt();
            int s = scanner.nextInt();
            
            // 我們根據2的k次冪來劃分s,重新分成物品
            int k = 1;// 相當於2的0次冪
            
            // 一直更新到無法存放k
            while ( k <= s){
                // 更新資料位置
                cnt++;
                // 將資料存入
                v[cnt] = a * k;
                w[cnt] = b * k;
                // 數量減去已存放的k,並且k翻倍(2次冪)
                s -= k;
                k *= 2;
            }
            
            // 判斷是否有剩餘元素
            if (s > 0){
                // 若存放剩餘元素,我們還需要存放
                cnt ++;
                v[cnt] = a * s;
                w[cnt] = b * s;
            }
        }
        
        // 我們目前擁有cnt個新物品(被我們分解的)
        n = cnt;
        
        // 我們對新物品進行裝載即可(01揹包)
        for (int i = 1; i <= n; i++) {
            for (int j = m; j >= v[i] ; j--) {
                f[j] = Math.max(f[j],f[j - v[i]] + w[i]);
            }
        }

        System.out.println(f[m]);
    }
}

分組揹包問題

我們首先介紹一下分組揹包規則:

/*揹包問題*/
    
有 N 組物品和一個容量是 V 的揹包。
    
每件物品的體積是 vij,價值是 wij,其中 i 是組號,j 是組內編號。

求解將哪些物品裝入揹包,可使物品總體積不超過揹包容量,且總價值最大,輸出最大價值。

/*輸入格式*/
    
第一行有兩個整數 N,V,用空格隔開,分別表示物品組數和揹包容量。

接下來有 N 組資料:

每組資料第一行有一個整數 Si,表示第 i 個物品組的物品數量;
    
每組資料接下來有 Si 行,每行有兩個整數 vij,wij,用空格隔開,分別表示第 i 個物品組的第 j 個物品的體積和價值;

/*輸出格式*/
    
輸出一個整數,表示最大價值。
    
/*限制條件*/
    
每組物品有若干個,同一組內的物品最多隻能選一個。

然後我們對其進行分析:

/*內容分析*/

首先我們有 N 組物品,總容量為 V
    
如果我們想要求得最大 W 的情況,我們就需要計算所有的 N組物品中每種物品使用 和 V 情況

/*求解方法分析*/
    
我們同樣採用一層迭代一層的原則,但由於每組商品只能選擇一次,所以我們在f[i][j]的情況下,需要與第i組的所有物品互動判斷一次
    
同樣我們由於f[i]只利用f[i-1]層原理,我們可以採用滾動陣列的原理來將二維陣列變為一維陣列

我們給出實際程式碼以及程式碼中的解析:

/*已最佳化演算法*/

import java.util.Scanner;

public class Packsack01 {
    final static int N = 110;

    static int n,m;

    // 這裡的f採用一維即可
    static int[] f = new int[N];

    // 我們這裡使用了分組概念,需要二維陣列記錄資訊
    static int[][] v = new int[N][N];
    static int[][] w = new int[N][N];

    // s這裡記錄該組的個數
    static int[] s = new int[N];

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        n = scanner.nextInt();
        m = scanner.nextInt();

        // 對分組輸入資料
        for (int i = 1; i <= n; i++) {
            // 記錄該組數量
            s[i] = scanner.nextInt();
            for (int j = 1; j <= s[i]; j++) {
                // 記錄v,w
                v[i][j] = scanner.nextInt();
                w[i][j] = scanner.nextInt();
            }
        }

        // 開始遍歷即可
        for (int i = 1; i <= n; i++) {
            for (int j = m; j >= 0; j--) {
                for (int k = 0;k <= s[i]; k++) {
                    if (v[i][k] <= j){
                        f[j] = Math.max(f[j],f[j - v[i][k]] + w[i][k]);
                    }
                }
            }
        }

        System.out.println(f[m]);
    }
}

結束語

好的,關於動態規劃篇的揹包問題就介紹到這裡,希望能為你帶來幫助~

相關文章