【恋上数据结构与算法二】(八)动态规划(Dynamic Prog
动态规划(Dynamic Programming)
◼ 动态规划,简称DP
是求解最优化问题的一种常用策略
◼ 通常的使用套路(一步一步优化)
1 .暴力递归(自顶向下,出现了重叠子问题)
2.记忆化搜索(自顶向下)
3.递推(自底向上)
动态规划的常规步骤
◼ 动态规划中的“动态”可以理解为是“会变化的状态”
1.定义状态(状态是原问题、子问题的解)
✓比如定义 dp(i) 的含义
2.设置初始状态(边界)
✓比如设置 dp(0) 的值
3.确定状态转移方程
✓比如确定 dp(i) 和 dp(i – 1) 的关系
动态规划的一些相关概念
◼ 来自维基百科的解释
Dynamic Programming is a method for solving a complex problem by breaking it down into a collectionof simpler subproblems, solving each of those subproblems just once, and storing their solutions.
1.将复杂的原问题拆解成若干个简单的子问题
2.每个子问题仅仅解决1次,并保存它们的解
3.最后推导出原问题的解
◼可以用动态规划来解决的问题,通常具备2个特点
1.最优子结构(最优化原理):通过求解子问题的最优解,可以获得原问题的最优解
2.无后效性
✓某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响(未来与过去无关)
✓在推导后面阶段的状态时,只关心前面阶段的具体状态值,不关心这个状态是怎么一步步推导出来的
无后效性
◼从起点(0, 0)走到终点(4, 4)一共有多少种走法?只能向右、向下走
◼假设 dp(i, j) 是从(0, 0)走到(i, j)的走法
dp(i, 0) = dp(0, j) = 1
dp(i, j) = dp(i, j – 1) + dp(i – 1, j)
◼ 无后效性
推导 dp(i, j) 时只需要用到 dp(i, j – 1)、dp(i – 1, j) 的值
不需要关心 dp(i, j – 1)、dp(i – 1, j) 的值是怎么求出来的
有后效性
◼如果可以向左、向右、向上、向下走,并且同一个格子不能走 2 次
◼ 有后效性
dp(i, j) 下一步要怎么走,还要关心上一步是怎么来的
✓也就是还要关心 dp(i, j – 1)、dp(i – 1, j) 是怎么来的?
练习1 – 找零钱
◼ 假设有25分、20分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?
此前用贪心策略得到的并非是最优解(贪心得到的解是 5 枚硬币)
◼假设 dp(n) 是凑到 n 分需要的最少硬币个数
如果第 1 次选择了 25 分的硬币,那么 dp(n) = dp(n – 25) + 1
如果第 1 次选择了 20 分的硬币,那么 dp(n) = dp(n – 20) + 1
如果第 1 次选择了 5 分的硬币,那么 dp(n) = dp(n – 5) + 1
如果第 1 次选择了 1 分的硬币,那么 dp(n) = dp(n – 1) + 1
所以 dp(n) = min { dp(n – 25), dp(n – 20), dp(n – 5), dp(n – 1) } + 1
找零钱 – 暴力递归
/**
* 暴力递归(自顶向下的调用,出现了重叠子问题)
*/
static int coins1(int n) {
if (n < 1) return Integer.MAX_VALUE;
if (n == 25 || n == 20 || n == 5 || n == 1) return 1;
int min1 = Math.min(coins1(n - 25), coins1(n - 20));
int min2 = Math.min(coins1(n - 5), coins1(n - 1));
return Math.min(min1, min2) + 1;
}
◼ 类似于斐波那契数列的递归版,会有大量的重复计算,时间复杂度较高
找零钱 – 记忆化搜索
/**
* 记忆化搜索(自顶向下的调用)
*/
static int coins2(int n) {
if (n < 1) return -1;
int[] dp = new int[n + 1];
int[] faces = {1, 5, 20, 25};
for (int face : faces) {
if (n < face) break;
dp[face] = 1;
}
return coins2(n, dp);
}
static int coins2(int n, int[] dp) {
if (n < 1) return Integer.MAX_VALUE;
if (dp[n] == 0) {
int min1 = Math.min(coins2(n - 25, dp), coins2(n - 20, dp));
int min2 = Math.min(coins2(n - 5, dp), coins2(n - 1, dp));
dp[n] = Math.min(min1, min2) + 1;
}
return dp[n];
}
找零钱 – 递推
/**
* 递推(自底向上)
*/
static int coins3(int n) {
if (n < 1) return -1;
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
int min = dp[i - 1];
if (i >= 5) min = Math.min(dp[i - 5], min);
if (i >= 20) min = Math.min(dp[i - 20], min);
if (i >= 25) min = Math.min(dp[i - 25], min);
dp[i] = min + 1;
}
return dp[n];
}
◼ 时间复杂度、空间复杂度:O(n)
思考题:请输出找零钱的具体方案(具体是用了哪些面值的硬币)
static int coins4(int n) {
if (n < 1) return -1;
int[] dp = new int[n + 1];
// faces[i]是凑够i分时最后那枚硬币的面值
int[] faces = new int[dp.length];
for (int i = 1; i <= n; i++) {
int min = dp[i - 1];
faces[i] = 1;
if (i >= 5 && dp[i - 5] < min) {
min = dp[i - 5];
faces[i] = 5;
}
if (i >= 20 && dp[i - 20] < min) {
min = dp[i - 20];
faces[i] = 20;
}
if (i >= 25 && dp[i - 25] < min) {
min = dp[i - 25];
faces[i] = 25;
}
dp[i] = min + 1;
// print(faces, i);// 输出找零钱的具体方案
}
print(faces, n);// 输出找零钱的具体方案
return dp[n];
}
// // 输出找零钱的具体方案
static void print(int[] faces, int i) {
System.out.print("[" + i + "] = ");
while (n > 0) {
System.out.print(faces[n] + " ");
i -= faces[I];
}
System.out.println();
}
找零钱 – 通用实现
static int coins5(int n, int[] faces) {
if (n < 1 || faces == null || faces.length == 0) return -1;
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
int min = Integer.MAX_VALUE;
for (int face : faces) {
if (i < face) continue;
int v = dp[i - face];
if (v < 0 || v >= min) continue;
min = v;
}
if (min == Integer.MAX_VALUE) {
dp[i] = -1;
} else {
dp[i] = min + 1;
}
}
return dp[n];
}
练习2 – 最大连续子序列和
◼给定一个长度为 n 的整数序列,求它的最大连续子序列和
比如 –2、1、–3、4、–1、2、1、–5、4 的最大连续子序列和是 4 + (–1) + 2 + 1 = 6
◼ 状态定义
假设 dp(i) 是以 nums[i] 结尾的最大连续子序列和(nums是整个序列)
✓以 nums[0] –2 结尾的最大连续子序列是 –2,所以 dp(0) = –2
✓以 nums[1] 1 结尾的最大连续子序列是 1,所以 dp(1) = 1
✓以 nums[2] –3 结尾的最大连续子序列是 1、–3,所以 dp(2) = dp(1) + (–3) = –2
✓以 nums[3] 4 结尾的最大连续子序列是 4,所以 dp(3) = 4
✓以 nums[4] –1 结尾的最大连续子序列是 4、–1,所以 dp(4) = dp(3) + (–1) = 3
✓以 nums[5] 2 结尾的最大连续子序列是 4、–1、2,所以 dp(5) = dp(4) + 2 = 5
✓以 nums[6] 1 结尾的最大连续子序列是 4、–1、2、1,所以 dp(6) = dp(5) + 1 = 6
✓以 nums[7] –5 结尾的最大连续子序列是 4、–1、2、1、–5,所以 dp(7) = dp(6) + (–5) = 1
✓以 nums[8] 4 结尾的最大连续子序列是 4、–1、2、1、–5、4,所以 dp(8) = dp(7) + 4 = 5
最大连续子序列和 – 状态转移方程和初始状态
◼ 状态转移方程
如果 dp(i – 1) ≤ 0,那么 dp(i) = nums[i]
如果 dp(i – 1) > 0,那么 dp(i) = dp(i – 1) + nums[i]
◼ 初始状态
dp(0) 的值是 nums[0]
◼ 最终的解
最大连续子序列和是所有 dp(i) 中的最大值 max { dp(i) },i ∈ [0, nums.length)
最大连续子序列和 – 动态规划 – 实现
static int maxSubArray1(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = dp[0];
for (int i = 1; i < dp.length; i++) {
int prev = dp[i - 1];
if (prev <= 0) {
dp[i] = nums[i];
} else {
dp[i] = prev + nums[i];
}
max = Math.max(dp[i], max);
}
return max;
}
◼ 空间复杂度:O(n),时间复杂度:O(n)
最大连续子序列和 – 动态规划 – 优化实现
static int maxSubArray2(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int dp = nums[0];
int max = dp;
for (int i = 1; i < nums.length; i++) {
if (dp <= 0) {
dp = nums[i];
} else {
dp = dp + nums[i];
}
max = Math.max(dp, max);
}
return max;
}
◼ 空间复杂度:O(1),时间复杂度:O(n)
练习3 – 最长上升子序列(LIS)
◼最长上升子序列(最长递增子序列,Longest Increasing Subsequence,LIS)
◼ 给定一个无序的整数序列,求出它最长上升子序列的长度(要求严格上升)(不要求连续)
比如 [10, 2, 2, 5, 1, 7, 101, 18] 的最长上升子序列是 [2, 5, 7, 101]、[2, 5, 7, 18],长度是 4
最长上升子序列 – 动态规划 – 状态定义
◼假设数组是 nums, [10, 2, 2, 5, 1, 7, 101, 18]
dp(i) 是以 nums[i] 结尾的最长上升子序列的长度,i ∈ [0, nums.length)
✓以 nums[0] 10 结尾的最长上升子序列是 10,所以 dp(0) = 1
✓以 nums[1] 2 结尾的最长上升子序列是 2,所以 dp(1) = 1
✓以 nums[2] 2 结尾的最长上升子序列是 2,所以 dp(2) = 1
✓以 nums[3] 5 结尾的最长上升子序列是 2、5,所以 dp(3) = dp(1) + 1 = dp(2) + 1 = 2
✓以 nums[4] 1 结尾的最长上升子序列是 1,所以 dp(4) = 1
✓以 nums[5] 7 结尾的最长上升子序列是 2、5、7,所以 dp(5) = dp(3) + 1 = 3
✓以 nums[6] 101 结尾的最长上升子序列是 2、5、7、101,所以 dp(6) = dp(5) + 1 = 4
✓以 nums[7] 18 结尾的最长上升子序列是 2、5、7、18,所以 dp(7) = dp(5) + 1 = 4
◼最长上升子序列的长度是所有 dp(i) 中的最大值 max { dp(i) },i ∈ [0, nums.length)
最长上升子序列 – 动态规划 – 状态转移方程
◼遍历 j ∈ [0, i)
当 nums[i] > nums[j]
✓nums[i] 可以接在 nums[j] 后面,形成一个比 dp(j) 更长的上升子序列,长度为 dp(j) + 1
✓dp(i) = max { dp(i), dp(j) + 1 }
当 nums[i] ≤ nums[j]
✓nums[i] 不能接在 nums[j] 后面,跳过此次遍历(continue)
◼ 状态的初始值
dp(0) = 1
所有的 dp(i) 默认都初始化为 1
最长上升子序列 – 动态规划 – 实现
/**
* 动态规划
*/
static int lengthOfLIS1(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
int max = dp[0] = 1;
for (int i = 1; i < dp.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[i] <= nums[j]) continue;
dp[i] = Math.max(dp[i], dp[j] + 1);
}
max = Math.max(dp[i], max);
}
return max;
}
◼空间复杂度:O(n),时间复杂度:O(n2)
最长上升子序列 – 二分搜索 – 思路
◼ 把每个数字看做是一张扑克牌,从左到右按顺序处理每一个扑克牌
将它压在(从左边数过来)第一个牌顶 ≥ 它的牌堆上面
如果找不到牌顶 ≥ 它的牌堆,就在最右边新建一个牌堆,将它放入这个新牌堆中
◼ 当处理完所有牌,最终牌堆的数量就是最长上升子序列的长度
最长上升子序列 – 二分搜索 – 思路
◼思路(假设数组是 nums,也就是最初的牌数组)
top[i] 是第 i 个牌堆的牌顶,len 是牌堆的数量,初始值为 0 遍历每一张牌 num
✓利用二分搜索找出 num 最终要放入的牌堆位置 index
✓num 作为第 index 个牌堆的牌顶,top[index] = num
✓如果 index 等于 len,相当于新建一个牌堆,牌堆数量 +1,也就是 len++
最长上升子序列 – 二分搜索 – 实现
/**
* 牌顶
*/
static int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
// 牌堆的数量
int len = 0;
// 牌顶数组
int[] top = new int[nums.length];
// 遍历所有的牌
for (int num : nums) {
int begin = 0;
int end = len;
while (begin < end) {
int mid = (begin + end) >> 1;
if (num <= top[mid]) {
end = mid;
} else {
begin = mid + 1;
}
}
// 覆盖牌顶
top[begin] = num;
// 检查是否要新建一个牌堆
if (begin == len) len++;
}
return len;
}
◼空间复杂度:O(n)
◼时间复杂度:O(nlogn)
练习4 – 最长公共子序列(LCS)
◼最长公共子序列(Longest Common Subsequence,LCS)
◼ 求两个序列的最长公共子序列长度
[1, 3, 5, 9, 10] 和 [1, 4, 9, 10] 的最长公共子序列是 [1, 9, 10],长度为 3
ABCBDAB 和 BDCABA 的最长公共子序列长度是 4,可能是
✓ABCBDAB 和 BDCABA > BDAB
✓ABCBDAB 和 BDCABA > BDAB
✓ABCBDAB 和 BDCABA > BCAB
✓ABCBDAB 和 BDCABA > BCBA
最长公共子序列 – 思路
◼假设 2 个序列分别是 nums1、nums2
i ∈ [1, nums1.length]
j ∈ [1, nums2.length]
◼假设 dp(i, j) 是【nums1 前 i 个元素】与【nums2 前 j 个元素】的最长公共子序列长度
dp(i, 0)、dp(0, j) 初始值均为 0
如果 nums1[i – 1] = nums2[j – 1],那么 dp(i, j) = dp(i – 1, j – 1) + 1
如果 nums1[i – 1] ≠ nums2[j – 1],那么 dp(i, j) = max { dp(i – 1, j), dp(i, j – 1) }
最长公共子序列 – 递归实现
static int lcs1(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) return 0;
if (nums2 == null || nums2.length == 0) return 0;
return lcs1(nums1, nums1.length, nums2, nums2.length);
}
/**
* 求nums1前i个元素和nums2前j个元素的最长公共子序列长度
* @param nums1
* @param I
* @param nums2
* @param j
*/
static int lcs1(int[] nums1, int i, int[] nums2, int j) {
if (i == 0 || j == 0) return 0;
if (nums1[i - 1] == nums2[j - 1]) {
return lcs1(nums1, i - 1, nums2, j - 1) + 1;
}
return Math.max(lcs1(nums1, i - 1, nums2, j),
lcs1(nums1, i, nums2, j - 1));
}
◼ 空间复杂度:O(k) , k = min{n, m},n、m 是 2 个序列的长度
◼时间复杂度:O(2n) ,当n=m时
最长公共子序列 – 递归实现分析
◼ 出现了重复的递归调用
最长公共子序列 – 非递归实现
static int lcs2(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) return 0;
if (nums2 == null || nums2.length == 0) return 0;
int[][] dp = new int[nums1.length + 1][nums2.length + 1];// 二维数组
for (int i = 1; i <= nums1.length; i++) {
for (int j = 1; j <= nums2.length; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[nums1.length][nums2.length];
}
◼空间复杂度:O(n∗m)
◼时间复杂度:O(n ∗ m)
◼dp 数组的计算结果如下所示
最长公共子序列 – 非递归实现 – 滚动数组
◼ 可以使用滚动数组优化空间复杂度
static int lcs3(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) return 0;
if (nums2 == null || nums2.length == 0) return 0;
int[][] dp = new int[2][nums2.length + 1];
for (int i = 1; i <= nums1.length; i++) {
int row = i & 1;
int prevRow = (i - 1) & 1;
for (int j = 1; j <= nums2.length; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[row][j] = dp[prevRow][j - 1] + 1;
} else {
dp[row][j] = Math.max(dp[prevRow][j], dp[row][j - 1]);
}
}
}
return dp[nums1.length & 1][nums2.length];
}
最长公共子序列 – 非递归实现 – 一维数组
◼可以将 二维数组 优化成 一维数组,进一步降低空间复杂度
static int lcs4(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) return 0;
if (nums2 == null || nums2.length == 0) return 0;
int[] dp = new int[nums2.length + 1];
for (int i = 1; i <= nums1.length; i++) {
int cur = 0;
for (int j = 1; j <= nums2.length; j++) {
int leftTop = cur;
cur = dp[j];
if (nums1[i - 1] == nums2[j - 1]) {
dp[j] = leftTop + 1;
} else {
dp[j] = Math.max(dp[j], dp[j - 1]);
}
}
}
return dp[nums2.length];
}
◼可以空间复杂度优化至O(k) ,k=min{n,m}
用length较小的当dp
static int lcs5(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) return 0;
if (nums2 == null || nums2.length == 0) return 0;
int[] rowsNums = nums1, colsNums = nums2;
// 用较小的做列数
if (nums1.length < nums2.length) {
colsNums = nums1;// 列
rowsNums = nums2;// 行
}
int[] dp = new int[colsNums.length + 1];// 用length较小的当dp
for (int i = 1; i <= rowsNums.length; i++) {
int cur = 0;
for (int j = 1; j <= colsNums.length; j++) {
int leftTop = cur;
cur = dp[j];
if (rowsNums[i - 1] == colsNums[j - 1]) {
dp[j] = leftTop + 1;
} else {
dp[j] = Math.max(dp[j], dp[j - 1]);
}
}
}
return dp[colsNums.length];
}
练习5 – 最长公共子串
◼ 最长公共子串(Longest Common Substring)
子串是连续的子序列
◼ 求两个字符串的最长公共子串长度
ABCBA 和 BABCA 的最长公共子串是 ABC,长度为 3
最长公共子串 – 思路
◼假设 2 个字符串分别是 str1、str2
i ∈ [1, str1.length]
j ∈ [1, str2.length]
◼假设 dp(i, j) 是以 str1[i – 1]、str2[j – 1] 结尾的最长公共子串长度
dp(i, 0)、dp(0, j) 初始值均为 0
如果 str1[i – 1] = str2[j – 1],那么 dp(i, j) = dp(i – 1, j – 1) + 1
如果 str1[i – 1] ≠ str2[j – 1],那么 dp(i, j) = 0
◼最长公共子串的长度是所有 dp(i, j) 中的最大值 max { dp(i, j)}
最长公共子串 – 实现
static int lcs1(String str1, String str2) {
if (str1 == null || str2 == null) return 0;
char[] chars1 = str1.toCharArray();
if (chars1.length == 0) return 0;
char[] chars2 = str2.toCharArray();
if (chars2.length == 0) return 0;
int[][] dp = new int[chars1.length + 1][chars2.length + 1];
int max = 0;
for (int i = 1; i <= chars1.length; i++) {
for (int j = 1; j <= chars2.length; j++) {
if (chars1[i - 1] != chars2[j - 1]) continue;
dp[i][j] = dp[i - 1][j - 1] + 1;
max = Math.max(dp[i][j], max);
}
}
return max;
}
◼空间复杂度:O(n∗m)
◼ 时间复杂度:O(n∗m)
◼dp 数组的计算结果如下所示
最长公共子串 – 一维数组实现
static int lcs2(String str1, String str2) {
if (str1 == null || str2 == null) return 0;
char[] chars1 = str1.toCharArray();
if (chars1.length == 0) return 0;
char[] chars2 = str2.toCharArray();
if (chars2.length == 0) return 0;
char[] rowsChars = chars1, colsChars = chars2;
if (chars1.length < chars2.length) {
colsChars = chars1;
rowsChars = chars2;
}
int[] dp = new int[colsChars.length + 1];
int max = 0;
for (int row = 1; row <= rowsChars.length; row++) {
int cur = 0;
for (int col = 1; col <= colsChars.length; col++) {
int leftTop = cur;
cur = dp[col];
if (chars1[row - 1] != chars2[col - 1]) {
dp[col] = 0;
} else {
dp[col] = leftTop + 1;
max = Math.max(dp[col], max);
}
}
}
return max;
}
◼空间复杂度:O(k), k=min{n,m}
◼ 时间复杂度:O(n∗m)
练习6 – 0-1背包
◼有 n 件物品和一个最大承重为 W 的背包,每件物品的重量是 𝑤i、价值是 𝑣i
在保证总重量不超过 W 的前提下,选择某些物品装入背包,背包的最大总价值是多少?
注意:每个物品只有 1 件,也就是每个物品只能选择 0 件或者 1 件
◼假设 values 是价值数组,weights 是重量数组
编号为 k 的物品,价值是 values[k],重量是 weights[k],k ∈ [0, n)
◼假设 dp(i, j) 是 最大承重为 j、有前 i 件物品可选 时的最大总价值,i ∈ [1, n],j ∈ [1, W]
dp(i, 0)、dp(0, j) 初始值均为 0
如果 j < weights[i – 1],那么 dp(i, j) = dp(i – 1, j)
如果 j ≥ weights[i – 1],那么 dp(i, j) = max { dp(i – 1, j), dp(i – 1, j – weights[i – 1]) + values[i – 1] }
0-1背包 – 实现
static int maxValue1(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[][] dp = new int[values.length + 1][capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = 1; j <= capacity; j++) {
if (j < weights[i - 1]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(
dp[i - 1][j],
values[i - 1] + dp[i - 1][j - weights[i - 1]]);
}
}
}
return dp[values.length][capacity];
}
0-1背包 – 非递归实现
◼dp 数组的计算结果如下所示
0-1背包 – 非递归实现 – 一维数组
◼dp(i, j) 都是由 dp(i – 1, k) 推导出来的,也就是说,第 i 行的数据是由它的上一行第 i – 1 行推导出来的
因此,可以使用一维数组来优化
另外,由于 k ≤ j ,所以 j 的遍历应该由大到小,否则导致数据错乱
static int maxValue2(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >= 1; j--) {
if (j < weights[i - 1]) continue;
dp[j] = Math.max(dp[j],
values[i - 1] + dp[j - weights[i - 1]]);
}
}
return dp[capacity];
}
0-1背包 – 非递归实现 – 一维数组优化
◼观察二维数组表,得出结论:j 的下界可以从 1 改为 weights[i – 1]
static int maxValue3(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >= weights[i - 1]; j--) {// j 的下界从 1 改为 weights[i – 1]
dp[j] = Math.max(dp[j],
values[i - 1] + dp[j - weights[i - 1]]);
}
}
return dp[capacity];
}
0-1背包 – 恰好装满
◼有 n 件物品和一个最大承重为 W 的背包,每件物品的重量是 𝑤i、价值是 𝑣i
在保证总重量恰好等于 W 的前提下,选择某些物品装入背包,背包的最大总价值是多少?
注意:每个物品只有 1 件,也就是每个物品只能选择 0 件或者 1 件
◼dp(i, j) 初始状态调整
dp(i, 0) = 0,总重量恰好为 0,最大总价值必然也为 0
dp(0, j) = –∞(负无穷),j ≥ 1,负数在这里代表无法恰好装满
0-1背包 – 恰好装满 – 实现
static int maxValueExactly(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
for (int j = 1; j <= capacity; j++) {
dp[j] = Integer.MIN_VALUE;
}
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >= weights[i - 1]; j--) {
dp[j] = Math.max(dp[j], values[i - 1] + dp[j - weights[i - 1]]);
}
}
return dp[capacity] < 0 ? -1 : dp[capacity];
}