LintCode揹包問題總結
揹包問題是動態規劃的一種題型,它的特點如下:
特點:
1. 用值作為dp維度
2. dp過程就是填寫矩陣
3. 可以用滾動陣列進行優化
有個揹包問題九講的連結推薦:揹包問題九講
Given n items with size Ai, an integer m denotes the size of a backpack. How full you can fill this backpack? 要求返回揹包最多能容納的大小是多大。
state:
f[i][j] 前i個物品,取出一些能否組成和為j
function:
f[i][j] =
1. f[i - 1][j - a[i]] // 能放下第i個物品,那麼要看除掉第i 個物品剩下的容量 j - a[i]時候與i - 1個物品的情況
2. f[i - 1][j] // 若前i-1個物品就能組成大小為j
Intialize:
f[0][0] = true;
Result:
檢查所有的f[n][j] (j = 0, 1, 2,..., n)
時間複雜度為O(m * n)
仔細觀察,這道題可以用滾動陣列優化,從而優化了空間複雜度:public int backpack(int capacity, int[] A) { // state dp[m][n]: if it can fill the capacity n from the first m items boolean dp[][] = new boolean[A.length + 1][capacity + 1]; // initialize for (int i = 0; i <= A.length; i++) { for (int j = 0; j <= capacity; j++) { dp[i][j] = false; } } dp[0][0] = true; // dp function for (int i = 1; i <= A.length; i++) { for (int j = 0; j <= capacity; j++) { dp[i][j] = dp[i - 1][j]; if (j >= A[i - 1] && dp[i - 1][j - A[i - 1]]) { dp[i][j] = true; } } } // result for (int i = capacity; i >= 0; i--) { if (dp[A.length][i]) { return i; } } return 0; }
public int backpackWithRollingArray(int capacity, int[] A) { // state dp[m][n]: if it can fill the capacity n from the first m items boolean dp[][] = new boolean[2][capacity + 1]; // initialize for (int i = 0; i < dp.length; i++) { for (int j = 0; j <= capacity; j++) { dp[i][j] = false; } } dp[0][0] = true; // dp function for (int i = 1; i <= A.length; i++) { for (int j = 0; j <= capacity; j++) { dp[i%2][j] = dp[(i - 1)%2][j]; if (j >= A[i - 1] && dp[(i - 1)%2][j - A[i - 1]]) { dp[i%2][j] = true; } } } // result for (int i = capacity; i >= 0; i--) { if (dp[A.length%2][i]) { return i; } } return 0; }
125. Backpack II
物品不僅有大小size,還有價值value,分別有2個數組表示每個物品的大小和價值,然後給一個大小為target的揹包,這個揹包裡能裝下的最大價值是多少。
State: f[i][j]表示前i個物品當中選出一些物品組成容量為j的最大價值
DP Function: f[i][j] = Max( f[i-1][j], f[i-1][j-A[i]]+Value[i]);
Initialize: f[0][0] = 0;
Result: f[m][n]
時間複雜度:O(m * n)
public int backPackII(int n, int[] A, int V[]) {
int m = A.length;
// state
int[][] dp = new int[m + 1][n + 1];
// initialize
dp[0][0] = 0;
// dp function
for (int i = 1; i <= m; i++) {
for (int j = 0; j <= n; j++) {
dp[i][j] = Math.max(dp[i][j], dp[i-1][j]);
if (A[i - 1] <= j) {
dp[i][j] = Math.max(dp[i][j], dp[i-1][j-A[i-1]]+V[i-1]);
}
}
}
// result
return dp[m][n];
}
440. Backpack III
這道題是上題Backpack II的變種,區別就每種item可以重複的選擇,基本思路跟上題一樣,只不過由於可以重複,所以在最內層迴圈還要再加一層while迴圈用於遍歷列舉重複的組合。時間複雜度是O(m * n * k)。
public int backPackIII(int[] A, int[] V, int n) {
int m = A.length;
// state
int[][] dp = new int[m + 1][n + 1];
// initialize
dp[0][0] = 0;
// dp function
for (int i = 1; i <= m; i++) {
for (int j = 0; j <= n; j++) {
int k = 0;
while (A[i - 1] * k <= j) {
dp[i][j] = Math.max(dp[i][j], dp[i-1][j-A[i-1]*k]+V[i-1]*k);
k++;
}
}
}
// result
int res = 0;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
res = Math.max(res, dp[i][j]);
}
}
return res;
}
562. Backpack IV
給定一些物品陣列和一個目標值,問有多少種可以組成目標的組合數,比如給定物品陣列 [2,3,6,7] 和目標值 7, 那麼就有2種可能:[7] 和 [2, 2, 3]。所以返回2。這道題也可以這樣描述:給1,2,5,10硬幣無數多個,請問湊80元的方案總數。
State:
dp[m][n] 前m種硬幣湊成n元的方案數量
DP Function:
dp[m][n] = dp[m - 1][n] + dp[m - 1][n - A[m] * 1] + dp[m - 1][n - A[m] * 2] + dp[m - 1][n - A[m] * 3] + ...
Initialize:
dp[0][0] = 1
result:
dp[m][n]
時間複雜度是O(m * n * k)
public int backpack(int [] nums, int target) {
// state dp[m][n]: the number of combinations that first m kinds of items form the target n
int m = nums.length, n = target;
int dp[][] = new int[m + 1][n + 1];
dp[0][0] = 1;
// dp function
for (int i = 1; i <= m; i++) {
for (int j = 0; j <= n; j++) {
int k = 0;
while (k * nums[i - 1] <= j) {
dp[i][j] += dp[i - 1][j - k * nums[i - 1]];
k++;
}
}
}
// result
return dp[m][n];
}
這道題是Backpack IV那道湊硬幣題目的變種,唯一的區別就是現在每種型別的硬幣是不可以重複的選擇。每個硬幣只能出現一次。
State:
dp[m][n] 前m種硬幣湊成n元的方案數量
DP Function:
dp[m][n] = dp[m - 1][n] + dp[m - 1][n - A[m] ];
Initialize:
dp[0][0] = 1
result:
dp[m][n]
時間複雜度是O(m * n)
public int backPackV(int[] nums, int target) {
// state dp[m][n]: the number of combinations that first m kinds of items form the target n
int m = nums.length, n = target;
int dp[][] = new int[m + 1][n + 1];
dp[0][0] = 1;
// dp function
for (int i = 1; i <= m; i++) {
for (int j = 0; j <= n; j++) {
dp[i][j] = dp[i - 1][j];
if (nums[i - 1] <= j) {
dp[i][j] += dp[i - 1][j - nums[i - 1]];
}
}
}
// result
return dp[m][n];
}
給定一個包含了一些數字的陣列,和一個目標值,從數組裡面取數做排列,使得排列的數字的和等於target。問有多少種排列方法。數字是闊以重複取出來的。比如給定陣列[1, 2, 4]和target值4。那麼能得到如下的組合。總共有6種,則返回6.
[1, 1, 1, 1]
[1, 1, 2]
[1, 2, 1]
[2, 1, 1]
[2, 2]
[4]
用dp[i]表示target為i的排列數有多少種。比如以上面那個例子為例,我們可以畫出如下的dp搜尋圖:
從而就不難寫出如下程式碼了:
public int backPackVI(int[] nums, int target) {
// state
int[] dp = new int[target + 1];
// initialize
dp[0] = 1;
// dp function
for (int i = 1; i <= target; i++) {
for (int num: nums) {
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
// result
return dp[target];
}
從一個數組中取k個數的和為target,求有多少種組合。假如陣列是[1,2,3,4], k = 2, target = 5。那麼有2種解:[1,4] 和 [2,3]。
state:
f[i][j][t]前 i 個數中取 j 個數出來組成和為 t 的組合數目
function:
f[i][j][t] = f[i - 1][j][t] + f[i - 1][j - 1][t - a[i - 1]] (不包括第i 個數的時候組成t的情況 + 包括第i個數的時候組成t的情況)
initialize:
f[i][0][0] = 1
result:
f[n][k][target]
時間複雜度:O(n * k * target)
public int kSum(int A[], int k, int target) {
int n = A.length;
// state
int[][][] dp = new int[n + 1][k + 1][target + 1];
// initialize
for (int i = 0; i <= n; i++) {
dp[i][0][0] = 1;
}
// dp function
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
for (int l = 0; l <= target; l++) {
dp[i][j][l] = dp[i-1][j][l];
if (l >= A[i-1]) {
dp[i][j][l] += dp[i-1][j-1][l-A[i-1]];
}
}
}
}
// result
return dp[n][k][target];
}
91. Minimum Adjustment Cost
題意有點複雜,給定一個整型陣列,調整這個陣列使得每兩個數之間的差值不超過給定target值。問你調整這個陣列所需要的最小開銷是多少。注意陣列中的每個數不會超過100,這是一個非常關鍵的條件。因為這樣的話,當前可取的值是1-100,並且與上一個值是在target的差值以內。那我們可以轉換成揹包問題:
State: f[i][v] 前i個數,第i個數調整為v,滿足相鄰兩數<=target,所需要的最小代價
Function: f[i][v] = min(f[i-1][v’] + |A[i]-v|, |v-v’| <= target)
Answer: f[n][a[n]-target~a[n]+target]
時間複雜度: O(n * A * T)
其實很簡單,就是當前index為v時,我們把上一個index從1-100全部過一次,取其中的最小值(判斷一下前一個跟當前的是不是abs <= target)
public int MinAdjustmentCost(ArrayList<Integer> A, int target) {
if (A == null || A.size() == 0) {
return 0;
}
// state
int[][] dp = new int[A.size() + 1][101];
// dp function
for (int i = 1; i <= A.size(); i++) {
for (int j = 1; j <= 100; j++) {
dp[i][j] = Integer.MAX_VALUE;
for (int k = 1; k <= 100; k++) {
if (Math.abs(k - j) > target) {
continue;
}
int diff = Math.abs(j - A.get(i - 1)) + dp[i-1][k];
dp[i][j] = Math.min(dp[i][j], diff);
}
}
}
// result
int res = Integer.MAX_VALUE;
for (int i = 1; i <= 100; i++) {
res = Math.min(res, dp[A.size()][i]);
}
return res;
}