Package Problem

dp数组的初始化、状态转移方程、遍历顺序至关重要

其中的关键是状态转移方程,初始化以及遍历顺序都由状态转移方程确定

01背包

01背包-二维dp数组(m行)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package 背包;

public class bag01_2d {
public static void main(String[] args) {
int[] weight = {4, 1, 3};
int[] value = {30, 15, 20};
int capacity = 4;
int res = calcMaxValue(weight, value, capacity);
System.out.println("res: " + res);
}

private static int calcMaxValue(int[] weight, int[] value, int capacity) {
int[][] dp = new int[weight.length][capacity + 1];
/*
* dp[i][j]:容量为j的背包,在物品0~i中任选,能获得的最大价值
* 状态转移;if(j >= weight[i]) dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) 选物品i或不选物品i
* else dp[i][j] = dp[i - 1][j] 不选物品i
* 初始化:由状态转移可以看出,dp[i][j]依赖于dp[i - 1][j],所以需要初始化第0行
* 第0行:选择物品0.如果j > weight[0],dp[0][j] = value[0]
* 第0列:背包容量为0,什么都装不下,dp[i][0] = 0
* */
for (int j = 0; j <= capacity; ++j)
dp[0][j] = j >= weight[0] ? value[0] : 0;
for (int i = 1; i < weight.length; ++i) {
for (int j = 1; j <= capacity; ++j) {
if (j >= weight[i])
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
else
dp[i][j] = dp[i - 1][j];
}
}
for (int i = 0; i < weight.length; ++i) {
for (int j = 0; j <= capacity; ++j)
System.out.print(dp[i][j] + "\t");
System.out.println();
}
System.out.println();
return dp[weight.length - 1][capacity];
}
}

输出

1
2
3
4
5
0	0	0	0	30	
0 15 15 15 30
0 15 15 20 35

res: 35

01背包-二维dp数组(两行)

状态转移:

1
2
3
4
if(j >= weight[i]) 
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) 选物品i或不选物品i
else
dp[i][j] = dp[i - 1][j] 不选物品i

可以看出dp[i][j]只可能被dp[i - 1][j]和dp[i - 1][j - weight[i]]推导出,也就是说第i行只与第i - 1行关联,那么dp数组只需要用两行

与使用m行的代码相比,只需要将dp数组更改为2行,dp[i]与dp[i - 1]改为dp[i % 2]与dp[(i - 1) % 2]

初始化后(遍历完第0个物品后)的dp数组为

1
2
0	0	0	0	30	
0 0 0 0 0

遍历完第1个物品后的dp数组为

1
2
0	0	0	0	30	
0 15 15 15 30

遍历第2个物品时,只需要遍历第1个物品的dp数据,不需要遍历第0个物品的dp数据,所以把遍历第2个物品的dp数据覆盖在第0行

1
2
0	0	0	20	35	
0 15 15 15 30

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package 背包;

public class bag01_2d {
public static void main(String[] args) {
int[] weight = {4, 1, 3};
int[] value = {30, 15, 20};
int capacity = 4;
int res = calcMaxValue(weight, value, capacity);
System.out.println("res: " + res);
}

private static int calcMaxValue(int[] weight, int[] value, int capacity) {
int[][] dp = new int[2][capacity + 1];
for (int j = 0; j <= capacity; ++j)
dp[0][j] = j >= weight[0] ? value[0] : 0;
for (int i = 1; i < weight.length; ++i) {
for (int j = weight[i]; j <= capacity; ++j) {
dp[i % 2][j] = Math.max(dp[(i - 1) % 2][j], dp[(i - 1) % 2][j - weight[i]] + value[i]);
}
}
for (int i = 0; i < 2; ++i) {
for (int j = 0; j <= capacity; ++j)
System.out.print(dp[i][j] + "\t");
System.out.println();
}
System.out.println();
return dp[(weight.length - 1) % 2][capacity];
}
}

