算法沉淀——动态规划之其它背包问题与卡特兰数
- 二维费用的背包问题
- 01.一和零
- 02.盈利计划
- 似包非包
- 组合总和 Ⅳ
- 卡特兰数
- 不同的二叉搜索树
二维费用的背包问题
01.一和零
题目链接: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 。
提示:
1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i]
仅由'0'
和'1'
组成1 <= m, n <= 100
思路
问题转化为二维费用的01背包问题:
- 状态表示:
dp[i][j][k]
表示从前i
个字符串中挑选,字符0
的个数不超过j
,字符1
的个数不超过k
,所有的选法中,最大的长度。
- 状态转移方程:
- 根据最后一步的状况,分两种情况讨论:
- 不选第
i
个字符串:相当于去前i - 1
个字符串中挑选,并且字符0
的个数不超过j
,字符1
的个数不超过k
。此时的最大长度为dp[i][j][k] = dp[i - 1][j][k]
。 - 选择第
i
个字符串:接下来在前i - 1
个字符串中挑选,字符0
的个数不超过j - a
,字符1
的个数不超过k - b
的最大长度,然后在这个长度后面加上字符串i
。此时dp[i][j][k] = dp[i - 1][j - a][k - b] + 1
。需要特判这种状态是否存在。
- 不选第
- 综上,状态转移方程为:
dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1)
。
- 根据最后一步的状况,分两种情况讨论:
- 初始化:
- 当没有字符串的时候,没有长度,因此初始化为
0
。
- 当没有字符串的时候,没有长度,因此初始化为
- 填表顺序:
- 保证第一维的循环从小到大即可。
- 返回值:
- 根据状态表示,返回
dp[l][m][n]
。
- 根据状态表示,返回
代码
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
int l=strs.size();
vector<vector<vector<int>>> dp(l+1,vector<vector<int>>(m+1,vector<int>(n+1)));
for(int i=1;i<=l;i++){
int a=0,b=0;
for(char ch:strs[i-1])
if(ch=='0') a++;
else b++;
for(int j=m;j>=0;j--)
for(int k=n;k>=0;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[l][m][n];
}
};
02.盈利计划
题目链接: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 <= n <= 100
0 <= minProfit <= 100
1 <= group.length <= 100
1 <= group[i] <= 100
profit.length == group.length
0 <= profit[i] <= 100
思路
- 状态表示:
dp[i][j][k]
表示从前i
个计划中挑选,总人数不超过j
,总利润至少为k
,有多少种选法。
- 状态转移方程:
- 根据最后一位的元素,有两种选择策略:
- 不选第
i
位置的计划:此时只能在前i - 1
个计划中挑选,总人数不超过j
,总利润至少为k
。此时有dp[i - 1][j][k]
种选法。 - 选择第
i
位置的计划:在前i - 1
个计划中挑选的限制变成了,总人数不超过j - g[i]
,总利润至少为max(0, k - p[i])
。此时有dp[i - 1][j - g[i]][max(0, k - p[i])]
种选法。
- 不选第
- 注意特殊情况:
- 如果
j - g[i] < 0
,说明人数过多,状态不合法,舍去。 - 对于
k - p[i] < 0
,说明利润太高,但问题要求至少为k
,因此将其取max(0, k - p[i])
。
- 如果
- 综上,状态转移方程为:
dp[i][j][k] = dp[i - 1][j][k] + dp[i - 1][j - g[i]][max(0, k - p[i])]
。
- 根据最后一位的元素,有两种选择策略:
- 初始化:
- 当没有任务时,利润为
0
。在这种情况下,无论人数限制为多少,都能找到一个「空集」的方案。因此初始化dp[0][j][0]
为1
,其中0 <= j <= n
。
- 当没有任务时,利润为
- 填表顺序:
- 根据状态转移方程,保证
i
从小到大即可。
- 根据状态转移方程,保证
- 返回值:
- 根据状态表示,返回
dp[l][m][n]
,其中l
表示计划数组的长度。
- 根据状态表示,返回
代码
class Solution {
const int MOD=1e9+7;
public:
int profitableSchemes(int n, int m, vector<int>& group, vector<int>& profit) {
int l = group.size();
vector<vector<vector<int>>> dp(l+1,vector<vector<int>>(n+1,vector<int>(m+1)));
for(int j=0;j<=n;j++) dp[0][j][0]=1;
for(int i=1;i<=l;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>=group[i-1])
dp[i][j][k]+=dp[i-1][j-group[i-1]][max(0,k-profit[i-1])];
dp[i][j][k]%=MOD;
}
return dp[l][n][m];
}
};
似包非包
组合总和 Ⅳ
题目链接:https://leetcode.cn/problems/combination-sum-iv/
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3
输出:0
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 1000
nums
中的所有元素 互不相同1 <= target <= 1000
思路
注意这里题目意思容易混淆概念,其实这里是一个排列问题而并非组合问题,所以应该是普通的动态规划问题
- 状态表示:
dp[i]
表示总和为i
时,一共有多少种排列方案。
- 状态转移方程:
- 对于
dp[i]
,根据最后一个位置划分,选择数组中的任意一个数nums[j]
,其中0 <= j <= n - 1
。 - 当
nums[j] <= i
时,排列数等于先找到i - nums[j]
的方案数,然后在每一个方案后面加上一个数字nums[j]
。 - 因为有很多个
j
符合情况,状态转移方程为:dp[i] += dp[i - nums[j]]
,其中0 <= j <= n - 1
。
- 对于
- 初始化:
- 当和为
0
时,我们可以什么都不选,即「空集」一种方案,因此dp[0] = 1
。
- 当和为
- 填表顺序:
- 根据状态转移方程,从左往右填表。
- 返回值:
- 根据状态表示,返回
dp[target]
的值。
- 根据状态表示,返回
代码
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(int& x:nums)
if(x<=i) dp[i]+=dp[i-x];
return dp[target];
}
};
卡特兰数
不同的二叉搜索树
题目链接:https://leetcode.cn/problems/unique-binary-search-trees/
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3
输出:5
示例 2:
输入:n = 1
输出:1
提示:
1 <= n <= 19
思路
- 状态表示:
dp[i]
表示当结点数量为i
个时,一共有多少颗 BST。
- 状态转移方程:
- 对于
dp[i]
,选择每一个结点j
作为头结点,分析不同头结点的 BST 数量。 - 根据 BST 的定义,
j
号结点的左子树的结点编号在[1, j-1]
之间,有j-1
个结点,右子树的结点编号在[j+1, i]
之间,有i-j
个结点。 - 因此,
j
号结点作为头结点的 BST 种类数量为dp[j-1] * dp[i-j]
。 - 综合每一个可能的头结点,状态转移方程为:
dp[i] += dp[j-1] * dp[i-j]
,其中1 <= j <= i
。
- 对于
- 初始化:
dp[0]
表示空树,也是一颗二叉搜索树,因此dp[0] = 1
。- 针对
i
从 1 开始的情况,需要通过dp[j-1] * dp[i-j]
来计算。
- 填表顺序:
- 从左往右填表,保证每一步都有所依赖的子问题的解。
- 返回值:
- 返回
dp[n]
的值,表示结点数量为n
时的 BST 种类数量。
- 返回
代码
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];
}
};