Skip to content

一和零

约 1553 字大约 5 分钟

动态规划背包问题

2025-02-26

474. 一和零

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 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

动态规划-数组递推-背包问题

先遍历物品,再遍历容量。物品有一种,容量有两种。

背包问题,经典的背包问题可以使用二维动态规划求解,两个维度分别是物品和容量。这道题有两种容量, 因此需要使用三维动态规划求解,三个维度分别是字符串、0 的容量和 1 的容量。

dp[i][j][k] 表示在前 i 个字符串中,使用 j 个 '0' 和 k 个 '1' 的最大子集大小。 遍历每个字符串,计算当前字符串中的 '0' 和 '1' 的个数,三重循环遍历每个容量,分别计算选择当前字符串和不选择当前字符串的情况。

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int length = strs.length;
        // dp[i][j][k] 表示在前 i 个字符串中,使用 j 个 '0' 和 k 个 '1' 的最大子集大小。i、j、k指的都是个数而不是索引。
        int[][][] dp = new int[length + 1][m + 1][n + 1];

        // 遍历字符串的数量,即物品,dp[0][j][k]取默认值0。注意dp[0][0][0]=0而不是1。空集的大小(即元素个数)是0,个数是1,这里计算的是大小。
        for (int i = 1; i <= length; i++) { 
            // 计算当前字符串中的 '0' 和 '1' 的个数
            int[] zerosOnes = getZerosOnes(strs[i - 1]);
            int zeros = zerosOnes[0], ones = zerosOnes[1];

            // 遍历 '0' 的数量,即容量
            for (int j = 0; j <= m; j++) {
                // 遍历 '1' 的数量,即容量
                for (int k = 0; k <= n; k++) {
                    // 不选当前字符串的情况
                    dp[i][j][k] = dp[i - 1][j][k];
                    // 选当前字符串的情况
                    if (j >= zeros && k >= ones) {
                        dp[i][j][k] = Math.max(dp[i][j][k], dp[i - 1][j - zeros][k - ones] + 1);
                    }
                }
            }
        }
        
        // 返回前 length 个字符串中,使用 m 个 '0' 和 n 个 '1' 的最大子集大小(即字符串的个数)
        return dp[length][m][n];
    }

    // 计算字符串中 '0' 和 '1' 的个数
    public int[] getZerosOnes(String str) {
        int[] zerosOnes = new int[2];
        int length = str.length();
        
        // 遍历字符串,计算 '0' 和 '1' 的个数
        for (int i = 0; i < length; i++) {
            zerosOnes[str.charAt(i) - '0']++;
        }
        
        return zerosOnes;
    }
}

背包问题-滚动更新-去掉物品维度。

正序遍历字符串数组,倒序遍历0和1的个数。dp[j][k] 表示最多有 j 个 '0' 和 k 个 '1' 的情况下, 最多可以包含的字符串数量。 如果我们正序遍历,我们会在计算 dp[j][k] 前更新 dp[j - zeros][k - ones], 从而导致计算dp[j][k]时使用了当前 i 轮次的新值而不是上轮次 i 的旧值,这会导致错误的结果。一个新的 i 轮次代表用新的dp矩阵覆盖旧的dp矩阵。

实现时,内层循环需采用倒序遍历的方式,这种方式保证转移来的是 dp[i−1][][] 中的元素值。

在更新 dp[j][k] 时,新的值 dp[j][k] 依赖于前一个 i 轮次的状态 dp[j - zeros][k - ones]

如果我们正序遍历,我们会在计算 dp[j][k] 前更新 dp[j - zeros][k - ones], 从而导致计算dp[j][k]时使用了当前 i 轮次的新值而不是上轮次 i 的旧值,这会导致错误的结果。一个新的 i 轮次代表用新的dp矩阵覆盖旧的dp矩阵。

只有在dp[i][j]依赖于dp[i-1][j+1]时才需要prev。即一个维度减,一个维度增。

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        // dp[j][k] 表示最多有 j 个 '0' 和 k 个 '1' 的情况下,最多可以包含的字符串数量
        int[][] dp = new int[m + 1][n + 1];
        int length = strs.length;
        
        // 遍历每个字符串。可以去掉dp数组的第i维,但不能去掉i循环。
        for (int i = 0; i < length; i++) {
            // 计算当前字符串中的 '0' 和 '1' 的个数
            int[] zerosOnes = getZerosOnes(strs[i]);
            int zeros = zerosOnes[0], ones = zerosOnes[1];
            
            // 倒序遍历 '0' 的数量,防止重复使用
            for (int j = m; j >= zeros; j--) {
                // 倒序遍历 '1' 的数量,防止重复使用
                for (int k = n; k >= ones; k--) {
                    // 更新 dp 数组,选择当前字符串
                    dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1);
                }
            }
        }
        
        // 返回最多可以包含的字符串数量
        return dp[m][n];
    }

    // 计算字符串中 '0' 和 '1' 的个数
    public int[] getZerosOnes(String str) {
        int[] zerosOnes = new int[2];
        int length = str.length();
        
        // 遍历字符串,计算 '0' 和 '1' 的个数
        for (int i = 0; i < length; i++) {
            zerosOnes[str.charAt(i) - '0']++;
        }
        
        return zerosOnes;
    }
}
  • 时间复杂度:O(lmn+L)O(lmn+L) ,其中 ll 是数组 strs 的长度, mmnn 分别是 0 和 1 的容量, LL 是数组 strs 中的所有字符串的长度之和。

    动态规划需要计算的状态总数是 O(lmn)O(lmn) ,每个状态的值需要 O(1)O(1) 的时间计算。

    对于数组 strs 中的每个字符串,都要遍历字符串得到其中的 0 和 1 的数量,因此需要 O(L)O(L)的时间遍历所有的字符串。总时间复杂度是 O(lmn+L)O(l m n+L)​ 。

  • 空间复杂度: O(mn)O(m n) ,其中 mmnn 分别是 0 和 1 的容量。使用空间优化的实现,需要创建 m+1m+1n+1n+1 列的二维数组 dpd p