问题来源
此题来源于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: 4Explanation: 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: 2Explanation: You could form "10", but then you'd have nothing left. Better form "0" and "1".
解决方案
这是一个非常典型的二维0/1背包问题,相当于是在问我们有一个背包的空间大小为m,最大载重为n,给定k个物品,已知每个物品的大小和重量,试问最多能放进多少个物品(每个物品只能放一次)。
该问题的状态方程为
f(m,n,k)=max(f(m,n,k−1),1+f(m−i,n−j,k−1))f(m,n,k) = max(f(m,n,k-1),1+f(m-i,n-j,k-1)) f(m,n,k)=max(f(m,n,k−1),1+f(m−i,n−j,k−1))
f(m,n,k)f(m,n,k)f(m,n,k)是指在限制为(m,n)(m,n)(m,n)的情况下,考虑前kkk个字符串所能得到的最多字符串的个数。
这个式子的意思是,我们从放第1个字符串开始考虑,直到第k个字符串,如果在限制为(m,n)(m,n)(m,n)的情况下,放这个字符串进去比不放这个字符串得到的个数要多,那么我们就放这个字符串进去,否则不放。
如果想直接看动态规划在这里最简洁的解法,请直接跳到解法3,有耐心的话就一步步看下去吧。
解法1
时间复杂度:O(m⋅n⋅k)O(m \cdot n \cdot k)O(m⋅n⋅k)
空间复杂度:O(m⋅n⋅k)O(m \cdot n \cdot k)O(m⋅n⋅k)
其中,mmm为0的个数, nnn为1的个数,kkk为已知字符串数组的长度strs.size()
这种解法虽然很浪费空间,但是保存了每种情况的状态,只有在这种情况下,我们才能逆推出这个最大长度是由哪些字符串组成的。
class Solution {
private:vector<vector<vector<int>>> rec;int strsN;private://计算每个字符串有几个0和几个1pair<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]));elserec[i][j][count] = (count == 0 ? 0 : rec[i][j][count - 1]);}}}return rec[m][n][strsN - 1];}
};
解法2
时间复杂度:O(m⋅n⋅k)O(m \cdot n \cdot k)O(m⋅n⋅k)
空间复杂度:O(m⋅n⋅2)O(m \cdot n \cdot 2)O(m⋅n⋅2),即O(m⋅n)O(m \cdot n)O(m⋅n)
其中,mmm为0的个数, nnn为1的个数,kkk为已知字符串数组的长度strs.size()
然后,我们发现其实每次更新状态kkk时仅仅用到了上一次的状态k−1k-1k−1,所以我们可以将存储状态的数组降成m⋅n⋅2m \cdot n \cdot 2m⋅n⋅2的大小。
class Solution {
private:vector<vector<vector<int>>> rec;private://计算每个字符串有几个0和几个1pair<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]);elserec[i][j][1] = max(rec[i][j][0], 1 + rec[i - p.first][j - p.second][0]);elseif (0 == level)rec[i][j][0] = rec[i][j][1];elserec[i][j][1] = rec[i][j][0];}}}return max(rec[m][n][0], rec[m][n][1]);}
};
解法3
时间复杂度:O(m⋅n⋅k)O(m \cdot n \cdot k)O(m⋅n⋅k)
空间复杂度:O(m⋅n)O(m \cdot n)O(m⋅n)
其中,mmm为0的个数, nnn为1的个数,kkk为已知字符串数组的长度strs.size()
然后,我们又再次发现,其实我们把上一次的状态和这次的状态放在同一个数组中就可以了!因为更新时是从后往前的,要用到的上一次的值并没有受到影响,于是又有了如下解法
class Solution {
private:vector<vector<int>> rec;private://计算每个字符串有几个0和几个1pair<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]));elserec[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;}
};
结束语
很多动态规划的问题都可以演变成背包问题,因此掌握背包问题的本质是非常重要的。
如有不足,还请指正~