题目来源
题目描述
class Solution {
public:
// m限制0,n限制1
int findMaxForm(vector<string>& strs, int m, int n) {
}
};
题目解析
class Solution {
std::vector<int> countZerosOnes(std::string &str){
std::vector<int> vec(2);
for (char ch : str) {
vec[ch - '0']++;
}
return vec;
}
public:
// m限制0,n限制1
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, 0)));
for (int i = 1; i <= len; ++i) {
std::string str = strs[i - 1];
auto count = countZerosOnes(str);
int zeros = count[0], ones = count[1];
for (int j = 0; j <= m; ++j) {
for (int k = 0; k <= n; ++k) {
if(zeros > j || ones > k){
dp[i][j][k] = dp[i - 1][j][k];
}else{
dp[i][j][k] =
std::max( dp[i - 1][j][k],
dp[i - 1][j - zeros][k - ones] + 1
);
}
}
}
}
return dp[len][m][n];
}
};
下面m和n的状态分析反了,待重写
回溯
啥叫做子集
数组可以看成是一个集合,子集是集合中的概念。
比如对于数组["1", "11"]
,它的子集有[]、["1"]、["11"]、["1"、"11"]
我们之前有做过一道题leetcode:78. 子集,不记得的话可以去看看
啥叫做最大子集
不管其他的条件,对于数组["1", "11"]
的所有子集[]、["1"]、["11"]、["1"、"11"]
,其最大子集就是["1"、"11"]
。可以简单理解为包含最多元素的那个子集。
因此,最大子集的长度就是4
然后题目表示只有那些最多有
m
个0和n
个1的子集才能满足条件,所以要剪枝。
就我在leetcode中遇过的题目而言,都是在源头剪枝,而不是搜索后筛选。
也就是在找出子集的过程中我们应该将不符合条件的那些子集提前干掉。
怎么干掉那些不符合条件的子集
先来写出个大致框架,然后再考虑剪枝
class Solution {
std::vector<std::vector<std::string>> subset;
std::vector<std::string> path;
void dfs(vector<string>& strs, int pos){
if("path满足要求,那么就加入到path中"){
// 所以必须要知道当前path有多少个0,多少个1了
subset.push_back(path);
}
for (int i = pos; i < strs.size(); ++i) {
path.emplace_back(strs[i]);
dfs(strs, i + 1);
path.pop_back();
}
}
public:
int findMaxForm(vector<string>& strs, int m, int n) {
dfs(strs, 0);
return 0;
}
};
那怎么知道当前path有多少个0,有多少个1呢?
- 我们可以先对strs数组进行预处理,统计下每个strs[i]当前有几个0和几个1。
- 虽然没有说strs[i]会不会重复,但是不管重不重复,对于每一个字符串其0和1的数目是固定的,所以我们可以用一个map<std::string, std::pair<int, int>>来统计每个strs[1]对应的0和对应的1的个数
class Solution {
std::vector<std::vector<std::string>> subset;
std::vector<std::string> path;
std::map<std::string, std::pair<int, int>> hash;
int ans = 0;
void dfs(vector<string>& strs, int pos, int c_m, int c_n, int m, int n){
if(c_m <= m && c_n <= n){
subset.push_back(path);
ans = std::max(ans, int( path.size()));
}else{ //有一个条件不满足就不必搜索了
return; //因为继续搜索c_m和c_n只会越来越大,不会缩小
}
for (int i = pos; i < strs.size(); ++i) {
path.emplace_back(strs[i]);
dfs( strs,i + 1,
c_m + hash[strs[i]].first,c_n + hash[strs[i]].second,
m,n);
path.pop_back();
}
}
public:
int findMaxForm(vector<string>& strs, int m, int n) {
// 预处理-----
for (int i = 0; i < strs.size(); ++i) {
std::string s = strs[i];
int one = 0, zeor = 0;
for(char c : s){
if(c == '0'){
++zeor;
}else{
++one;
}
}
hash[s] = {zeor, one};
}
// 搜索
dfs(strs, 0, 0, 0, m, n);
return ans;
}
};
超时
- 要缩小回溯的时间复杂度,可以往动态规划的方向来思考。因为递归—>备忘录—>动态规划。
多维01背包
这道题如果抽象成[背包问题]的话,应该是:
- 每个字符串的价值都是1(对该答案的贡献都是1),选择的成本是该字符串中1的数量和0的数量
- 提问:在1的数量不超过m,0的数量不超过n的条件下,最大价值是多少(选取了多少个字符,最大价值就是多少)
我们一般做的01背包只有一个限制,就是容量j;而本题有两种限制,即选取的字符串子集中0和1的数量上限。
(1)定义状态:
-
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]表示前i个字符串(可选字符区间
[
0...
i
−
1
]
[0...i-1]
[0...i−1]),背包中最多能装
j
个
1
,
k
个
0
j个1,k个0
j个1,k个0的情况下,最多能够选取的字符个数(每个字符串都有不定长度的1和0)
- dp[0][?][?]表示无可选字符串时,最多能够选取的字符个数。自然的,既然没有字符可以选,因此dp[0][?][?]=0
- dp[1][?][?]表示前1个字符(即strs[0]),背包中最多能装?个1,?个0的情况下,,最多能够选取的字符个数。
- dp[2][?][?]表示前2个字符(即strs[0]、strs[1]),背包中最多能装?个1,?个0的情况下,,最多能够选取的字符个数。
- …
- dp[len][?][?]表示前len个字符(即strs[0]、strs[1]、strs[2]…strs[len-1]),背包中最多能装?个1,?个0的情况下,,最多能够选取的字符个数。
- 另外:
- dp[?][0][0]表示xxxx字符串时,背包中最多能装0个1、0个0的情况下,最多能够选取的字符串个数。自然地,什么字符都不能选择,因此dp[?][0][0] = 0
- dp[?][0][1…?]表示xxxx字符,背包中最多能装0个1,x个0,这个时候我们不能确定dp[?][0][1…?]的值,因为有的字符串就是只有1没有0
- 从上面还可以推导出:
- dp第一维的长度是len + 1,第二维的长度是m + 1,第三维的长度是n+1
(2)状态转移方程
- 只考虑最后一步,也就是怎么填写 d p [ l e n ] [ m ] [ n ] dp[len][m][n] dp[len][m][n]。这个时候我们正在挑选 s t r s [ l e n − 1 ] strs[len - 1] strs[len−1]
- 首先,我们应该先求出 s t r s [ l e n − 1 ] strs[len - 1] strs[len−1]有几个1,有几个0
- 那么,对于
s
t
r
s
[
l
e
n
−
1
]
strs[len - 1]
strs[len−1]
- 情况一:如果选择不放,那么 d p [ l e n ] [ m ] [ n ] = d p [ l e n − 1 ] [ m ] [ n ] dp[len][m][n] = dp[len - 1][m][n] dp[len][m][n]=dp[len−1][m][n]
- 情况二:如果选择放入,那么必须先满足前提条件: s t r [ l e n − 1 ] 1 的 个 数 < = m str[len - 1]_{1的个数} <= m str[len−1]1的个数<=m&& s t r [ l e n − 1 ] 0 的 个 数 > n str[len - 1]_{0的个数} > n str[len−1]0的个数>n,此时 d p [ l e n ] [ m ] [ n ] = 1 + d p [ l e n − 1 ] [ m − s t r [ l e n − 1 ] 1 的 个 数 ] [ n − s t r [ l e n − 1 ] 0 的 个 数 ] dp[len][m][n] = 1 + dp[len - 1][m - str[len - 1]_{1的个数}][n - str[len - 1]_{0的个数}] dp[len][m][n]=1+dp[len−1][m−str[len−1]1的个数][n−str[len−1]0的个数]
- 我们应该从上面情况中选择一种价值最大的
- 因此有:
(3)初始条件和边界情况。
- 根据上面的分析,初始情况
- dp[0][?][?]表示无可选字符串时,最多能够选取的字符个数。自然的,既然没有字符可以选,因此dp[0][?][?]=0
- dp[?][0][0]表示xxxx字符串时,背包中最多能装0个1、0个0的情况下,最多能够选取的字符串个数。自然地,什么字符都不能选择,因此dp[?][0][0] = 0
- 边界条件:无边界条件
(4)计算顺序 - 从小到大,从前到后
(5)返回值
- dp[len][m][n]