動態規劃篇——揹包問題
本次我們介紹動態規劃篇的揹包問題,我們會從下面幾個角度來介紹:
- 揹包問題概述
- 零一揹包問題
- 完全揹包問題
- 多重揹包問題
- 分組揹包問題
揹包問題概述
揹包問題算是很經典的動態規劃問題,我們在面試中也經常出現
首先我們給出動態規劃的思想:
然後我們簡單介紹一下揹包問題:
/*揹包問題*/
有 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]);
}
}
結束語
好的,關於動態規劃篇的揹包問題就介紹到這裡,希望能為你帶來幫助~