输出

1
2
3
4
0	0	0	20	35	
0 15 15 15 30

res: 35

01背包-滚动数组(一维dp数组)

两行能搞定,一行也行!

现在使用一维dp数组

1
int[] dp = new int[capacity + 1];

回顾一下使用二维dp数组是如何填表的

1
2
3
4
5
6
0	0	0	0	30	
0 15 15 15 30
0 15 15 20 35
// int[] weight = {4, 1, 3};
// int[] value = {30, 15, 20};
// int capacity = 4;

直接看第二行最后一个元素35(此时i = 2, j = 4),它是由dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);更新

dp[i - 1][j - weight[i]] = dp[1][1] = 15, value[i] = 20

35是由左上方的15加上value[2]得到的

所以使用一维滚动数组,在更新dp[4]的时候(相对于两维数组是要更新dp[2][4]),

需要上一行的dp[1](相对于二维dp数组是dp[1][1]),

那么j的遍历顺序一定要从后往前!否则(从前往后),dp[1]是二维dp数组中dp[2][1]的数据

1
for (int j = capacity; j >= weight[i]; --j)

代码

初始化为0即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package 背包;

public class bag01 {


public static void main(String[] args) {
int[] weight = {4, 1, 3};
int[] value = {30, 15, 20};
int capacity = 4;
int res = calcMaxValue(weight, value, capacity);
System.out.println(res);
}

private static int calcMaxValue(int[] weight, int[] value, int capacity) {
int[] dp = new int[capacity + 1];
for (int i = 0; i < weight.length; ++i)
for (int j = capacity; j >= weight[i]; --j)
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
for (int j = 0; j <= capacity; j++){
System.out.print(dp[j] + " ");
}
return dp[capacity];
}
}

输出

1
0 15 15 20 35 35

494. 目标和

方法一:记忆化搜索背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public int findTargetSumWays(int[] nums, int target) {
this.nums = nums;
n = nums.length;
for (int x : nums)
target += x;
if (target % 2 != 0 || target < 0)
return 0;
target /= 2;
dp = new int[n][target + 1];
for (int[] arr : dp)
Arrays.fill(arr, -1);
return dfs(0, target);
}

private int dfs(int index, int target) {
if (index == n)
return target == 0 ? 1 : 0;
if (dp[index][target] != -1)
return dp[index][target];
int choose = 0, pass = dfs(index + 1, target);
if (target >= nums[index])
choose = dfs(index + 1, target - nums[index]);
return dp[index][target] = pass + choose;
}

int[] nums;
int n;
int[][] dp;
}

方法二:01背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int n = nums.length;
for (int x : nums)
target += x;
if (target % 2 != 0 || target < 0)
return 0;
target /= 2;
int[][] dp = new int[n + 1][target + 1];
dp[0][0] = 1;
for (int i = 0; i < n; ++i) {
for (int j = 0; j <= target; ++j) {
dp[i + 1][j] = dp[i][j];
if (j >= nums[i])
dp[i + 1][j] += dp[i][j - nums[i]];
}
}
return dp[n][target];
}
}

方法三:滚动数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int n = nums.length;
for (int x : nums)
target += x;
if (target % 2 != 0 || target < 0)
return 0;
target /= 2;
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 0; i < n; ++i)
for (int j = target; j >= nums[i]; --j)
dp[j] += dp[j - nums[i]];
return dp[target];
}
}

322. 零钱兑换

方法一:记忆化搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public int coinChange(int[] coins, int amount) {
this.coins = coins;
n = coins.length;
dp = new int[n][amount + 1];
for (int[] arr : dp)
Arrays.fill(arr, -1);
int res = dfs(0, amount);
return res == Integer.MAX_VALUE / 2 ? -1 : res;
}

