[01背包] 474. 一和零

139 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

每日刷题 2022.06.05

题目

  • 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
  • 请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
  • 如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例

  • 示例1
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5031 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 41 ,大于 n 的值 3
  • 示例2
输入: strs = ["10", "0", "1"], m = 1, n = 1
输出: 2
解释: 最大的子集是 {"0", "1"} ,所以答案是 2

提示

1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i] 仅由 '0' 和 '1' 组成
1 <= m, n <= 100

解题思路

  • 01背包的解题思路很像,主要在转换思路上
  • 01背包不同的是,本题不能再是j - 1这样的写法,因为在本题中每个字符串都有自己独有的0和1的个数,因此再往回查找状态的时候,应该减去当前字符串strs[i]中的1和0的个数。
  • 因此状态转移方程:dp[i][j][z] = dp[i - 1][j][z] + dp[i - 1][j - lin][z - yi],其中lin表示strs[i]中的0的个数,yi表示strs[i]中的1的个数。
  • 状态转移方程式含义:对于第i个字符串,要求j个0和z个1的情况下,能够得到的最大子集的长度。
  • 接下来就需要考虑初始化的边界问题:
    • 既然不再是减1,假设对于当前i = 1,我们不能再确定其是否会越界,如果此时strs[1]的字符串中有3个1,但是当前的j = 1,那么数组就会出现越界的问题。此时就可以考虑下,为什么会越界,是因为strs[1]中的1超过了我们要选择的1的个数。
    • 因此不能选择strs[1], dp[i][j][z] = dp[i - 1][j][z];
  • 初始化:dp[0][0][z]dp[0][j][0]这两种情况,都应该为0,因为此时没有可以选择的strs[i].

问题

  • 为什么三维数组的三层长度,分别是len + 1 \ m + 1 \ n + 1,因为对于每一个字符串都需要考虑到,而我们的状态转移方程式中当前的状态是依赖于前一个状态的,因此需要设置为len + 1
  • m + 1: 是因为最终需要的是m0n1,为了能取到m和n

AC代码

/**
 * @param {string[]} strs
 * @param {number} m
 * @param {number} n
 * @return {number}
 */
var findMaxForm = function(strs, m, n) {
  const len = strs.length;
  // 创建一个三维数组
  let dp = new Array(len + 1).fill(0).map(() => new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0)));
  // 因为什么东西都没有,因此开始的0行全部赋为0
  let yi = 0,lin = 0;
  for(let i = 1; i <= len; i++) {
    yi = 0,lin = 0;
    // 统计当前的字符串中1和0的个数
    let arr = strs[i - 1].split('');
    arr.forEach(ele => {
      if(ele == 1) yi++;
      else if(ele == 0) lin++;
    });

    // dp
    for(let j = 0; j <= m; j++) {
      for(let z = 0; z <= n; z++) {
        // m = 0,n = 1
        // 不选的情况:当前字符串中的1大于要求的1的个数 或者 当前字符串中的0大于要求的0的个数
        if(j < lin || z < yi) dp[i][j][z] = dp[i - 1][j][z];
        // 选择:符合要求的0和1的个数
        if(j - lin >= 0 && z - yi >= 0)
        dp[i][j][z] = Math.max(dp[i - 1][j][z], dp[i - 1][j - lin][z - yi] + 1);
      }
    }
  }
  return dp[len][m][n];
};

优化后的代码

  • 问题:为什么优化后,不需要写不选的情况了呢?

优化前:

image.png

优化后

  • 去掉一层循环后,需要将循环的区间进行转换,当前所利用的数组就是上一层得到的结果,因此无需再考虑不选的情况
  • 对比:那么没有优化的时候,为什么不能不考虑不选的情况呢?因为不选的时候,不加入不选的情况,那么下下一层的结果就会被影响。 image.png

AC代码

/**
 * @param {string[]} strs
 * @param {number} m
 * @param {number} n
 * @return {number}
 */
var findMaxForm = function(strs, m, n) {
  const len = strs.length;
  // 创建二维数组
  let dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
  for(let k = 0; k < len; k++) {
    let lin = 0, yi = 0;
    // 查找当前字符串中0和1的个数
    strs[k].split('').forEach(ele => {
      if(ele == 0) lin++;
      else if(ele == 1) yi++; 
    });
    // dp
    for(let i = m; i >= lin; i--) {
      for(let j = n; j >= yi; j--) {
        dp[i][j] = Math.max(dp[i][j], dp[i - lin][j - yi] + 1);
      }
    }
  }
  return dp[m][n];
};