1 一和零
1.1 题目链接
1.2 题目描述
给你一个二进制字符串数组 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 <= 6001 <= strs[i].length <= 100strs[i]仅由'0'和'1'组成1 <= m, n <= 100
1.3 解法(动态规划)
算法思路:
先将问题转化成我们熟悉的题型。
- i. 在⼀些物品中「挑选」⼀些出来,然后在满⾜某个「限定条件」下,解决⼀些问题,⼤概率是背包模型;
- ii. 由于每⼀个物品都只有 1 个,因此是⼀个「01 背包问题」。
- 但是,我们发现这⼀道题⾥⾯有「两个限制条件」。因此是⼀个「⼆维费⽤的 01 背包问题」。那么我们定义状态表⽰的时候,来⼀个三维 dp 表,把第⼆个限制条件加上即可。
-
状态表⽰:
dp[i][j][k] 表⽰:从前 i 个字符串中挑选,字符 0 的个数不超过 j ,字符 1 的个数不超过 k ,所有的选法中,最⼤的⻓度。
-
状态转移⽅程:
- 线性 dp 状态转移⽅程分析⽅式,⼀般都是「根据最后⼀步」的状况,来分情况讨论。为了⽅便叙述,我们记第 i 个字符中,字符 0 的个数为 a ,字符 1 的个数为 b :
- i. 不选第 i 个字符串:相当于就是去前 i - 1 个字符串中挑选,并且字符 0 的个数不超过 j ,字符 1 的个数不超过 k 。此时的最⼤⻓度为 dp[i][j][k] = dp[i - 1][j][k] ;
- ii. 选择第 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[len][m][n] 。
- 其中 len 表⽰字符串数组的⻓度。
-
空间优化:
- 所有的「背包问题」,都可以进⾏空间上的优化。
- 对于「⼆维费⽤的 01 背包」类型的,我们的优化策略是:
- i. 删掉第⼀维;
- ii. 修改第⼆层以及第三层循环的遍历顺序即可。
1.4 C++算法代码:
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];
}
};
2 盈利计划
2.1 题目链接
2.2 题目描述
集团里有 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 <= 1000 <= minProfit <= 1001 <= group.length <= 1001 <= group[i] <= 100profit.length == group.length0 <= profit[i] <= 100
2.3 解法(动态规划)
算法思路:
这道题⽬⾮常难读懂,但是如果结合例⼦多读⼏遍,你就会发现是⼀个经典的「⼆维费⽤的背包问题」。因此我们可以仿照「⼆维费⽤的背包」来定义状态表⽰。
-
状态表⽰:
- dp[i][j][k] 表⽰:从前 i 个计划中挑选,总⼈数不超过 j ,总利润⾄少为 k ,⼀共有多少种选法。
- 注意注意注意,这道题⾥⾯出现了⼀个「⾄少」,和我们之前做过的背包问题不⼀样。因此,我们在分析「状态转移⽅程」的时候要结合实际情况考虑⼀下。
-
状态转移⽅程:
- ⽼规矩,根据「最后⼀个位置」的元素,结合题⽬的要求,我们有「选择」最后⼀个元素或者「不选择」最后⼀个元素两种策略:
- i. 不选 i 位置的计划:那我们只能去前 i - 1 个计划中挑选,总⼈数不超过 j ,总利润⾄少为 k 。此时⼀共有 dp[i - 1][j][k] 种选法;
- ii. 选择 i 位置的计划:那我们在前 i - 1 个计划中挑选的时候,限制就变成了,总⼈数不超过 j - g[i] ,总利润⾄少为 k - p[i] 。此时⼀共有 dp[i - 1][j - g[i]][k - p[i]] 。
- 第⼆种情况下有两个细节需要注意:
-
- j - g[i] < 0 :此时说明 g[i] 过⼤,也就是⼈数过多。因为我们的状态表⽰要求⼈数是不能超过 j 的,因此这个状态是不合法的,需要舍去。
-
- k - p[i] < 0 :此时说明 p[i] 过⼤,也就是利润太⾼。但是利润⾼,不正是我们想要的嘛?所以这个状态「不能舍去」。但是问题来了,我们的 dp 表是没有负数的下标的,这就意味着这些状态我们⽆法表⽰。其实,根本不需要负的下标,我们根据实际情况来看,如果这个任务的利润已经能够达标了,我们仅需在之前的任务中,挑选出来的利润⾄少为 0 就可以了。因为实际情况不允许我们是负利润,那么负利润就等价于利润⾄少为 0 的情况。所以说这种情况就等价于 dp[i][j][0] ,我们可以对 k - p[i] 的结果与 0 取⼀个 max 。
-
- 综上,我们的状态转移⽅程为:
- dp[i][j][k] = dp[i - 1][j][k] + dp[i - 1][j - g[i - 1]][max(0, k - p[i - 1])] 。
-
初始化:
- 当没有任务的时候,我们的利润为 0 ,此时⽆论⼈数限制为多少,我们都能找到⼀个「空集」的⽅案。
- 因此初始化 dp[0][j][0] 的位置为 1 ,其中 0 <= j <= n 。
-
填表顺序:
根据「状态转移⽅程」,我们保证 i 从⼩到⼤即可。
-
返回值:
- 根据「状态表⽰」,我们返回 dp[len][m][n] 。
- 其中 len 表⽰字符串数组的⻓度。
-
空间优化:
- 所有的「背包问题」,都可以进⾏空间上的优化。
- 对于「⼆维费⽤的 01 背包」类型的,我们的优化策略是:
- i. 删掉第⼀维;
- ii. 修改第⼆层以及第三层循环的遍历顺序即可。
2.4 C++算法代码:
class Solution {
public:
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
const int MOD = 1e9 + 7;
int len =group.size();
vector<vector<vector<int>>> dp (len + 1, vector<vector<int>>(n + 1, vector<int>(minProfit + 1)));
for(int j = 0; j <= n; j++) dp[0][j][0] = 1;
for(int i = 1; i <= len; i++)
{
for(int j = 0; j <= n; j++)
{
for(int k = 0; k <= minProfit; 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[len][n][minProfit];
}
};
空间优化后的算法代码:
class Solution {
public:
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
const int MOD = 1e9 + 7;
int len =group.size();
vector<vector<int>> dp (n + 1, vector<int>(minProfit + 1));
for(int j = 0; j <= n; j++) dp[j][0] = 1;
for(int i = 1; i <= len; i++)
{
for(int j = n; j >= group[i - 1] ; j--)
{
for(int k = minProfit; k >= 0; k--)
{
dp[j][k] += dp[j - group[i - 1]][max(0, k-profit[i - 1])];
dp[j][k] %= MOD;
}
}
}
return dp[n][minProfit];
}
};