private int dfs(int index, int amount) {
if (index == n)
return amount == 0 ? 0 : Integer.MAX_VALUE / 2;
if (dp[index][amount] != -1)
return dp[index][amount];
if (coins[index] > amount)
return dfs(index + 1, amount);
return dp[index][amount] = Math.min(dfs(index + 1, amount), dfs(index, amount - coins[index]) + 1);
}

int[] coins;
int n;
int[][] dp;
}

方法二:完全背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
for (int[] arr : dp)
Arrays.fill(arr, Integer.MAX_VALUE >> 1);
dp[0][0] = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j <= amount; ++j) {
dp[i + 1][j] = dp[i][j];
if (coins[i] <= j)
dp[i + 1][j] = Math.min(dp[i][j], dp[i + 1][j - coins[i]] + 1);
}
}
return dp[n][amount] == Integer.MAX_VALUE >> 1 ? -1 : dp[n][amount];
}
}

方法三:滚动数组

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int[] dp = new int[amount + 1];
Arrays.fill(dp, Integer.MAX_VALUE >> 1);
dp[0] = 0;
for (int i = 0; i < n; ++i)
for (int j = 0; j <= amount; ++j)
if (j >= coins[i])
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
return dp[amount] == Integer.MAX_VALUE >> 1 ? -1 : dp[amount];
}
}

2787. 将一个数字表示成幂的和的方案数

方法一:01背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int numberOfWays(int n, int x) {
int mod = (int) 1e9 + 7;
// 01背包, 从1^x, 2^x,...,n^x里选物品,恰好能装满容量为n的背包的方案数
int[][] dp = new int[n + 1][n + 1];
// 没有物品,恰好装满容量为0的背包的方案数:1,什么都不装
dp[0][0] = 1;
for (int i = 1; i <= n; ++i) {
int s = (int) Math.pow(i, x);
for (int j = 0; j <= n; ++j) {
dp[i][j] = dp[i - 1][j];
if (j >= s)
dp[i][j] = (dp[i][j] + dp[i - 1][j - s]) % mod;
}
}
return dp[n][n];
}
}

方法二:滚动数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int numberOfWays(int n, int x) {
int mod = (int) 1e9 + 7;
// 01背包, 从1^x, 2^x,...,n^x里选物品,恰好能装满容量为n的背包的方案数
int[] dp = new int[n + 1];
// 没有物品,恰好装满容量为0的背包的方案数:1,什么都不装
dp[0] = 1;
for (int i = 1; i <= n; ++i) {
int s = (int) Math.pow(i, x);
for (int j = n; j >= s; --j) {
dp[j] = (dp[j] + dp[j - s]) % mod;
}
}
return dp[n];
}
}

方法三:静态处理

所有x,n在static语句块中处理好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
static int[][] dp;
static {
int n = 300, x = 5;
int mod = (int) 1e9 + 7;
// 01背包, 从1^x, 2^x,...,n^x里选物品,恰好能装满容量为n的背包的方案数
dp = new int[x][n + 1];
// 没有物品,恰好装满容量为0的背包的方案数:1,什么都不装
for (int i = 0; i < x; ++i)
dp[i][0] = 1;
for (int k = 1; k <= x; ++k) {
for (int i = 1; i <= n; ++i) {
int s = (int) Math.pow(i, k);
for (int j = n; j >= s; --j) {
dp[k - 1][j] = (dp[k - 1][j] + dp[k - 1][j - s]) % mod;
}
}
}
}
public int numberOfWays(int n, int x) {
return dp[x - 1][n];
}
}

879. 盈利计划

方法一:记忆化搜索(爆内存)

方案数用 +!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
this.group = group;
this.profit = profit;
this.n = n;
this.minProfit = minProfit;
dp = new int[group.length][n + 1][10000];
for (int[][] a : dp)
for (int[] b : a)
Arrays.fill(b, -1);
return dfs(0, n, 0);
}

