尽量详解 474.一和零 、377.组合总和IV

动态规划题的基本模板

private void dp(int m,int n){
        //1.状态:xxxx; 选择:xxxx
        //2.dp[i][j]=val,i表示xx,j表示xx,val表示xx
        
        //3.初始条件
        
        //状态转移
        for (状态一) {
            for (状态二){
                for(选择){
                //4.状态转移方程
                    dp[i][j] = ???
                }
            }
        }
    }

做动态规划题最关键也是最难得部分就是构造状态转移方程,在我做过不多的几十题基本上都符合上面这个的模板格式。

先前我认为某一状态当被遍历到了之后代入状态转移方程求得解就是最优解,所以我有一种错觉,好像每一个子问题可以通过状态转移方程经过一次计算就可以得到最优解。

但是,我今天做的两题动态规划题好像都有点不一样,子问题的最优解是多次比较、迭代的结果,而非“一步到位”。带着疑惑我就去找了找运筹学里关于状态转移的描述:

从边界条件开始,由后向前逐段递推寻找最优,在每一个阶段的计算中都要用到前一阶段最优结果,依次进行,求得最后一个子问题的最优解就是整个问题的最优解

好了,描述完(大概??)我的疑惑之后就可以把两道题拿出来瞅一瞅了。


1)377.组合总和IV

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
(请注意,顺序不同的序列被视作不同的组合)

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum-iv/

我以为这题最有意思的一点是“顺序不同的序列被视为不同的组合”,如果不考虑排序,那么这题就是一题典型的背包问题。

public static int combinationSum4(int[] nums, int target) {
    //1.status:数组索引下标;choise:是否将当前值加入组合、
    //2.dp
    int n = nums.length;
    int dp[][] = new int[n+1][target+1];//dp[i][j]=x,前i个数,和为j的组合数
    //3.base case
    //dp[0][j] = 0;
    for(int i = 0;i <= n;i++)
        dp[i][0] = 1;
    //4.transFunc
    for(int i = 1;i <= n;i++){
        for(int j = 1;j <= target;j++){
            if(nums[i-1] > j)
                dp[i][j] = dp[i-1][j];
            else{
                dp[i][j] = dp[i-1][j]+dp[i][j-nums[i-1]];
            }
        }
    }
    return dp[n][target];
}

如何满足顺序不同而组合不同的这个条件呢?

public int combinationSum4(int[] nums, int target) {
    //1.status:组合总和;choise:
    //2.dp
    int n = nums.length;
    int dp[] = new int[target+1];//dp[i]=x,和为j的组合数
    //3.base case
    dp[0] = 1;
    //4.transFunc
    for(int i = 0;i <= target;i++){
        for(int num : nums){
            if(i+num <= target)
                dp[i+num] += dp[i];
        }
    }
    return dp[target];
}

我试着尽力清楚的描述一下这个状态转移过程到底是干了些啥事,如果没描述清楚呢,你就当我没说好吧 : )
换一个思路,我们不需要一次就直接求出来某个总和的组合数,而是找到一个总和数j,然后我们依次在这个总和数的基础上再加数组中的每一个值得到一个更大的总和数k,因而这说明总和为k的组合又增加了一个,最后当所有的情况都遍历完了,解就出来了。

搭配上这个图来看可能会更清楚一些。
在这里插入图片描述


2)474.一和零

在计算机界中,我们总是追求用有限的资源获取最大的收益。

现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。

你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。

注意:

给定 0 和 1 的数量都不会超过 100。
给定字符串数组的长度不会超过 600。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/ones-and-zeroes

