LeetCode 474. Ones and Zeroes 动态规划解法+拓展

问题来源

此题来源于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个物品,已知每个物品的大小和重量,试问最多能放进多少个物品(每个物品只能放一次)。
该问题的状态方程为

f ( m , n , k ) = m a x ( 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,k1),1+f(mi,nj,k1))

f ( m , n , k ) f(m,n,k) f(m,n,k)是指在限制为 ( m , n ) (m,n) (m,n)的情况下,考虑前 k k k个字符串所能得到的最多字符串的个数。
这个式子的意思是,我们从放第1个字符串开始考虑,直到第k个字符串,如果在限制为 ( m , n ) (m,n) (m,n)的情况下,放这个字符串进去比不放这个字符串得到的个数要多,那么我们就放这个字符串进去,否则不放。
如果想直接看动态规划在这里最简洁的解法,请直接跳到解法3,有耐心的话就一步步看下去吧。

解法1

时间复杂度: O ( m ⋅ n ⋅ k ) O(m \cdot n \cdot k) O(mnk)
空间复杂度: O ( m ⋅ n ⋅ k ) O(m \cdot n \cdot k) O(mnk)
其中, m m m为0的个数, n n n为1的个数, k k 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];
	}
};

解法2

时间复杂度: O ( m ⋅ n ⋅ k ) O(m \cdot n \cdot k) O(mnk)
空间复杂度: O ( m ⋅ n ⋅ 2 ) O(m \cdot n \cdot 2) O(mn2),即 O ( m ⋅ n ) O(m \cdot n) O(mn)
其中, m m m为0的个数, n n n为1的个数, k k k为已知字符串数组的长度strs.size()
然后,我们发现其实每次更新状态 k k k时仅仅用到了上一次的状态 k − 1 k-1 k1,所以我们可以将存储状态的数组降成 m ⋅ n ⋅ 2 m \cdot n \cdot 2 mn2的大小。

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]);
    }
};

解法3

时间复杂度: O ( m ⋅ n ⋅ k ) O(m \cdot n \cdot k) O(mnk)
空间复杂度: O ( m ⋅ n ) O(m \cdot n) O(mn)
其中, m m m为0的个数, n n n为1的个数, k k 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;
	}
};

结束语

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七元权

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值