目录
1--0-1背包问题
0-1 背包问题的特征:
一共有 n 个物品,但每个物品只能选择一次;
二维 dp 解法:
dp[i][j] 表示背包容量为 j ,可以在 0-i 种物品选取,其最大价值;
初始化:dp[0][j] = value[0](j >= weight[0]),dp[i][0] = 0;
状态转移方程:dp[i][j] == std::max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),其中 dp[i-1][j] 表示不选择物品 i,dp[i-1][j-weight[i]] + value[i] 表示选择物品 i;
遍历顺序:两个 for 循环来正序遍历物品 i 和背包容量 j(可以先遍历物品 i,再遍历背包容量 j;也可以先遍历背包容量 j,再遍历物品 i);
一维 dp 解法:
dp[j] 表示背包容量为 j 时,其最大价值;
初始化:dp[0] = 0;
状态转移方程:dp[j] = std::max(dp[j], dp[j - weight[i]] + value[i]),其中 dp[j - weight[i]] + value[i] 表示选取物品 i 时;
遍历顺序:两个 for 循环,第一个 for 循环正序遍历物品,第二个 for 循环倒叙(j 从大到小)遍历背包容量;(不能先遍历背包容量,再遍历物品);
2--分割等和子集
主要思路:
可以转换为 0-1 背包问题,将一半的和作为背包的容量,另一半的和作为物品的价值,同时每个物品的价值和重量相同;
计算背包的容量:target = sum / 2;
初始化:dp[0] = 0;
状态转移方程:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);j 表示背包的容量,dp[j]表示背包容量为 j 时,背包所装物品的最大价值;
遍历顺序:经典0-1背包一维遍历顺序,先正序遍历物品,再倒叙遍历背包容量;
当 dp[target] == target 时,表明恰好可以将数组划分为两个子集,一个子集的和作为背包容量,另一个子集的和作为物品的价值;
#include <iostream>
#include <vector>
class Solution {
public:
bool canPartition(std::vector<int>& nums) {
int sum = 0;
for(int num : nums){
sum += num;
}
if(sum % 2 == 1) return false; // 不能二等分
int target = sum / 2; // 一半和当作容量,另一半和作为价值
std::vector<int> dp(target+1, 0);
// 初始化
dp[0] = 0;
// 遍历
for(int i = 0; i < nums.size(); i++){
for(int j = target; j >= nums[i]; j--){ // j >= nums[i] 确保容量大于重量,这里物品的重量和价值相同,均为nums[i]
dp[j] = std::max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
if(dp[target] == target) return true; // 一半作为容量,另一半作为价值,因此可以等分
return false;
}
};
int main(int argc, char *argv[]) {
// nums = [1,5,11,5]
std::vector<int> test = {1, 5, 11, 5};
Solution S1;
bool res = S1.canPartition(test);
if(res) std::cout << "true" << std::endl;
else std::cout << "false" << std::endl;
return 0;
}
3--最后一块石头的重量II
主要思路:
类似于上题,可以转换为 0-1 背包问题;用一半的石头作为背包容量,另一半石头作为物品;
背包装尽可能多的石头,最后两者相减相消即可得到最小的重量;
#include <iostream>
#include <vector>
class Solution {
public:
int lastStoneWeightII(std::vector<int>& stones) {
int sum = 0;
for(int num : stones){
sum += num;
}
int vec = sum / 2; // 一半作为容量
std::vector<int> dp(vec+1, 0);
// 初始化
dp[0] = 0;
// 遍历
for(int i = 0; i < stones.size(); i++){
for(int j = vec; j >= stones[i]; j--){
dp[j] = std::max(dp[j], dp[j - stones[i]] + stones[i]); // 装尽可能多的石头
}
}
return sum - dp[vec] - dp[vec]; // sum - dp[vec] 表示另一半的石头
}
};
int main(int argc, char *argv[]) {
// stones = [2,7,4,1,8,1]
std::vector<int> test = {2, 7, 4, 1, 8, 1};
Solution S1;
int res = S1.lastStoneWeightII(test);
std::cout << res << std::endl;
return 0;
}
4--目标和
主要思路:
转化为 0-1 背包问题,一部分数值连同 target 转化为背包容量,剩余一部分数值转化为物品,求解恰好装满背包容量的方法数;dp[j] 表示背包容量为 j 时,装满背包的方法数;
状态转移方程:dp[j] += dp[j - nums[i]],其实质是:当背包已经装了nums[i]时,剩余容量为 j - nums[i],此时装满剩余容量的方法数为 dp[j - nums[i]],遍历不同的 nums[i] 将方法数相加即可;
是有点难理解。。。
#include <iostream>
#include <vector>
class Solution {
public:
int findTargetSumWays(std::vector<int>& nums, int target) {
int sum = 0;
for(int num : nums) sum += num;
if(sum < std::abs(target)) return 0; // 数组全部元素相加相减都不能构成target
if((sum + target) % 2 == 1) return 0; // 不能二等分
int bagsize = (sum + target) / 2;
std::vector<int> dp(bagsize + 1, 0);
dp[0] = 1;
for(int i = 0; i < nums.size(); i++){ // 遍历物品
for(int j = bagsize; j >= nums[i]; j--){ // 遍历背包容量
dp[j] += dp[j - nums[i]];
}
}
return dp[bagsize];
}
};
int main(int argc, char *argv[]) {
// nums = [1, 1, 1, 1, 1], target = 3
std::vector<int> test = {1, 1, 1, 1, 1};
int target = 3;
Solution S1;
int res = S1.findTargetSumWays(test, target);
std::cout << res << std::endl;
return 0;
}
5--一和零
主要思路:
转化为 0-1 背包问题,背包容量为能装 m 个 0 和 n 个 1;
dp[i][j] 表示0背包容量为i,1背包容量为j时,能装 strs 最大子集的长度;
初始化 dp[0][0] = 0, dp[i][0] = 0; dp[0][j] = 0;
状态转移方程:dp[i][j] = max(dp[i][j], dp[i-m1][j-n1] + 1);m1 和 n1 分别表示字符串 0 和 1 的个数,dp[i-m1][j-n1] + 1 表示装上当前字符串后子集的长度;
#include <iostream>
#include <vector>
#include <string>
class Solution {
public:
int findMaxForm(std::vector<std::string>& strs, int m, int n) {
std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
for(int i = 0; i < strs.size(); i++){ // 遍历物品
int m1 = 0, n1 = 0;
for(char c : strs[i]){ // 统计当前字符串 0 和 1 字符的个数
if(c == '0') m1++;
else n1++;
}
// 遍历背包容量
for(int j = m; j >= m1; j--){
for(int k = n; k >= n1; k--){
dp[j][k] = std::max(dp[j][k], dp[j - m1][k - n1] + 1);
}
}
}
return dp[m][n];
}
};
int main(int argc, char *argv[]) {
// strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
std::vector<std::string> test = {"10", "0001", "111001", "1", "0"};
int m = 5, n = 3;
Solution S1;
int res = S1.findMaxForm(test, m, n);
std::cout << res << std::endl;
return 0;
}
6--完全背包问题
完全背包问题的特征:
一共有 n 个物品,但每个物品可以任意选择多次;
一般解法:
两层 for 循环从小到大遍历物品和背包容量;
当先遍历物品,再遍历背包时,结果与物品顺序无关,一般用于组合问题;
当先遍历背包,再遍历物品时,结果与物品顺序有关,一般用于排列问题;
7--零钱兑换II
主要思路:
本题所有硬币可以任意取多次,因此是一个完全背包问题;
对于上例,[2 2 1] 和[1 2 2] 是等价的,因此本题对应于组合问题,应先遍历物品,再遍历背包容量;
#include <iostream>
#include <vector>
class Solution {
public:
int change(int amount, std::vector<int>& coins){
std::vector<int> dp(amount + 1, 0); // dp[j] 表示背包容量为 j 时,凑成总金额为 j 的组合数
dp[0] = 1; // 初始化
for(int i = 0; i < coins.size(); i++){ // 遍历物品
for(int j = coins[i]; j <= amount; j++){ // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
int main(int argc, char *argv[]) {
// amount = 5, coins = [1, 2, 5]
std::vector<int> test = {1, 2, 5};
int amount = 5;
Solution S1;
int res = S1.change(amount, test);
std::cout << res << std::endl;
return 0;
}
8--组合总和IV
主要思路:
本题所有整数可以任意取多次,因此是一个完全背包问题;
对于上例,[2 2 1] 和[1 2 2] 不是等价的,因此本题对应于排列问题,应先遍历背包容量,再遍历物品;
#include <iostream>
#include <vector>
class Solution {
public:
int combinationSum4(std::vector<int>& nums, int target) {
std::vector<int> dp(target + 1, 0); // dp[j] 表示target为j时对应的组合个数
dp[0] = 1; // 初始化
for(int i = 0; i <= target; i++){
for(int j = 0; j < nums.size(); j++){
// C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] >= INT_MAX - dp[i - num]。
if(i < nums[j] || dp[i] >= INT_MAX - dp[i - nums[j]]) continue; // 跳过背包容量小于物品容量的情况
dp[i] += dp[i - nums[j]];
}
}
return dp[target];
}
};
int main(int argc, char *argv[]) {
// nums = [1, 2, 3], target = 4
std::vector<int> test = {1, 2, 3};
int target = 4;
Solution S1;
int res = S1.combinationSum4(test, target);
std::cout << res << std::endl;
return 0;
}
9--零钱兑换
主要思路:
本题所有硬币可以任意取多次,因此是一个完全背包问题;
#include <iostream>
#include <vector>
#include <limits.h>
class Solution {
public:
int coinChange(std::vector<int>& coins, int amount) {
std::vector<int> dp(amount + 1, INT_MAX); // dp[j] 表示凑成总金额为j时的最少硬币数
dp[0] = 0;
for(int i = 0; i < coins.size(); i++){ // 先遍历物品
for(int j = coins[i]; j <= amount; j++){ // 再遍历背包容量
if(dp[j - coins[i]] != INT_MAX){ // 如果dp[j - coins[i]]是初始值则跳过
// dp[j - coins[i]] != INT_MAX 确保dp[j - coins[i]] 确保组合中的硬币相加 == j - coins[i],即恰好组合成金额
dp[j] = std::min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
int main(int argc, char *argv[]) {
// coins = [1, 2, 5], amount = 11
std::vector<int> test = {1, 2, 5};
int amount = 11;
Solution S1;
int res = S1.coinChange(test, amount);
std::cout << res << std::endl;
return 0;
}
10--完全平方数
主要思路:
完全平方数可以任意选取多次,因此可以转换为完全背包问题;
#include <iostream>
#include <vector>
#include <limits.h>
class Solution {
public:
int numSquares(int n) {
std::vector<int> dp(n + 1, INT_MAX); // dp[j] 表示构成整数 j 使用的最少完全平方数
dp[0] = 0; // 初始化
for(int i = 1; i*i <= n; i++){ // 遍历物品
for(int j = i*i; j <= n; j++){ // 遍历背包
dp[j] = std::min(dp[j], dp[j - i*i] + 1);
}
}
return dp[n];
}
};
int main(int argc, char *argv[]) {
int n = 12;
Solution S1;
int res = S1.numSquares(n);
std::cout << res << std::endl;
return 0;
}
11--单词拆分
主要思路:
本题中字符串列表的字符串可以任意选择多次,因此也可以转换为完全背包问题;
#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>
class Solution {
public:
bool wordBreak(std::string s, std::vector<std::string>& wordDict) {
std::unordered_set<std::string> hash; // 先将字符串存到哈希表中,便于后面查询
for(std::string word : wordDict){
hash.insert(word);
}
std::vector<bool> dp(s.length() + 1, false); // dp[j] 表示目标字符串的前j个字符,其能由字符串列表中字符串构成
dp[0] = true;
for(int i = 1; i <= s.length(); i++){ // 遍历背包
for(int j = 0; j <= i; j++){ // 遍历物品
// dp[j] 可以由列表构成,同时s个字符构成的字符串,其剩余字符串也能由列表构成
if(dp[j] == true && hash.find(s.substr(j, i-j)) != hash.end()){ // 这里将s.substr(j, i-j)认为是即将加入到背包的物品
dp[i] = true;
break; // 找到一种成功的组合就 break
}
}
}
return dp[s.length()];
}
};
int main(int argc, char *argv[]) {
// s = "leetcode", wordDict = ["leet", "code"]
std::string str = "leetcode";
std::vector<std::string> wordDict = {"leet", "code"};
Solution S1;
bool res = S1.wordBreak(str, wordDict);
if(res) std::cout << "true" << std::endl;
else std::cout << "false" << std::endl;
return 0;
}