此题来源于LeetCode 474. Ones and Zeroes
在写这篇之前,我百度了一下这道题,发现已经有很多人写过这个问题了,然而大多数只是为了答题而答题,给出了代码,很少有文字解释的,也很少有深入拓展的。因此,我这次来给出一个比较详尽的版本,并且在最后对结果进行了拓展。
已知一个字符串数组,数组内的字符串都是仅由0和1组成的,现在给定m个0和n个1,试问这m个0和n个1最多可以组成几个数组中的字符串。
比如:
Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
Output: 4
Explanation: This are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are “10,”0001”,”1”,”0”
又比如:
Input: Array = {"10", "0", "1"}, m = 1, n = 1
Output: 2
Explanation: You could form "10", but then you'd have nothing left. Better form "0" and "1".
这是一个非常典型的二维0/1背包问题,相当于是在问我们有一个背包的空间大小为m,最大载重为n,给定k个物品,已知每个物品的大小和重量,试问最多能放进多少个物品(每个物品只能放一次)。
该问题的状态方程为
时间复杂度: O(m⋅n⋅k)
空间复杂度: O(m⋅n⋅k)
其中, m 为0的个数, n 为1的个数, k 为已知字符串数组的长度strs.size()
这种解法虽然很浪费空间,但是保存了每种情况的状态,只有在这种情况下,我们才能逆推出这个最大长度是由哪些字符串组成的。
class Solution {
private:
vector<vector<vector<int>>> rec;
int strsN;
private:
//计算每个字符串有几个0和几个1
pair<int, int> countNums(string s){
int os = 0;
int zs = 0;
for (int i = 0; i < s.length(); i++){
if ('0' == s[i])
zs++;
}
os = s.length() - zs;
return make_pair(zs, os);
}
public:
int findMaxForm(vector<string>& strs, int m, int n) {
strsN = strs.size();
//创建一个m*n*strN的数组来存放每种情况下的状态
rec = vector<vector<vector<int>>>(m + 1, vector<vector<int>>(n + 1, vector<int>(strsN, 0)));
for (int count = 0; count < strsN; count++){
pair<int, int> p = countNums(strs[count]);
for (int i = m; i >= 0; i--){
for (int j = n; j >= 0; j--){
if (i >= p.first && j >= p.second)
rec[i][j][count] = (count == 0 ? 1 : max(rec[i][j][count - 1], 1 + rec[i - p.first][j - p.second][count - 1]));
else
rec[i][j][count] = (count == 0 ? 0 : rec[i][j][count - 1]);
}
}
}
return rec[m][n][strsN - 1];
}
};
时间复杂度: O(m⋅n⋅k)
空间复杂度: O(m⋅n⋅2) ,即 O(m⋅n)
其中, m 为0的个数, n 为1的个数, k 为已知字符串数组的长度strs.size()
然后,我们发现其实每次更新状态 k 时仅仅用到了上一次的状态 k−1 ,所以我们可以将存储状态的数组降成 m⋅n⋅2 的大小。
class Solution {
private:
vector<vector<vector<int>>> rec;
private:
//计算每个字符串有几个0和几个1
pair<int, int> countNums(string s){
int os = 0;
int zs = 0;
for (int i = 0; i < s.length(); i++){
if ('0' == s[i])
zs++;
}
os = s.length() - zs;
return make_pair(zs, os);
}
public:
int findMaxForm(vector<string>& strs, int m, int n) {
//创建一个m*n*2的数组来存放每种情况下的状态
rec = vector<vector<vector<int>>>(m + 1, vector<vector<int>>(n + 1, vector<int>(2, 0)));
for (int count = 0; count < strs.size(); count++){
pair<int, int> p = countNums(strs[count]);
//设置level来让rec[i][j][0]和rec[i][j][1]轮流变成上一组的状态
int level = count % 2;
for (int i = m; i >= 0; i--){
for (int j = n; j >= 0; j--){
if (i >= p.first && j >= p.second)
if (0 == level)
rec[i][j][0] = max(rec[i][j][1], 1 + rec[i - p.first][j - p.second][1]);
else
rec[i][j][1] = max(rec[i][j][0], 1 + rec[i - p.first][j - p.second][0]);
else
if (0 == level)
rec[i][j][0] = rec[i][j][1];
else
rec[i][j][1] = rec[i][j][0];
}
}
}
return max(rec[m][n][0], rec[m][n][1]);
}
};
时间复杂度: O(m⋅n⋅k)
空间复杂度: O(m⋅n)
其中, m 为0的个数, n 为1的个数, k 为已知字符串数组的长度strs.size()
然后,我们又再次发现,其实我们把上一次的状态和这次的状态放在同一个数组中就可以了!因为更新时是从后往前的,要用到的上一次的值并没有受到影响,于是又有了如下解法
class Solution {
private:
vector<vector<int>> rec;
private:
//计算每个字符串有几个0和几个1
pair<int, int> countNums(string s){
int os = 0;
int zs = 0;
for (int i = 0; i < s.length(); i++){
if ('0' == s[i])
zs++;
}
os = s.length() - zs;
return make_pair(zs, os);
}
public:
int findMaxForm(vector<string>& strs, int m, int n) {
//设置一个二维数组来记录状态
rec = vector<vector<int>>(m + 1, vector<int>(n + 1, 0));
for (int count = 0; count < strs.size(); count++){
pair<int, int> p = countNums(strs[count]);
int level = count % 2;
for (int i = m; i >= p.first; i--){
for (int j = n; j >= p.second; j--){
rec[i][j] = max(rec[i][j], 1 + rec[i - p.first][j - p.second]);
}
}
}
return rec[m][n];
}
};
如果仅仅是针对问题本身的话,解法3自然是最理想的一种解法。但是,如果我们想知道这个最大长度的字符串组是由哪些字符串组成的又该怎么办呢?这个时候,就要用解法1记录了所有状态的数组逆推了~
下面给出的代码只能找到其中的一组解,并不能找到所有解,因为可能有很多种情况。找所有解的方法只需在这之上拓展一下即可,不过不要忽略了重复解的情况,这是一个难点~
class Solution {
private:
vector<vector<vector<int>>> rec;
int strsN;
private:
pair<int, int> countNums(string s){
int os = 0;
int zs = 0;
for (int i = 0; i < s.length(); i++){
if ('0' == s[i])
zs++;
}
os = s.length() - zs;
return make_pair(zs, os);
}
public:
void findMaxForm(vector<string>& strs, int m, int n) {
strsN = strs.size();
rec = vector<vector<vector<int>>>(m + 1, vector<vector<int>>(n + 1, vector<int>(strsN, 0)));
for (int count = 0; count < strsN; count++){
pair<int, int> p = countNums(strs[count]);
for (int i = m; i >= 0; i--){
for (int j = n; j >= 0; j--){
if (i >= p.first && j >= p.second)
rec[i][j][count] = (count == 0 ? 1 : max(rec[i][j][count - 1], 1 + rec[i - p.first][j - p.second][count - 1]));
else
rec[i][j][count] = (count == 0 ? 0 : rec[i][j][count - 1]);
}
}
}
}
vector<string> getOneSol(vector<string> strs, int m, int n){
//调用findMaxForm()把状态存到rec中
findMaxForm(strs, m, n);
vector<string> res;
int zs = m;
int os = n;
for (int i = strsN - 1; i >= 1; i--){
pair<int, int> p = countNums(strs[i]);
if (rec[zs][os][i] == rec[zs][os][i - 1])
continue;
else{
res.push_back(strs[i]);
zs -= p.first;
os -= p.second;
}
if (zs <= 0 && os <= 0)
break;
}
pair<int, int> p = countNums(strs[0]);
if (p.first <= zs && p.second <= os)
res.push_back(strs[0]);
return res;
}
};
很多动态规划的问题都可以演变成背包问题,因此掌握背包问题的本质是非常重要的。
如有不足,还请指正~