public int dfs(int index, int hc, int p) {
if (index == group.length)
return p >= minProfit ? 1 : 0;
if (dp[index][hc][p] != -1)
return dp[index][hc][p];
int x = group[index], y = profit[index];
if (hc >= x)
return dp[index][hc][p] = (dfs(index + 1, hc, p) + dfs(index + 1, hc - x, p + y)) % mod;
return dp[index][hc][p] = dfs(index + 1, hc, p) % mod;
}

int[] group, profit;
int n, minProfit, mod = (int) 1e9 + 7;
int[][][] dp;

}

方法二:10000改成sum

勉勉强强过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
this.group = group;
this.profit = profit;
this.n = n;
this.minProfit = minProfit;
int sum = 0;
for (int x : profit)
sum += x;
dp = new int[group.length][n + 1][sum + 1];
for (int[][] a : dp)
for (int[] b : a)
Arrays.fill(b, -1);
return dfs(0, n, 0);
}

public int dfs(int index, int hc, int p) {
if (index == group.length)
return p >= minProfit ? 1 : 0;
if (dp[index][hc][p] != -1)
return dp[index][hc][p];
int x = group[index], y = profit[index];
if (hc >= x)
return dp[index][hc][p] = (dfs(index + 1, hc, p) + dfs(index + 1, hc - x, p + y)) % mod;
return dp[index][hc][p] = dfs(index + 1, hc, p) % mod;
}

int[] group, profit;
int n, minProfit, mod = (int) 1e9 + 7;
int[][][] dp;

}

方法三:01背包

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
int sum = 0, mod = (int) 1e9 + 7;
for (int x : profit)
sum += x;
// 从group下标0~i中选任务,当前的最大可用人数是j,获得的利润至少为k的方案数
int[][][] dp = new int[group.length + 1][n + 1][minProfit + 1];
// 没有任务,没有人,可获得的利润大于等于0的方案数是1:什么都不选
for (int i = 0; i <= n; ++i)
dp[0][i][0] = 1;
for (int i = 1; i <= group.length; ++i) {
int x = group[i - 1], y = profit[i - 1];
for (int j = 0; j <= n; ++j) {
for (int k = 0; k <= minProfit; ++k) {
dp[i][j][k] = dp[i - 1][j][k];
if (j >= x)
dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j - x][Math.max(0, k - y)]) % mod;
}
}
}
return dp[group.length][n][minProfit];
}
}

279. 完全平方数

方法一:记忆化搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {

static int[] nums = new int[10000];
static {
for (int i = 1; i < nums.length; ++i)
nums[i] = i * i;
}

public int numSquares(int n) {
this.n = n;
m = (int) Math.sqrt(n);
for (int i = 1; i <= n; ++i)
nums[i] = i * i;
dp = new int[m + 1][n + 1];
for (int[] arr : dp)
Arrays.fill(arr, -1);
return dfs(1, n);
}

int n, m, inf = Integer.MAX_VALUE / 2;
int[][] dp;

public int dfs(int index, int sum) {
if (index == m + 1)
return sum == 0 ? 0 : inf;
if (dp[index][sum] != -1)
return dp[index][sum];
int x = nums[index];
if (sum >= x)
return dp[index][sum] = Math.min(dfs(index, sum - x) + 1, dfs(index + 1, sum));
return dp[index][sum] = dfs(index + 1, sum);
}

}

方法二:完全背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {

public int numSquares(int n) {
int m = (int) Math.sqrt(n);
int[] dp = new int[n + 1];
// dp[0] = 0 没有物品,容量为0,需要0个数
Arrays.fill(dp, 100010);
dp[0] = 0;
for (int i = 1; i <= m; ++i) {
int x = i * i;
for (int j = x; j <= n; ++j) {
dp[j] = Math.min(dp[j], dp[j - x] + 1);
}
}
return dp[n];
}

}

Package Problem
https://leopol1d.github.io/2023/06/26/package-problem/
作者
Leopold
发布于
2023年6月26日
许可协议