背包问题是什么
给你一个可装载重量为W
的背包和N个物品
,每个物品有重量
和价值
两个属性。其中第i个物品
的重量为wt[i]
,价值为val[i]
,现在让你用这个背包装物品,最多能装的价值
是多少?
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
算法返回 6,选择前两件物品装进背包,总重量 3 小于W,可以获得最大价值 6。
1、明确状态
只要给定几个可选物品和一个背包的容量限制,就形成了一个背包问题,对不对?所以状态有两个,就是「背包的容量」和「可选择的物品」。
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
2、明确dp数组的定义
dp[i][w]
的定义如下:对于前i个物品
,当前背包的容量为w
,这种情况下可以装的最大价值是dp[i][w]
比如说,如果 dp[3][5] = 6
,其含义为:对于给定的一系列物品中,若只对前 3 个物品
进行选择,当背包容量为 5
时,最多可以装下的价值为 6
。
3、明确base case
base case 就是dp[0][..] = dp[..][0] = 0
,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。
4、明确状态转移方程
dp[i][w]
表示:对于前i个物品,当前背包的容量为w时,这种情况下可以装下的最大价值是dp[i][w]。
1、如果你没有把这第i个物品
装入背包,那么很显然,最大价值dp[i][w]
应该等于dp[i-1][w]
。你不装嘛,那就继承之前的结果。
2、如果你把这第i个物品
装入了背包,那么dp[i][w]
应该等于dp[i-1][w-wt[i-1]] + val[i-1]
。
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
//对于标准0/1背包问题解法
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
// vector 全填入 0,base case 已初始化
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
return dp[N][W];
}
原题链接
给定一个只包含正整数
的非空数组。是否可以将这个数组分割成两个子集,使得两个子集
的元素和相等
。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
那么对于本文的问题,我们可以先对集合求和,得出sum,把问题转化为背包问题:
给一个可装载重量为sum/2
的背包和N个物品
,每个物品的重量为nums[i]
。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
1、明确状态
状态就是「背包的容量」和「可选择的物品」
2、明确dp数组的定义
dp[i][j] = x
: 对于前i个物品
,当前背包的容量为j
时,若x为true
,则说明可以恰好将背包装满
,若x为false
,则说明不能恰好将背包装满
。
比如说,如果dp[4][9] = true
,其含义为:对于容量为 9
的背包,若只是用前 4 个物品
,可以有一种方法把背包恰好装满
。
根据这个定义,我们想求的最终答案就是dp[N][sum/2]
.
3、明确base case
base case
: dp[..][0] = true
和dp[0][..] = false
,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。
4、明确状态转移方程
可以根据「选择」对dp[i][j]
得到以下状态转移:
1、如果不把nums[i]
算入子集,或者说你不把这第i个物品
装入背包,那么是否能够恰好装满背包,取决于上一个状态dp[i-1][j]
,继承之前的结果。
2、如果把nums[i]
算入子集,或者说你把这第i个物品
装入了背包,那么是否能够恰好装满背包,取决于状态dp[i - 1][j-nums[i-1]]
。
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= sum; j++) {
if (j - nums[i - 1] < 0) {
// 背包容量不足,不能装入第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
// 装入或不装入背包
dp[i][j] = dp[i - 1][j] | dp[i - 1][j-nums[i-1]];
}
}
}
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int num : nums) sum += num;
// 和为奇数时,不可能划分成两个和相等的集合
if (sum % 2 != 0) return false;
int n = nums.size();
sum = sum / 2;
vector<vector<bool>>
dp(n + 1, vector<bool>(sum + 1, false));
// base case
for (int i = 0; i <= n; i++)
dp[i][0] = true;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= sum; j++) {
if (j - nums[i - 1] < 0) {
// 背包容量不足,不能装入第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
// 装入或不装入背包
dp[i][j] = dp[i - 1][j] | dp[i - 1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
};
进行状态压缩
注意到dp[i][j]
都是通过上一行dp[i-1][..]
转移过来的,之前的数据都不会再使用了。
bool canPartition(vector<int>& nums) {
int sum = 0, n = nums.size();
for (int num : nums) sum += num;
if (sum % 2 != 0) return false;
sum = sum / 2;
vector<bool> dp(sum + 1, false);
// base case
dp[0] = true;
for (int i = 0; i < n; i++)
for (int j = sum; j >= 0; j--)
if (j - nums[i] >= 0)
dp[j] = dp[j] || dp[j - nums[i]];
return dp[sum];
}
原题链接
给定不同面额的硬币 coins
和一个总金额 amount
。编写一个函数来计算可以凑成总金额
所需的最少的硬币个数
。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
我们可以把这个问题转化为背包问题的描述形式:
有一个背包,最大容量为amount,有一系列物品coins,每个物品的重量为coins[i],每个物品的数量无限。刚好把背包装满请问所需的最少的物品个数?
1、明确状态
由于coins数量无限,coins的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 amount。
2、明确dp数组的定义
dp(n) 的定义:输入一个目标金额 n,返回凑出目标金额 n 的最少coins数量。
3、明确base case
显然目标金额 amount 为 0 时算法返回 0,dp[0] = 0
;
4、明确状态转移方程
int coinChange(vector<int>& coins, int amount) {
// 数组大小为 amount + 1,初始值也为 amount + 1
vector<int> dp(amount + 1, amount + 1);
// base case
dp[0] = 0;
// 外层 for 循环在遍历所有状态的所有取值
for (int i = 0; i < dp.size(); i++) {
// 内层 for 循环在求所有选择的最小值
for (int coin : coins) {
// 子问题无解,跳过
if (i - coin < 0) continue;
dp[i] = min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
原题链接
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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
注意:
你可以假设:
0 <= amount (总金额) <= 5000
1 <= coin (硬币面额) <= 5000
硬币种类不超过 500 种
结果符合 32 位符号整数
我们可以把这个问题转化为背包问题的描述形式:
有一个背包,最大容量为amount,有一系列物品coins,每个物品的重量为coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?
1、明确状态
状态就是「背包的容量」和「可选择的物品」
2、明确dp数组的定义
dp[i][j]
的定义如下:
前i个物品
,当背包容量为j时,有dp[i][j]
种方法可以装满背包。也就是若只使用coins中的前i个硬币
的面值,若想凑出金额j
,有dp[i][j]
种凑法。3、明确base case
dp[0][..] = 0, dp[..][0] = 1
,因为如果不使用任何硬币面值,就无法凑出任何金额;如果凑出的目标金额为 0,那么“无为而治”就是唯一的一种凑法。
4、明确状态转移方程
1、如果你不把这第i个物品
装入背包,也就是说你不使用coins[i]
这个面值的硬币,那么凑出面额j的方法数dp[i][j]
应该等于dp[i-1][j]
,继承之前的结果。
2、如果你把这第i个物品
装入了背包,也就是说你使用coins[i]
这个面值的硬币,那么dp[i][j]
应该等于dp[i][j-coins[i-1]]。
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++)
if (j - coins[i-1] >= 0)
dp[i][j] = dp[i - 1][j]
+ dp[i][j - coins[i-1]];
else
dp[i][j] = dp[i - 1][j];
}
class Solution {
public:
int change(int amount, vector<int>& coins) {
int n = coins.size();
vector<vector<int>>dp(n+1,vector<int>(amount+1,0));
// base case
for (int i = 0; i <= n; i++)
dp[i][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++)
if (j - coins[i-1] >= 0)
dp[i][j] = dp[i - 1][j]
+ dp[i][j - coins[i-1]];
else
dp[i][j] = dp[i - 1][j];
}
return dp[n][amount];
}
};
压缩状态
dp数组的转移只和dp[i][..]
和dp[i-1][..]
有关,所以可以压缩状态,进一步降低算法的空间复杂度:
class Solution {
public:
int change(int amount, vector<int>& coins) {
int n = coins.size();
vector<int>dp(amount+1,0);
// base case
dp[0] = 1;
for (int i = 0; i < n; i++)
for (int j = 1; j <= amount; j++)
if (j - coins[i] >= 0)
dp[j] = dp[j] + dp[j-coins[i]];
return dp[amount];
}
};
1、https://leetcode-cn.com/problems/partition-equal-subset-sum/