public int findMaxForm(String[] strs, int m, int n) {
    //status: ;choice

    //dp[j][k]=x,j个0,k个1,能拼出存在于数组中的字符串的最大数量
    int len = strs.length;
    int[][] dp = new int[m+1][n+1];
    //base case
    int[][] strs2 = new int[len][2];
    for(int i = 0;i < len;i++){
        for(int j = 0;j < strs[i].length();j++){
            if(strs[i].charAt(j) == '0') 
                strs2[i][0]++;
            else
                strs2[i][1]++;
        }
    }
    //transFunc
    for(int k = 0;k < len;k++){//选择
        for(int i = m;i >= strs2[k][0];i--){//状态一:0的个数
            for(int j = n;j >= strs2[k][1];j--){//状态二:1的个数

                dp[i][j] = Math.max(dp[i][j],dp[i-strs2[k][0]][j-strs2[k][1]]+1);
                
            }
        }
    }
    return dp[m][n];
}

我试着尽力清楚的描述一下这个状态转移过程到底是干了些啥事,如果没描述清楚呢,你就当我没说好吧 : )
从外往里看,最外面一层循环for(int k = 0;k < len;k++)是数组中的字符串的依次选择,再往里两层循环是状态的转移,而我之前遇到的基本都是外层循环是状态转移,内层是选择,这就是我觉得这题比较有意思的地方(害,主要还是我题做的少)
先看状态转移方程dp[i][j] = Math.max(dp[i][j],dp[i-strs2[k][0]][j-strs2[k][1]]+1);,在对于下标为k的字符串,dp[i][j]的值有两种可能的取值:
①当前字符串不选择,也就是延续原最大值,
②选择当前字符串,减去当前字符串中0和1的个数后,再加上而剩下0和1的个数时到能拼出存在于数组中的字符串的最大数量。
最终当0的个数为i,1的个数为j时能拼出存在于数组中的字符串的最大数量时①②两种情况下最大的那一种。但是奥,字符串怎么依次遍历呢?什么时候遍历呢?我不管,我就偏偏把遍历字符串数组放到状态选择的循环体里,那么问题来了,情况②倒还是一样好计算,但情况①怎么表示?我哪知道原来最大值的dp[?][?]是谁跟谁呢!
所以,字符串数组的遍历在最外层循环,那么就表示为每一次我都可以更新一下dp[i][j],最终当我遍历完字符串数组之后,最后得到的dp数组里存的就是我要找的问题的最优解,而情况①里原最大值不就是它上一次迭代后在dp[i][j]里存的值嘛!
别急,还有一个地方没有讲清楚,状态ij怎么安排遍历的顺序,再观察状态转移方程dp[i-strs2[k][0]][j-strs2[k][1]],再看看定义“在每一个阶段的计算中都要用到前一阶段最优结果”,我们在更新最优解的过程中要保留前一段最优结果,而前一段最优结果的i=i-strs2[k][0]j=j-strs2[k][1],所以我们需要在遍历状态的时候从i=mj=n最大位置开始,依次减小,依次更新dp数组。

当然啦,这题还有一个更好理解的版本

public int findMaxForm(String[] strs, int m, int n) {
    //status: ;choice

    //dp[i][j][k]=x,前i个字符串,j个0,k个1,能拼出存在于数组中的字符串的最大数量
    int len = strs.length;
    int[][][] dp = new int[len+1][m+1][n+1];
    //base case
    int[][] strs2 = new int[len][2];
    for(int i = 0;i < len;i++){
        for(int j = 0;j < strs[i].length();j++){
            if(strs[i].charAt(j) == '0')
                strs2[i][0]++;
            else
                strs2[i][1]++;
        }
    }
    //transFunc
    for(int i = 1;i <= len;i++){
        for(int j = 0;j <= m;j++){
            for(int k = 0;k <= n;k++){
                if(j - strs2[i-1][0] >= 0 && k - strs2[i-1][1] >= 0){
                    dp[i][j][k] = Math.max(dp[i-1][j-strs2[i-1][0]][k - strs2[i-1][1]] + 1,dp[i-1][j][k]);
                }else{
                    dp[i][j][k] = dp[i-1][j][k];
                }
            }
        }
    }
    return dp[len][m][n];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值