目录
0/1背包问题
一、【模板】01背包
二、分割等和子集
三、目标和
四、最后一块石头的重量 II
完全背包问题
一、【模板】完全背包
二、零钱兑换
三、零钱兑换 II
四、完全平方数
二维费用的背包问题
一、一和零
二、盈利计划
似包非包
组合总和
卡特兰数
不同的二叉搜索树
背包问题:
有1个背包,地上一堆物品,挑选一些物品放入背包中
问:能挑选物品的最大的价值是多少?
两个限制:
1.物品的属性(比如物品的体积,重量, 价值等)
2.背包的属性(背包的最大容量,承重等)
当物品的个数都限制为1个时,此时的背包问题就是0/1背包问题,因为每个物品选就是1个,不选就是0个
当物品的个数没有任何限制,都有无穷多个时,此时的背包问题就是完全背包问题
本篇博客介绍0/1背包问题
0/1背包问题
一、【模板】01背包
【模板】01背包_牛客题霸_牛客网 (nowcoder.com)https://www.nowcoder.com/practice/fd55637d3f24484e96dad9e992d3f62e?tpId=230&tqId=2032484&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D1961.题目解析
两个问题:
1.能装的物品的最大价值是多少?(可以装满,也可以不装满)
2.要求恰好装满,此时物品的最大价值是多少?
2.算法分析
问题一:
1.状态表示
背包问题本质还是线性dp问题,所以状态表示依旧是 经验 + 题目要求
dp[i]: 从前i个物品中挑选,所有选法中,能挑选的最大价值(×)
因为考虑第i个物品要不要放入背包的时候,必须先知道当前背包的容量才行
dp[i][j]: 从前i个物品中挑选,总体积不超过j, 所有选法中,能挑选出来的最大价值
2.状态转移方程
3.初始化
物品的编号从1开始,因此给dp表添加一行一列
第一行表示从前0个物品中挑选, 体积不超过j,能挑选的最大价值, 全是0
第一列表示从前i个物品中挑选, 体积不超过0,能挑选的最大价值, 全是0
4.填表顺序
从上往下填表
5.返回值
dp[n][V]
问题二:
1.状态表示
dp[i][j]: 从前i个物品中挑选,总体积正好等于 j, 所有选法中,能挑选出来的最大价值
2.状态转移方程
3.初始化
物品的编号从1开始,因此给dp表添加一行一列
dp[0][0] 表示从前0个物品中挑选,体积恰好为0的最大价值,什么都不选即可,那就是0
第一行的其余位置:
从前0个物品中挑选,价值恰好为j(j > 0)的最大价值,由于0个物品,所以凑不出价值恰好为j,因此值都是-1
第一列的其余位置:
从前i个物品中挑选,体积恰好为 0 的最大价值,一个物品都不选即可, 所以都是0
4.填表顺序
从上往下填表
5.返回值
dp[n][V] == -1 ? 0 : dp[n][V]
3.算法代码
#include <iostream>
using namespace std;
#include <string.h>
const int N = 1010;
int n, V, v[N], w[N]; //物品个数,背包体积,物品体积数组,物品价值数组
int dp[N][N];
int main()
{
//读入数据
cin >> n >> V;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
//第一问
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= V; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i-1][j-v[i]] + w[i]);
}
}
cout << dp[n][V] << endl;
//第二问
memset(dp, 0, sizeof(dp)); //清空dp表
for(int j = 1; j <= V; j++) //初始化dp表
dp[0][j] = -1;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= V; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= v[i] && dp[i-1][j-v[i]] != -1)
dp[i][j] = max(dp[i][j], dp[i-1][j-v[i]] + w[i]);
}
}
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
}
四、空间优化
我们在最开始讲解动态规划时就提到过,背包问题中经常可以使用空间优化,也就是不必使用二维数组,根据状态转移方程, 填写dp[i][j]时,只需要dp[i-1][j]与dp[i-1][j-v[i]]的值,因此只需要两行即可,也就是两个一维数组即可
而我们可以i进一步优化,也就是只需要一个一维数组即可, 也就是用dp[i-1][j-v[i]]的值与dp[i-1][j]的值在原始数组上更新dp[i][j]即可, 不过要注意,由于在原始数组上更新,防止发生覆盖,需要从右向左更新, 而代码只需要把所有的二位数组的第一维全删掉,填表的第二层for循环从右向左填并且可以把更新状态转移方程的条件放在循环条件的判断处即可
#include <iostream>
using namespace std;
#include <string.h>
const int N = 1010;
int n, V, v[N], w[N]; //物品个数,背包体积,物品体积数组,物品价值数组
int dp[N];
int main()
{
//读入数据
cin >> n >> V;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
//第一问
for(int i = 1; i <= n; i++)
for(int j = V; j >= v[i]; j--)
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
cout << dp[V] << endl;
//第二问
memset(dp, 0, sizeof(dp)); //清空dp表
for(int j = 1; j <= V; j++) //初始化dp表
dp[j] = -1;
for(int i = 1; i <= n; i++)
for(int j = V; j >= v[i]; j--)
if(dp[j-v[i]] != -1)
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
}
二、分割等和子集
416. 分割等和子集 - 力扣(LeetCode)https://leetcode.cn/problems/partition-equal-subset-sum/1.题目解析
给定一个只包含正整数的数组,问是否可以将数组划分成元素和相等的两部分
2.算法分析
转化一下: 本质就是从数组中挑选一些数,看这些数之和能否等于sum/2即可
本题虽然不完全是0/1背包问题,但是可以划分到该0/1背包问题,因为每个数要么选,要么不选,相当于每个物品选或不选,而且本题比0/1背包问题更加简单,因为本题就是看数字能否凑成sum/2即可, 0/1背包问题求的是物品的价值和最大~
1.状态表示
dp[i][j]: 表示从前i个数中挑选,和能否凑成j
2.状态转移方程
3.初始化
添加有一行一列
dp[0][0] = true, 表示一个数都不选,是否可以凑成0, 当然可以
第一行的其余位置: i =0, 表示一个数都不选,能否凑成1, 2, 3...., 当然不可以,值为false
第一列的其余位置: j = 0, 表示从前i个数选,和能否凑成0, 一个都不选即可,值为true
4.填表顺序
从上往下填表
5.返回值
dp[n][sum/2]
3.算法代码
二维dp表:
class Solution {
public:
bool canPartition(vector<int>& nums)
{
//求数组所有元素的和
int sum = 0;
for(auto x : nums)
sum += x;
if(sum % 2) return false; //和是奇数, 直接返回false
//1.创建dp表
int m = nums.size(), aim = sum / 2;
vector<vector<bool>> dp(m+1, vector<bool>(aim+1));
//2.初始化
for(int i = 0; i <= m ;i++)
dp[i][0] = true;
//3.填表
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= aim; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= nums[i-1]) //注意下标的映射关系
dp[i][j] = dp[i][j] || dp[i-1][j-nums[i-1]];
}
}
//4.返回值
return dp[m][aim];
}
};
滚动数组优化:
class Solution {
public:
bool canPartition(vector<int>& nums)
{
//求数组所有元素的和
int sum = 0;
for(auto x : nums)
sum += x;
if(sum % 2) return false; //和是奇数, 直接返回false
//1.创建dp表
int m = nums.size(), aim = sum / 2;
vector<bool> dp(aim+1);
//2.初始化
dp[0] = true;
//3.填表
for(int i = 1; i <= m; i++)
for(int j = aim; j >= nums[i-1]; j--)
dp[j] = dp[j] || dp[j-nums[i-1]];
//4.返回值
return dp[aim];
}
};
三、目标和
494. 目标和 - 力扣(LeetCode)https://leetcode.cn/problems/target-sum/1.题目解析
给定一个非负nums数组和整数target, 给每个数组元素前可以加正负号,使得最终表达式运算的结果 == target即可,求满足条件的表达式的数目
2.算法分析
题目本质就是将数组划分成一堆正数和负数,假如正数的和是a, 负数的和的绝对值是b, 那么a-b==target, 而我们知道a + b == sum, 消元得 a = (target + sum) / 2;
因此问题就转化成了在数组中挑一些数,使得这些数的和等于a, 问共有多少种选法?? 此时与题目二基本一样了, 仍然属于0/1背包问题
1.状态表示
dp[i][j]: 从前i个数中挑选,总和正好等于j, 一共有多少种选法
2.状态转移方程
3.初始化
添加一行一列
dp[0][0] = 1, 表示 从前 0个数中挑选,总和恰好为0, 只要一个数都不选即可
第一行的其余位置: 表示 从前 0个数中挑选,总和恰好为j(>0), 显然不可能,因此值都为0
第一列的其余位置: 表示 从前 i个数中挑选,总和恰好为0, 但是nums中的元素值可能为0,因此可能有很多选法,但实际上不用初始化,因为初始化是为了防止越界,而第一列j = 0, 如果要用dp[i-1][j-nums[i]]这个状态转移方程,必须满足j >= nums[i], 也就是nums[i]只能等于0,此时dp[i-1][j-nums[i]]就是 dp[i-1][0]了,也就是正上方的值,因此不会越界, 因此无需初始化
4.填表顺序
从上往下填表
5.返回值
dp[n][a]
3.算法代码
二维dp表:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target)
{
int sum = 0;
for(auto x : nums)
sum += x;
int a = (sum + target) / 2;
if(a < 0 || (sum + target) % 2) return 0; //a小于0或者sum+target是奇数, 直接返回0
//1.创建dp表
int n = nums.size();
vector<vector<int>> dp(n+1, vector<int>(a+1));
//2.初始化
dp[0][0] = 1;
//3.填表
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= a; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= nums[i-1])
dp[i][j] += dp[i-1][j-nums[i-1]];
}
}
//4.返回值
return dp[n][a];
}
};
滚动数组优化:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target)
{
int sum = 0;
for(auto x : nums)
sum += x;
int a = (sum + target) / 2;
if(a < 0 || (sum + target) % 2) return 0; //a小于0或者sum+target是奇数, 直接返回0
//1.创建dp表
int n = nums.size();
vector<int> dp(a+1);
//2.初始化
dp[0] = 1;
//3.填表
for(int i = 1; i <= n; i++)
for(int j = a; j >= nums[i-1]; j--)
dp[j] += dp[j-nums[i-1]];
//4.返回值
return dp[a];
}
};
四、最后一块石头的重量 II
1049. 最后一块石头的重量 II - 力扣(LeetCode)https://leetcode.cn/problems/last-stone-weight-ii/description/1.题目解析
之前我们已经在<<优先级队列>>博客中讲解了"最后一块石头的重量 I", 每次选两块最重的石头,而本题每次随机选两个石头进行碰撞,如果最后还剩余了一块石头,那么求最终剩余石头的最小重量,如果没有剩余石头,那么返回0
2.算法分析
本题的难点仍然在于如何把原问题进行转化,假设给定的数组元素是[a, b, c, d, e]
每次随机选两个石头进行碰撞, 可以发现最终结果无非是给原始数组元素添加上正负号,使得最终
的结果最小即可, 而我们可以把数分成两堆,一堆是正数,一堆是负数,假设正数的和是a, 负数和的绝对值是b, 已知a + b == sum, 求a - b 的最小值(也可能是b-a), 而a - b要最小,也就是a/b要最接近 sum的一半,此时问题就变成了0/1背包问题
把每一个数看成一个物品,每个物品的体积和价值都是nums[i], 挑选一些物品,背包的容量为sum / 2, 问能够装的的最大价值
1.状态表示
dp[i][j]: 从前i个数选,总和不超过j, 此时的最大和
2.状态转移方程
3.初始化
添加一行一列
第一行表示从前0个物品中挑选, 体积不超过j,能挑选的最大价值, 全是0
第一列表示从前i个物品中挑选, 体积不超过0,能挑选的最大价值, 全是0
4.填表顺序
从上往下
5.返回值
dp[n][m]相当于 a, sum - dp[n][m]是b, 由于dp[n][m] <= sum / 2的, b - a 一定 >= 0, 因此返回 b - a 即 sum - 2 * dp[n][m]
3.算法代码
二维dp表:
class Solution {
public:
int lastStoneWeightII(vector<int>& nums)
{
int n = nums.size();
int sum = 0;
for(auto x : nums)
sum += x;
int aim = sum / 2;
//1.创建dp表
vector<vector<int>> dp(n+1, vector<int>(aim+1));
//2.填表
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= aim; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= nums[i-1])
dp[i][j] = max(dp[i][j], dp[i-1][j-nums[i-1]] + nums[i-1]);
}
}
//3.返回值
return sum - 2 * dp[n][aim];
}
};
滚动数组优化:
class Solution {
public:
int lastStoneWeightII(vector<int>& nums)
{
int n = nums.size();
int sum = 0;
for(auto x : nums)
sum += x;
int aim = sum / 2;
//1.创建dp表
vector<int> dp(aim+1);
//2.填表
for(int i = 1; i <= n; i++)
for(int j = aim; j >= nums[i-1]; j--)
dp[j] = max(dp[j], dp[j-nums[i-1]] + nums[i-1]);
//3.返回值
return sum - 2 * dp[aim];
}
};
完全背包问题
一、【模板】完全背包
完全背包和0/1背包的区别就是完全背包问题中每个物品的个数是不限的
【模板】完全背包_牛客题霸_牛客网 (nowcoder.com)https://www.nowcoder.com/practice/237ae40ea1e84d8980c1d5666d1c53bc?tpId=230&tqId=2032575&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D1961.题目解析
两个问题:
1.能装的物品的最大价值是多少?(可以装满,也可以不装满)
2.要求恰好装满,此时物品的最大价值是多少?
2.算法分析
问题一:
1.状态表示
dp[i][j]: 从前i个物品中挑选,总体积不超过j, 所有选法中,能挑选出来的最大价值
2.状态转移方程
3.初始化
添加一行一列
第一列不可能越界,因为第一列 j = 0, 而 dp[i][j-v[i]] + w[i] 是在 j >= v[i]的情况下成立的,因此v[i] 只能等于0, 因此不会越界。
第一行表示从前0个物品挑选,体积不超过 j的最大价值,全都初始化成0即可
4.填表顺序
从上往下填表,每一行从左往右
5.返回值
dp[n][V]
问题二:
1.状态表示
dp[i][j]: 从前i个物品中挑选,总体积正好等于j , 所有选法中,能挑选出来的最大价值
2.状态转移方程
3.初始化
添加一行一列
同理,第一列不可能越界
dp[0][0] 表示从前0个物品中挑选,体积恰好为0的最大价值,那就是0
第一行的其余位置: 从前0个物品中挑选,体积恰好为j(j>0)的最大价值,由于没有物品,所以体积不可能 > 0, 因此都是-1
4.填表顺序
从上往下填表,每一行从左往右
5.返回值
dp[n][V] == -1 ? 0 : dp[n][V]
3.算法代码
#include <iostream>
using namespace std;
#include <string.h>
const int N = 1001;
int n, V, v[N], w[N];
int dp[N][N];
int main()
{
cin >> n >> V;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= V; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= v[i])
dp[i][j] = max(dp[i][j], dp[i][j-v[i]] + w[i]);
}
}
cout << dp[n][V] << endl;
memset(dp, 0, sizeof(dp));
for(int j = 1; j <= V; j++)
dp[0][j] = -1;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= V; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= v[i] && dp[i][j-v[i]] != -1)
dp[i][j] = max(dp[i][j], dp[i][j-v[i]] + w[i]);
}
}
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
}
4.空间优化
更新dp[i][j]会用到两个绿色格子的值,而空间优化的本质就是只用一个一维dp表
黄色表示还没更新的值,绿色表示已经更新过的值,因此要填dp[i][j], 必须要保证用到的两个格子已经变成了绿色格子才可以,因此必须从左往右填一维dp表!! 这也是和0/1背包空间优化不同的地方,0/1背包是从右往左填一维dp表的
#include <iostream>
using namespace std;
#include <string.h>
const int N = 1001;
int n, V, v[N], w[N];
int dp[N];
int main()
{
//输入
cin >> n >> V;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
//问题一
for(int i = 1; i <= n; i++)
for(int j = v[i]; j <= V; j++)
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
cout << dp[V] << endl;
//问题二
memset(dp, 0, sizeof(dp));
for(int j = 1; j <= V; j++)
dp[j] = -1;
for(int i = 1; i <= n; i++)
for(int j = v[i]; j <= V; j++)
if(dp[j-v[i]] != -1)
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
}
补充:
在问题二的代码中,用状态转移方程之前要先判断要用到的状态是否存在,事实上,我们也可以不用判断,只要保证求完max之后不会对结果产生影响即可,因此在初始化的时候,可以初始化成-0x3f3f3f3f即可
#include <iostream>
using namespace std;
#include <string.h>
const int N = 1001;
int n, V, v[N], w[N];
int dp[N];
int main()
{
//输入
cin >> n >> V;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
//问题一
for(int i = 1; i <= n; i++)
for(int j = v[i]; j <= V; j++)
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
cout << dp[V] << endl;
//问题二
memset(dp, 0, sizeof(dp));
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] = max(dp[j], dp[j-v[i]] + w[i]);
cout << (dp[V] < 0 ? 0 : dp[V]) << endl;
}
二、零钱兑换
322. 零钱兑换 - 力扣(LeetCode)https://leetcode.cn/problems/coin-change/1.题目解析
给定整数数组coins和一个整数amount表示总金额,求凑成amount所需要的硬币最小个数
2.算法分析
1.状态表示
dp[i][j]: 从前 i 个硬币中挑选,总和正好等于 j, 所有选法中,最少的硬币个数
2.状态转移方程
3.初始化
dp[0][0] = 0
第一行其余位置: 0x3f3f3f3f (参考上一道题目的补充内容)
4.填表顺序
从上往下填表, 每一行从左往右
5.返回值
dp[n][amout] >= 0x3f3f3f3f ? -1 : dp[n][amount]
3.算法代码
二维dp表
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
int n = coins.size();
const int INF = 0x3f3f3f3f;
vector<vector<int>> dp(n+1, vector<int>(amount+1));
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++)
{
dp[i][j] = dp[i-1][j];
if(j >= coins[i-1])
dp[i][j] = min(dp[i][j], dp[i][j-coins[i-1]] + 1);
}
}
return dp[n][amount] >= INF ? -1 : dp[n][amount];
}
};
空间优化:
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
int n = coins.size();
const int INF = 0x3f3f3f3f;
vector<int> dp(amount+1, INF);
dp[0] = 0;
for(int i = 1; i <= n; i++)
for(int j = coins[i-1]; j <= amount; j++)
dp[j] = min(dp[j], dp[j-coins[i-1]] + 1);
return dp[amount] >= INF ? -1 : dp[amount];
}
};
三、零钱兑换 II
518. 零钱兑换 II - 力扣(LeetCode)https://leetcode.cn/problems/coin-change-ii/description/1.题目解析
本题求的是凑成 amount 的硬币的组合个数
2.算法分析
1.状态表示
dp[i][j]: 从前 i 个硬币中挑选,总和正好等于 j, 一共有多少种选法
2.状态转移方程
3.初始化
dp[0][0] = 1, 表示从前 0 个硬币中挑选,总和恰好为0的,共有1种选法(就是什么都不选)
第一行的其余位置初始化成0即可, 表示从前 0 个硬币中挑选,总和恰好为 j (>0),共有0种选法
4.填表顺序
从上往下填表,每一行从左往右
5.返回值
dp[n][amount]
3.算法代码
二维dp表
class Solution
{
public:
int change(int amount, vector<int>& coins)
{
int n = coins.size();
vector<vector<int>> dp(n+1, vector<int>(amount+1));
dp[0][0] = 1;
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= amount; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= coins[i-1])
dp[i][j] += dp[i][j-coins[i-1]];
}
}
return dp[n][amount];
}
};
空间优化
class Solution
{
public:
int change(int amount, vector<int>& coins)
{
int n = coins.size();
vector<int> dp(amount+1);
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];
}
};
四、完全平方数
279. 完全平方数 - 力扣(LeetCode)https://leetcode.cn/problems/perfect-squares/1.题目解析
给定一个数n, 返回和为n的完全平方数的最小数量
2.算法分析
转化成完全背包问题
1.状态表示
dp[i][j]: 从前 i 个完全平方数中挑选,总和正好等于 j, 所有选法中,最小的数量
2.状态转移方程
3.初始化
dp[0][0] = 0, 从前 0 个完全平方数中挑选,总和恰好等于 0,只需要0个完全平方数即可
第一行其余位置:
从前 0 个完全平方数中挑选,总和恰好等于 j( > 0),显然做不到,初始化成为0x3f3f3f3f即可
4.填表顺序
从上往下填表,每一行从左往右
5.返回值
dp[根号下n][n]
3.算法代码
二维dp表
class Solution
{
public:
int numSquares(int n)
{
//1.创建dp表
int m = sqrt(n);
vector<vector<int>> dp(m+1, vector<int>(n+1));
const int INF = 0x3f3f3f3f;
//2.初始化
for(int j = 1; j <= n; j++)
dp[0][j] = INF;
//3.填表
for(int i = 1; i <= m; i++)
{
for(int j = 0; j <= n; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= i*i)
dp[i][j] = min(dp[i][j], dp[i][j-i*i] + 1);
}
}
//4.返回值
return dp[m][n];
}
};
空间优化
class Solution
{
public:
int numSquares(int n)
{
//1.创建dp表
int m = sqrt(n);
vector<int> dp(n+1);
const int INF = 0x3f3f3f3f;
//2.初始化
for(int j = 1; j <= n; j++)
dp[j] = INF;
//3.填表
for(int i = 1; i <= m; i++)
for(int j = i*i; j <= n; j++)
dp[j] = min(dp[j], dp[j-i*i] + 1);
//4.返回值
return dp[n];
}
};
二维费用的背包问题
一、一和零
474. 一和零 - 力扣(LeetCode)https://leetcode.cn/problems/ones-and-zeroes/description/1.题目解析
给你一个二进制字符串数组
strs
和两个整数m
和n
,找出并返回strs
的最大子集的长度,该子集中 最多 有m
个0
和n
个1
2.算法分析
本质就是在字符串数组中挑选一些字符串出来,需要满足两个条件(1和0的个数限制), 这类问题就是二维费用的背包问题(也分为二维费用的0/1背包问题 和 二维费用的完全背包问题
1.状态表示
dp[i][j][k] 表示 从前 i 个字符串中挑选,字符0的个数不超过j, 字符1的个数不超过k, 所有的选法中,最大的长度2.状态转移方程
3.初始化
当i=0, 表示从前 0 个字符串中挑选,字符0的个数不超过j, 字符1的个数不超过k, 没有字符串可选,因此长度就是0, 全部初始化成0即可
4.填表顺序
i从小到大即可
5.返回值
dp[len][m][n]
len表示字符串的长度
3.算法代码
三维dp表
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n)
{
int len = strs.size();
vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m+1, vector<int>(n+1)));
for(int i = 1; i <= len; i++)
{
//统计当前字符串中0/1字符的个数
int a = 0, b = 0;
for(auto ch : strs[i-1])
if(ch == '0') a++;
else b++;
for(int j = 0; j <= m; j++)
{
for(int k = 0; k <= n; k++)
{
dp[i][j][k] = dp[i-1][j][k];
if(j >= a && k >= b)
dp[i][j][k] = max(dp[i][j][k], dp[i-1][j-a][k-b] + 1);
}
}
}
return dp[len][m][n];
}
};
空间优化
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n)
{
int len = strs.size();
vector<vector<int>> dp(m+1, vector<int>(n+1));
for(int i = 1; i <= len; i++)
{
//统计当前字符串中0/1字符的个数
int a = 0, b = 0;
for(auto ch : strs[i-1])
if(ch == '0') a++;
else b++;
for(int j = m; j >= a; j--)
for(int k = n; k >= b; k--)
dp[j][k] = max(dp[j][k], dp[j-a][k-b] + 1);
}
return dp[m][n];
}
};
二、盈利计划
879. 盈利计划 - 力扣(LeetCode)https://leetcode.cn/problems/profitable-schemes/1.题目解析
n表示员工人数, minProfit表示至少需要产生的利润,profit数组和group数组对应, 第i种工作需要group[i]个员工完成,会产生profit[i]的利润,现在问有多少种选择工作的方法,使得能够产生的利润>=minProfit
2.算法分析
本质是二维费用的0/1背包问题
1.状态表示
dp[i][j][k]: 从前i个工作中挑选,总人数不超过 j, 总利润至少为 k, 一共有多少种选法
2.状态转移方程
3.初始化
dp[0][j][0], i = 0表示没有计划, 没有计划就没有利润,因此 k 也是0,此时什么都不选就满足条件,因此将dp[0][j][0]初始化成1
4.填表顺序
i从小到大即可
5.返回值
dp[len][n][m]
len是group数组或profit数组的长度
3.算法代码
三维dp表
class Solution {
public:
int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p)
{
const int MOD = 1e9 + 7;
//1.创建dp表
int len = g.size();
vector<vector<vector<int>>> dp(len+1, vector<vector<int>>(n+1, vector<int>(m+1)));
//2.初始化dp表
for(int j = 0; j <= n; j++)
dp[0][j][0] = 1;
//3.填表
for(int i = 1; i <= len; i++)
{
for(int j = 0; j <= n; j++)
{
for(int k = 0; k <= m; k++)
{
dp[i][j][k] += dp[i-1][j][k];
if(j >= g[i-1])
dp[i][j][k] += dp[i-1][j-g[i-1]][max(0, k-p[i-1])];
dp[i][j][k] %= MOD;
}
}
}
//4.返回值
return dp[len][n][m];
}
};
空间优化
class Solution {
public:
int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p)
{
const int MOD = 1e9 + 7;
//1.创建dp表
int len = g.size();
vector<vector<int>> dp(n+1, vector<int>(m+1));
//2.初始化dp表
for(int j = 0; j <= n; j++)
dp[j][0] = 1;
//3.填表
for(int i = 1; i <= len; i++)
for(int j = n; j >= g[i-1]; j--)
for(int k = m; k >= 0; k--)
{
dp[j][k] += dp[j-g[i-1]][max(0, k-p[i-1])];
dp[j][k] %= MOD;
}
//4.返回值
return dp[n][m];
}
};
似包非包
组合总和
377. 组合总和 Ⅳ - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum-iv/description/1.题目解析
给定一个nums数组和target目标值,从nums数组中找出和为target的排列个数,每个数可以选择无数次,注意顺序不同算是不同的情况
2.算法分析
"背包问题"研究的本质是限制条件下的组合问题,也就是我们挑选物品没有考虑顺序,而本题是要考虑顺序的,因此本题不能用"完全背包"的方法来解决
1.状态表示
本题的状态表示方式是之前的博客没有提到的:
根据分析问题的过程中,发现重复子问题,抽象出一个状态表示
[a, b, c, d, e], 在整个区间中选择凑成总和为target, 如果第一个数选了a, 那只需要在整个区间凑成总和为target-a即可, 于是我们发现了重复子问题
dp[i]表示: 凑成总和为 i, 一共有多少种排列数
2.状态转移方程
3.初始化
dp[0] = 1,因为数组中元素>=1,因此要凑出总和为0, 什么都不选,也就是空集
4.填表顺序
从左往右
5.返回值
dp[target]
3.算法代码
class Solution {
public:
int combinationSum4(vector<int>& nums, int target)
{
vector<double> dp(target+1);
dp[0] = 1;
for(int i = 1; i <= target; i++)
for(auto x : nums)
if(i >= x) dp[i] += dp[i-x];
return dp[target];
}
};
卡特兰数
不同的二叉搜索树
96. 不同的二叉搜索树 - 力扣(LeetCode)https://leetcode.cn/problems/unique-binary-search-trees/description/1.题目解析
给定整数n, 求由n个节点(值是1到n)组成的二叉搜索树有多少种
2.算法分析
1.状态表示 --- 根据分析问题的过程中,发现重复子问题,抽象出一个状态表示
随便选一个数,作为根,此时就只需要分析左子树和右子树有多少种二叉搜索树, 然后乘起来
dp[i]表示: 节点个数为i时,一共有多少种二叉搜索树
2.状态转移方程
节点为[1, 2, ... i], 随便选一个数j作为根,则左子树的节点个数为j-1, 右子树的节点个数为i-j,
dp[i] += dp[j-1] * dp[i-j]
(1<= j <= i)
3.初始化
dp[0] = 1,空树也是一颗二叉搜索树
4.填表顺序
从左往右
5.返回值
dp[n]
3.算法代码
class Solution {
public:
int numTrees(int n)
{
vector<int> dp(n+1);
dp[0] = 1;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= i; j++)
dp[i] += dp[j-1] * dp[i-j];
return dp[n];
}
};