问题来源

此题来源于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;}
};

结束语

很多动态规划的问题都可以演变成背包问题,因此掌握背包问题的本质是非常重要的。
如有不足,还请指正~

LeetCode 474. Ones and Zeroes 动态规划解法+拓展相关推荐

  1. leetcode 474. Ones and Zeroes | 474. 一和零(双约束背包问题)

    题目 https://leetcode.com/problems/ones-and-zeroes/ 题解 这是一个双约束的背包问题.此题需要的前置知识,可以参考我之前的博客:算法设计与分析 0-1背包 ...

  2. leetcode 64. 最小路径和(递归 / 动态规划解法图解)(Java版)

    题目 leetcode 64. 最小路径和 提示: m == grid.length n == grid[i].length 1 <= m, n <= 200 0 <= grid[i ...

  3. leetcode 53. 最大子序和 动态规划解法、贪心法以及二分法

    题目 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和. 示例: 输入: [-2,1,-3,4,-1,2,1,-5,4] 输出: 6 解释: 连续子 ...

  4. LeetCode 172. Factorial Trailing Zeroes

    LeetCode 172. Factorial Trailing Zeroes 问题来源LeetCode 172. Factorial Trailing Zeroes 问题描述 Given an in ...

  5. [东哥的leetcode刷题日记] leetcode 283 : Move Zeroes

    leetcode 283 : Move Zeroes 题目链接: https://leetcode-cn.com/problems/move-zeroes/ 难度: 简单 归类 : 数组操作 题目: ...

  6. 5. 最长回文子串——暴力法---动态规划解法---扩展中心法

    暴力法 动态规划解法 class Solution {public String longestPalindrome(String s) {if (s == null) return null;cha ...

  7. 八十五、Python | Leetcode数据结构之图和动态规划算法系列

    @Author:Runsen @Date:2020/7/7 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...

  8. matlab多种分配方案_基于MATLAB的水资源优化分配问题动态规划解法

    基于 MATLAB 的水资源优化分配问题动态规划解法 摘要:介绍了动态规划的基本原理,针对水资源分配问题进行了 动态规划方法分析.针对具体问题采用逆序解法的表格法进行了计 算,然后用 matlab 编 ...

  9. MDP动态规划解法(三)

    上一篇我们已经说到了,增强学习的目的就是求解马尔可夫决策过程(MDP)的最优策略,使其在任意初始状态下,都能获得最大的Vπ值.(本文不考虑非马尔可夫环境和不完全可观测马尔可夫决策过程(POMDP)中的 ...

最新文章

  1. xshell复制粘贴
  2. 一篇文章带你从认识Python装饰器到熟练使用
  3. 很好的一篇讲LTP在编解码中的作用的文章
  4. linux驱动之I2C
  5. php编译时出错make: *** [libphp5.la] Error 1
  6. Hyper-V 2016 系列教程34 在局域网内架设Windows时间服务器
  7. C# .NET 使用DotNetZip开源类库 处理 压缩/解压 Zip 处理乱码情况
  8. opencv-python 鼠标事件和坐标点截图
  9. 【C/C++】成员变量的初始化顺序
  10. android studio导入android studio工程
  11. Oracle 数据库导出(exp)导入(imp)说明
  12. 窗方法原理之矩形窗及汉明窗
  13. 安装JDK11并配置环境变量(附百度网盘下载地址)
  14. 计算机毕业设计基于Android的计算器app设计
  15. 解决训练时显存不断增大问题
  16. please configure web facet first
  17. STM8/STM32 SPI模式的MAX7456代码
  18. 5G MEC场景下用户体验驱动的视频加速方案
  19. 计算机械产量定额,机械台班产量定额的计算方式
  20. python爱情动画_人生苦短,我用Python-从Houdini里导出RBD解算的Skin动画

热门文章

  1. mysql5.7.11解压版安装_Mysql5.7.11在windows10上的安装与配置(解压版)
  2. activiti 批量 mysql_Activiti6系列(3)- 快速体验
  3. python 数字转化excel行列_Python实现excel的列名称转数字、26进制(A-Z)与10进制互相转换...
  4. 访问对象的属性和方法
  5. php在html里面的位置,关于script在html中的摆放位置解析
  6. 下载java后缀的文件闪退_关于jarfile 打开闪退问题
  7. 米家对讲机_对前面两代产品不断总结和完善的产物,米家对讲机2代开箱体验...
  8. 二叉树面试题:判断树是否为完全二叉树和求二叉树的镜像
  9. layer ajax 用法,layer加载遮罩层使用 Ajax Loading Demo
  10. 字符设备驱动高级篇5——静态映射表、动态映射结构体方式操作寄存器