1.题目
题目地址(474. 一和零 - 力扣(LeetCode))
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
2.题解
2.1 三维dp数组
思路
这里设计一个三维dp数组dp[i][j][k]
, 其中i表示在[0,i-1]中选择任意个数, j表示最多容忍的0的个数,k表示最多容忍的1的个数;而dp的值则记录已经选择了字符串的最大可能个数!
对于每一个字符串,我们转换为0-1背包问题,可以选择 选 或者 不选:
1.不选的话就是dp[i][j][k] = dp[i-1][j][k], 从之前的进行继承
2.选的话就是:
2.1 如果 j > zeros, k > ones
, 说明最大容忍个数允许这个字符串作为子集的一份子加入,
所以我们dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - zeros][k - ones] + 1);
, 保留原值和更新值(加了一个字符串,不要忘记+1)中的较大值即可
2.2 反之我们根本无法加入该字符串到当前子集中,所以无需考虑,跳过即可
代码
- 语言支持:C++
C++ Code:
class Solution {
public:
// 计算当前字符串中0和1的数量
vector<int> getZerosOnes(string& str) {
vector<int> count(2);
for (char ch : str) {
count[ch - '0']++;
}
return count;
}
int findMaxForm(vector<string>& strs, int m, int n) {
int len = strs.size();
// dp数组记录当前[0,i-1]范围中,0个数为j,1个数为k情况下选择的字符串个数
vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m + 1, vector<int>(n + 1, 0)));
// 更新dp数组(从1开始,之前已有隐式初始化所有i=0的值为0)
for (int i = 1; i <= len; i++) {
string str = strs[i - 1]; // 这里strs索引依旧是从0开始,所以不要忘记-1
vector<int> count = getZerosOnes(str);
int zeros = count[0], ones = count[1];
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= n; k++) {
// 如果不选择该字符串,保留原字符串个数
dp[i][j][k] = dp[i - 1][j][k];
// 如果选择字符串,可能遇到更新的dp已经存在值
// 这时候我们选取字符串个数多的保留即可(注意这里选择了该字符串,不要忘记了+1)
if (j >= zeros && k >= ones) {
dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - zeros][k - ones] + 1);
}
}
}
}
return dp[len][m][n];
}
};
复杂度分析
令 n 为数组长度。
- 时间复杂度:\(O(lmn+L)\)
其中\(l\)是数组strs的长度,\(m\)和\(n\)分别是 0 和 1 的容量,\(L\)是数组strs中的所有字符串的长度之和。
动态规划需要计算的状态总数是\(O(lmn)\),每个状态的值需要\(O(1)\)的时间计算。
对于数组strs中的每个字符串,都要遍历字符串得到其中的0 和 1 的数量,因此需要\(O(L)\)的时间遍历所有的字符串。
总时间复杂度是\(O(lmn+L)\) 。 - 空间复杂度:\(O(lmn)\)
2.2 二维dp数组(空间优化)
思路
同416-分割等和子集,我们这里发现其实对于dp[i]... = dp[i-1]..., 我们每次只需要记录上一次的dp值即可,而不要记录每一个i对应的dp值
所以这里我们只需要dp[j][k]即可解决问题。
但是这里我们需要注意一个问题:如果我们正序遍历,由于dp[j][k] = dp[j-zeros][k-ones]的存在,这里更大的序列值优先被更新;
等我们遍历到这些较大序列dp,会发现它的值已经不是上一次存储的值,而在之前已经被更新了;
为了避免这种情况,我们选择倒序遍历,便可以完美解决问题!
代码
class Solution {
public:
// 计算当前字符串中0和1的数量
vector<int> getZerosOnes(string& str) {
vector<int> count(2);
for (char ch : str) {
count[ch - '0']++;
}
return count;
}
int findMaxForm(vector<string>& strs, int m, int n) {
int len = strs.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
// 更新dp数组(从1开始,之前已有隐式初始化所有i=0的值为0)
for (int i = 1; i <= len; i++) {
string str = strs[i - 1]; // 这里strs索引依旧是从0开始,所以不要忘记-1
vector<int> count = getZerosOnes(str);
int zeros = count[0], ones = count[1];
for (int j = m; j >= 0; j--) {
for (int k = n; k >= 0; k--) {
// 如果选择字符串,可能遇到更新的dp已经存在值
// 这时候我们选取字符串个数多的保留即可(注意这里选择了该字符串,不要忘记了+1)
if (j >= zeros && k >= ones) {
dp[j][k] = max(dp[j][k], dp[j - zeros][k - ones] + 1);
}
}
}
}
return dp[m][n];
}
};
复杂度分析
令 n 为数组长度。
- 时间复杂度:\(O(lmn+L)\)
其中\(l\)是数组strs的长度,\(m\)和\(n\)分别是 0 和 1 的容量,\(L\)是数组strs中的所有字符串的长度之和。
动态规划需要计算的状态总数是\(O(lmn)\),每个状态的值需要\(O(1)\)的时间计算。
对于数组strs中的每个字符串,都要遍历字符串得到其中的0 和 1 的数量,因此需要\(O(L)\)的时间遍历所有的字符串。
总时间复杂度是\(O(lmn+L)\) 。 - 空间复杂度:\(O(mn)\)