目录
1. 完全背包
1.1 【模板】完全背包
1.2 零钱兑换
1.3 零钱兑换 II
1.4 完全平方数
2. 二维费用的背包问题
2.1 一和零
2.2 盈利计划
--- 如果背包问题原先没有基础的,建议先看上一篇博客 --- 面试高频手撕算法 - 01背包系列
1. 完全背包
1.1 【模板】完全背包
【题目链接】
【模板】完全背包_牛客题霸_牛客网你有一个背包,最多能容纳的体积是V。 现在有n种物品,每种物品有任意多个,第。题目来自【牛客题霸】https://www.nowcoder.com/practice/237ae40ea1e84d8980c1d5666d1c53bc?tpId=230&&tqId=38965&sourceUrl=https%3A%2F%2Fwww.nowcoder.com%2Fexam%2Foj【题目描述】
你有一个背包,最多能容纳的体积是V。
现在有 n 种物品,每种物品有任意多个,第 i 种物品的体积为 vi,价值为 wi。
(1)求这个背包至多能装多大价值的物品?
(2)若背包恰好装满,求至多能装多大价值的物品?
第一问:【算法原理】
再做动态规划系列问题的时候,无非就是这几大步骤:
① 状态定义
② 推导状态转移方程
③ 初始化 dp 表
④ 填表顺序
⑤
返回值
1. 状态定义
背包问题本质上还是一个线性 dp, 所以状态的定义根据线性 dp 的经验:
状态定义: dp[i] 表示从前 i 个物品中挑选, 所有选法, 能挑选出来的最大价值 (试错)
但是这样定义状态之后, 发现推不出来, 因为不知道体积, 所以需要定义一个二维的 dp.
状态定义:dp[i][j]表示从前 i 个物品中挑选,总体积不超过 j,所有选法中,能挑选出来的最大价值
2. 推导状态转移方程
根据最后一个位置的状况, 分情况讨论:
像这种,当我们要表示一个状态的时候,发现它需要很多个状态拼接而成的时候,这个时候我们需要想一个策略,将这些状态用一个或两个等有限个状态来表示。
【数学思想】
数学里面有种思想是错位相减,此处的做法有点类似,通过给 dp[i][j] 的列上减去一个体积,得到另一个表达式,然后寻找两个表达式之间的关联关系:
于是,我们的状态转移方程就转换成了:dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - v[i]] + w[i])
3. 初始化 dp 表
为了方便代码进行 dp 的一个过程, 我们都会根据情况给 dp 表多加一行, 一列.
由于使用到前面的列的时候, 会进行判断, 所以初始化的时候, 可以不用初始化列, 只需要初始化行.
当只有 0 个商品的时候,想要体积不超过 0,1,2,3..... 最大价值肯定都是 0 了.
4. 填表顺序
从状态转移方程来分析,dp[i] 会使用到上一行,以及当前行前面的列, 所以填表顺序从上往下,从左往右填.
5. 返回值
从状态表示:从前 i 个物品中挑选,总体积不超过 j 的最大价值, 再结合题目要求,
得出最终的状态:从前 n 个物品中挑选,总体积不超过 V 的最大价值,所以返回 dp[n][V] 即可.
第一问:【编写关键代码】
/**
* (1)求这个背包至多能装多大价值的物品?
* @param v 每个商品所对应的体积
* @param w 每个商品所对应的价值
* @param V 背包体积
* @param n 商品数量
* @return
*/
public int getMaxVlaue(int[] v, int[] w, int V, int n) {
int[][] dp = new int[n + 1][V + 1];
// 从前 i 个商品中挑选,总体积不超过 j,最大价值是多少
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= V; ++j) {
// 不挑选 i 商品
dp[i][j] = dp[i - 1][j];
// 挑选 i 商品, 数学思想简化状态转移方程
if(j - v[i] >= 0) {
dp[i][j] = Math.max(dp[i][j],dp[i][j - v[i]] + w[i]);
}
}
}
return dp[n][V];
}
此处先理解最基础的代码,等看完第二问, 再统一做空间优化.
第二问:【算法原理】
1. 状态定义
有了前面的经验,接下来就直接定义状态了.
状态定义:dp[i][j]表示从前 i 个物品中挑选,总体积正好为 j,所有选法中,能挑选出来的最大价值
2. 推导状态转移方程
此处的状态转移方程和第一问几乎一模一样, 只需要处理一些细节:
根据上面的数学推导,得出的状态转移方程:dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - v[i] + w[i]])
但是此处需要正好装满,所以 j - v[i] 就有可能不存在,所以需要像 01 背包那样,给第一行除第一个位置,全部设为 -1 或者 -0x3f3f3f3f,因为没有商品的时候,不可能凑出体积为 1,2,3 的情况.
4. 填表顺序
从上往下,从左往右
5.返回值
dp[n][V]
第二问:【编写关键代码】
/**
* (2)若背包恰好装满,求至多能装多大价值的物品?
* @param v 每个商品所对应的体积
* @param w 每个商品所对应的价值
* @param V 背包体积
* @param n 商品数量
* @return
*/
public static int getMaxVlaue(int[] v, int[] w, int V, int n) {
int[][] dp = new int[n + 1][V + 1];
// 状态定义:从前 i 个商品中挑选,体积恰好等于 j,最大价值为多少
// 体积不能正好等于 j 的,统统初始化为 -0x3f3f3f3f
for(int j = 1; j <= V; ++j) dp[0][j] = -0x3f3f3f3f;
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= V; ++j) {
// 不选 i
dp[i][j] = dp[i - 1][j];
// 选 i,-0x3f3f3f3f 不需要做条件判断,因为它足够小,取 max 不影响
if(j - v[i] >= 0) {
dp[i][j] = Math.max(dp[i][j],dp[i][j - v[i]] + w[i]);
}
}
}
return dp[n][V] < 0 ? 0 : dp[n][V];
}
【空间优化】
- 利用滚动数组做空间上的优化
- 直接在原始代码上稍加修改即可
利用滚动数组优化的时候,需要注意内层循环的填表顺序,由于 dp[i][j] 只会用到上一行的当前列,以及当前行前面的列,所以内层循环填表和 01 背包不一样,完全背包空间优化后,内层循环需要从左往右填表.
第一问:空间优化后的代码
/**
* (1)求这个背包至多能装多大价值的物品?
* @param v 每个商品所对应的体积
* @param w 每个商品所对应的价值
* @param V 背包体积
* @param n 商品数量
* @return
*/
public int getMaxVlaue(int[] v, int[] w, int V, int n) {
int[] dp = new int[V + 1];
// 从前 i 个商品中挑选,总体积不超过 j,最大价值是多少
for(int i = 1; i <= n; ++i) {
for(int j = v[i]; j <= V; ++j) {
dp[j] = Math.max(dp[j],dp[j - v[i]] + w[i]);
}
}
return dp[V];
}
第二问:空间优化后的代码
/**
* (2)若背包恰好装满,求至多能装多大价值的物品?
* @param v 每个商品所对应的体积
* @param w 每个商品所对应的价值
* @param V 背包体积
* @param n 商品数量
* @return
*/
public static int getMaxVlaue(int[] v, int[] w, int V, int n) {
int[] dp = new int[V + 1];
// 状态定义:从前 i 个商品中挑选,体积恰好等于 j,最大价值为多少
// 体积不能正好凑成 j 的,统统初始化为 -0x3f3f3f3f
for(int j = 1; j <= V; ++j) dp[j] = -0x3f3f3f3f;
for(int i = 1; i <= n; ++i) {
for(int j = v[i]; j <= V; ++j) {
dp[j] = Math.max(dp[j],dp[j - v[i]] + w[i]);
}
}
return dp[V] < 0 ? 0 : dp[V];
}
有了这两道完全背包的基础之后,后面的完全包相关的题目,可以先自己尝试去做,在看答案之前,自己能做出来,记忆还是非常深刻的.
1.2 零钱兑换
【题目链接】
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/coin-change/【题目描述】
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的最少的硬币个数 。
如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins =[1, 2, 5]
, amount =11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins =[2]
, amount =3
输出:-1
示例 3:
输入:coins = [1], amount = 0 输出:0
【算法原理】
这道题还是很容易能看出来是完全背包问题.
1. 状态定义
模仿完全背包的状态定义:dp[i][j] 表示:从前 i 个物品中挑选,总体积不超过 j,所有选法中,最大价值.
状态定义:dp[i][i] 表示从前 i 个硬币中挑选,总和正好等于 j,所有的选法中,最少的硬币个数
2. 推导状态转移方程
根据最后一个位置的状况, 分情况讨论:
有了上题的经验,当出现这种定义一个状态需要无线个状态来拼接的时候,这个时候我们要想办法把它变成一个或两个状态.
大家可以自己动手写写递推关系,找到 dp[i][j] 与 dp[i][j - coins[i]] 的关系,得到最终的状态转移方程:dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i] + 1])
3. 编写代码
/**
* 零钱兑换 - 完全背包
* @param coins 硬币数组
* @param amount 目标值
* @return
*/
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int INF = 0x3f3f3f3f;
int[][] dp = new int[n + 1][amount + 1];
// 状态定义:dp[i][i] 表示从前i个硬币中挑选,总和正好等于j,所有的选法中,最少硬币个数
// 不难正好凑成 j 元
for(int j = 1; j <= amount; ++j) dp[0][j] = INF;
for(int i = 1; i <= n; ++i) {
for(int j = 0; j <= amount; ++j) {
// 不选 i
dp[i][j] = dp[i - 1][j];
// 选 i, 具体选几个 i,用数学方法优化
if(j >= coins[i - 1]) {
dp[i][j] = Math.min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
}
}
}
// 可能不存在正好凑出的情况
return dp[n][amount] >= INF ? -1 : dp[n][amount];
}
4. 空间优化
- 删掉一维
- 内层循环从左往右
/**
* 零钱兑换 - 完全背包
* @param coins 硬币数组
* @param amount 目标值
* @return
*/
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int INF = 0x3f3f3f3f;
int[] dp = new int[amount + 1];
// 状态定义:dp[i][i] 表示从前i个硬币中挑选,总和正好等于j,所有的选法中,最少硬币个数
// 不能正好凑成 j 元
for(int j = 1; j <= amount; ++j) dp[j] = INF;
for(int i = 1; i <= n; ++i) {
for(int j = coins[i - 1]; j <= amount; ++j) {
dp[j] = Math.min(dp[j], dp[j - coins[i - 1]] + 1);
}
}
// 可能不存在正好凑出的情况
return dp[amount] >= INF ? -1 : dp[amount];
}
1.3 零钱兑换 II
【题目链接】
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/coin-change-ii/【题目描述】
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5] 输出:4 解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2] 输出:0 解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:
输入:amount = 10, coins = [10] 输出:1
【算法原理】
零钱兑换 II 其实比零钱兑换还简单,此处求的是凑成 j 元的方法数,只要不是求最大价值或最小价值中的正好凑成某某某,初始化工作就很简单.
1.定义状态
状态定义:dp[i][i] 表示从前 i 个硬币中挑选,总和正好等于 j,总共有多少种选法
2. 推导状态转移方程
有前面的基础了,数学思想直接得出状态转移方程: dp[i][j] = dp[i - 1][j] + dp[i][j- coins[i]]
此处需要注意的是,第二个状体千万不能在后面 + 1,因为求得是选法,以 i 结尾,是把最后一个位置拼在前面的选法中,所以本质上是同一种选法,求个数才需要 + 1,可以好好体会为什么是这样.
3. 编写代码
/**
* 零钱兑换 II - 完全背包
* @param coins 硬币数组
* @param amount 目标值
* @return
*/
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
// 状态定义:dp[i][i] 表示从前 i 个硬币中挑选,总和正好等于 j,总共有多少种选法
dp[0][0] = 1; // 初始化
for(int i = 1; i <= n; ++i) {
for(int j = 0; j <= amount; ++j) {
// 不选 i
dp[i][j] = dp[i - 1][j];
if(j >= coins[i - 1]) {
// 选 i, 数学思想优化状态转移方程
dp[i][j] += dp[i][j - coins[i - 1]];
}
}
}
return dp[n][amount];
}
4. 空间优化
- 删掉一维
- 内层循环从左往右
/**
* 零钱兑换 II - 完全背包
* @param coins 硬币数组
* @param amount 目标值
* @return
*/
public int change(int amount, int[] coins) {
int n = coins.length;
int[] dp = new int[amount + 1];
// 状态定义:dp[i][i] 表示从前 i 个硬币中挑选,总和正好等于 j,总共有多少种选法
dp[0] = 1; // 初始化
for(int i = 1; i <= n; ++i) {
for(int j = coins[i - 1]; j <= amount; ++j) {
dp[j] += dp[j - coins[i - 1]];
}
}
return dp[amount];
}
1.4 完全平方数
【题目链接】
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/perfect-squares/【题目描述】
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。
例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12 输出:3 解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13 输出:2 解释:13 = 4 + 9
【算法原理】
这道题如果想到的是贪心,那么就想错了,对于 13,贪心做法先找 9,再找 4,看似没问题,但是如果是 12,贪心做法先找 9, 再找三个 1,这样就需要 4 个完全平方数了,而三个 4 只需要 3 个完全平方数.
所以我们干脆就从第一个开始往后挑,那么就是这样理解:从前 i 个完全平方数中挑选几个数,正好凑成 j,所有的选法中,使用最少的完全平方数.
这正好就是完全背包问题!
1.状态定义
状态定义:从前 i 个完全平方数中挑选,总和正好等于 j,所有的选法中,最小的数量
2. 推到状态转移方程
使用数学思想得出最终的状态转移方程:dp[i][j] = Math.min(dp[i - 1][j],dp[i][j - i * i] + 1)
3. 编写代码
/**
* 完全平方数
* @param n 目标值
* @return
*/
public int numSquares(int n) {
int INF = 0x3f3f3f3f;
// i^2 <= n
int m = (int)Math.sqrt(n);
int[][] dp = new int[m + 1][n + 1];
// 状态定义:从前 i 个完全平方数中挑选,总和正好等于 j,所有的选法中,最小的数量
for(int j = 1; j <= n; ++j) dp[0][j] = INF; // 不能正好凑成 j
for(int i = 1; i <= m; ++i) {
for(int j = 1; j <= n; ++j) {
// 不选 i ^ 2
dp[i][j] = dp[i - 1][j];
if(j >= i * i) {
// 选 i ^ 2
dp[i][j] = Math.min(dp[i][j],dp[i][j - i * i] + 1);
}
}
}
return dp[m][n] >= INF ? 0 : dp[m][n];
}
此处二维 dp 表的横坐标,最多用到 1 ~ 根号 n,因为 13 只会用到 1,4,9,不可能用到 16,所以可以得出 i ^ 2 <= n。
4. 空间优化
/**
* 完全平方数
* @param n 目标值
* @return
*/
public int numSquares(int n) {
int INF = 0x3f3f3f3f;
int m = (int)Math.sqrt(n);
int[] dp = new int[n + 1];
// 状态定义:从前 i 个完全平方数中挑选,总和正好等于 j,所有的选法中,最小的数量
for(int j = 1; j <= n; ++j) dp[j] = INF; // 不能正好凑成 j
for(int i = 1; i <= m; ++i) {
for(int j = i * i; j <= n; ++j) {
dp[j] = Math.min(dp[j],dp[j - i * i] + 1);
}
}
return dp[n] >= INF ? 0 : dp[n];
}
2. 二维费用的背包问题
什么是二维费用的背包问题呢,简单来说就是有两个限定条件的背包问题,接下来结合题目来理解二维费用的背包问题.
2.1 一和零
【题目链接】
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/ones-and-zeroes/【题目描述】
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1 输出:2 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
题目意思:从前 i 个字符串中挑选,个数上满足 0 <= 5 && 1 <= n,所有选法中的最大子集.
【算法原理】
这题本质上就是二维费用的 01 背包,只不过比 01 背包多一维,做法大致相同.
1.状态定义
状态定义:dp[i][j][k] 表示从前 i 个字符串中挑选,字符 0 的个数不超过 j
字符 1 的个数不超过 k,所有选法中,最大的长度.
2. 推导状态转移方程
最终的状态转移方程: dp[i][j][k] = Math.max(dp[i - 1][j][k],dp[i - 1][j - a][k - b] + 1)
3. 初始化
当没有字符串的时候,最大子集长度当然就是 0 了,所以不需要初始化.
4. 编写代码
/**
* 一和零 - 二位费用的 01 背包
* @param strs
* @param m
* @param n
* @return
*/
public int findMaxForm(String[] strs, int m, int n) {
int L = strs.length;
int[][][] dp = new int[L + 1][m + 1][n + 1];
// 从数组中前 i 个字符串中挑选,0的个数不超过 m,
// 1的个数不超过 n 的所有子集中,长度最大的子集
for(int i = 1; i <= L; ++i) {
// 计算当前字符串 0,1 的个数
int a = 0, b = 0;
for(char ch : strs[i - 1].toCharArray()) {
if(ch == '0') a++;
else b++;
}
for(int j = 0; j <= m; ++j) {
for(int k = 0; k <= n; ++k) {
// 不选 i
dp[i][j][k] = dp[i -1][j][k];
// 选 i
if(j >= a && k >= b) {
dp[i][j][k] = Math.max(dp[i][j][k],
dp[i - 1][j - a][k - b] + 1);
}
}
}
}
return dp[L][m][n];
}
5. 空间优化
/**
* 一和零 - 二位费用的 01 背包
* @param strs
* @param m
* @param n
* @return
*/
public int findMaxForm(String[] strs, int m, int n) {
int L = strs.length;
int[][] dp = new int[m + 1][n + 1];
// 状态定义:从数组中前 i 个字符串中挑选, 0 的个数不超过 m,
// 1 的个数不超过 n 的所有子集中, 长度最大的子集
for(int i = 1; i <= L; ++i) {
// 计算当前字符串 0,1 的个数
int a = 0, b = 0;
for(char ch : strs[i - 1].toCharArray()) {
if(ch == '0') a++;
else b++;
}
for(int j = m; j >= a; --j) {
for(int k = n; k >= b; --k) {
dp[j][k] = Math.max(dp[j][k], dp[j - a][k - b] + 1);
}
}
}
return dp[m][n];
}
2.2 盈利计划
【题目链接】
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/profitable-schemes/【题目描述】
集团里有 n 名员工,他们可以完成各种各样的工作创造利润。
第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。
如果成员参与了其中一项工作,就不能参与另一项工作。
工作的任何至少产生 minProfit 利润的子集称为盈利计划 。并且工作的成员总数最多为 n 。
有多少种计划可以选择?因为答案很大,所以返回结果模 10^9 + 7 的值。
示例 1:
输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3] 输出:2 解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。 总的来说,有两种计划。
示例 2:
输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8] 输出:7 解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。 有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。
题目的意思如下:
【算法原理】
这道题与 "一和零" 唯一的区别就是:此处有一个限定条件是满足 >= 某个值,等会需要注意状态转移方程上的差异.
1. 状态定义
状态定义:dp[i][j][k] 表示从前 i 个计划中挑选,总人数不超过 j,
总利润至少为 k,一共有多少种选法
2. 推导状态转移方程
此处为什么 k - profit[i] 可以 <0 ? 因为题目要求的就是利润至少 >= x,如果 profit[i] 已经 > x,那不是更好,并且前面的挑选,我只需要保证利润 >= 0 就可以了.
所以对于选 i 的这种情况,状态转移方程是需要稍做处理的,因为像 dp[i][j][-3] 这种状态是没有意义的,所以使它利润为 0 即可,也就是 dp[i - 1][j - group[i]][max(0, k - profit(i))]
所以最终的状态转移方程 : dp[i][j][k] = dp[i - 1][j] + dp[i - 1][j - group[i]][max(0, k - profit(i))]
3. 编写代码
/**
* 盈利计划 - 二维费用的背包问题
* @param n 员工数量
* @param minProfit 至少产生 minProfit 利润
* @param group 员工数组
* @param profit 工作所对应的利润数组
* @return
*/
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
int mod = (int)1e9 + 7;
int L = group.length;
int m = minProfit;
int[][][] dp = new int[L + 1][n + 1][m + 1];
// 初始化: 任务0, 利润0, 人数 0 <= j <= n, 都有一种选法,所以初始化为 1
for(int j = 0; j <= n; ++j) dp[0][j][0] = 1;
// 状态:从前 i 个任务中挑选, 人数不超过 j, 利润至少为 k 的选法种数
for(int i = 1; i <= L; ++i) {
for(int j = 0; j <= n; ++j) {
for(int k = 0; k <= m; ++k) {
// 不选 i
dp[i][j][k] = dp[i - 1][j][k];
// 选 i, j - g[i] 必须要 >= 0, 而 k - p[i] 可以小于 0
if(j >= group[i - 1]) {
dp[i][j][k] += dp[i - 1][j - group[i - 1]][Math.max(0, k - profit[i - 1])];
}
// 每次让他模上 mod
dp[i][j][k] %= mod;
}
}
}
return dp[L][n][m];
}
4. 空间优化
/**
* 盈利计划 - 二维费用的背包问题
* @param n 员工数量
* @param minProfit 至少产生 minProfit 利润
* @param group 员工数组
* @param profit 工作所对应的利润数组
* @return
*/
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit){
int mod = (int)1e9 + 7;
int L = group.length;
int m = minProfit;
int[][] dp = new int[n + 1][m + 1];
// 初始化: 任务0, 利润0, 人数不超过 j,0 <= j <= n, 都有一种选法,所以初始化为 1
for(int j = 0; j <= n; ++j) dp[j][0] = 1;
// 状态:从前 i 个任务中挑选, 人数不超过 j, 利润至少为 k 的选法种数
for(int i = 1; i <= L; ++i) {
for(int j = n; j >= group[i - 1]; --j) {
for(int k = m; k >= 0; --k) {
dp[j][k] +=
dp[j - group[i - 1]][Math.max(0, k - profit[i - 1])];
dp[j][k] %= mod;
}
}
}
return dp[n][m];
}
【总结 看完背包问题的这两篇文章,之后遇到类似的题目,也能有感觉往背包方面去靠,另外就是要理解背包问题的核心:它是用来解决有限制条件下的 " 组合 " 问题!而有些问题看似也是从一堆物品中挑选,并要求满足某个条件,实则并不是背包问题的,例如这道题:组合总和 IV
像这种 1,2,1 和 1,1,2 算两种选法的,就不是背包问题,因为这种在数学上称之为排列,而背包问题解决的是组合问题.
背包系列讲解到这就结束了,希望能帮助大家.