算法学习 | day37/60 最后一块石头的重量 II/目标和/一和零

        今天主要是一些01的应用,从不同的层面:

        经典的 01 背包问题:装满背包最大的价值为多少;

        分割等和子集:判断能否装满;

        最后的石头:能装的最大质量;

        目标和:有多少种方法可以装满;

        一和零:装满最多有多少个物品;

        展示了01背包的不同类型。

一、题目打卡

        1.1 最后一块石头的重量 II

        题目链接:. - 力扣(LeetCode)

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int weightSum = accumulate(stones.begin(), stones.end(), 0);
        // dp[i] 这里的含义是容量为 i 的背包,最多可以承受的重量/价值
        vector<int> dp(1501,0); //石头最重的情况是100,最长为30,只用开辟他的一半

        for(int i = 0 ; i < stones.size();i++){
            for(int j = weightSum/2 ; j >= stones[i] ; j--){
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return weightSum - 2 * dp[weightSum/2];
    }
};

        这个重点在于理解题意,只要能明白最终题目需要做到的事情是分割所有的石头成两堆,让两边尽可能相等这一点,就和之前写的题目联系起来了。

        1.2 目标和

        题目链接:. - 力扣(LeetCode)

        做这个题目之前我先贴几个现在看到的 01 背包的题目类型:

        经典的 01 背包问题:装满背包最大的价值为多少;

        分割等和子集:判断能否装满;

        最后的石头:能装的最大质量;

        目标和:有多少种方法可以装满;

        一和零:装满最多有多少个物品;

        关于这个题,看到了一个比较好的评论解析:

        这里,我们可以把状态定义为dp【i】【j】,表示用数组中前i个元素组成和为j的方案数。那么状态转移方程就是:

dp【i】【j】 = dp【i-1】[j-nums【i】] + dp【i-1】[j+nums【i】]

这个方程的意思是,如果我们要用前i个元素组成和为j的方案数,那么有两种选择:第i个元素取正号或者取负号。如果取正号,那么前i-1个元素就要组成和为j-nums【i】的方案数;如果取负号,那么前i-1个元素就要组成和为j+nums【i】的方案数。所以两种选择的方案数相加就是dp【i】【j】。

但是这样定义状态会导致空间复杂度过高,因为我们需要一个二维数组来存储所有可能的状态。所以我们可以对问题进行一些变换,把它转化为一个背包问题。

我们可以把数组中所有取正号的元素看作一个集合P,所有取负号的元素看作一个集合N。那么有:

sum(P) - sum(N) = target

sum(P) + sum(N) = sum(nums)

两式相加得:

sum(P) = (target + sum(nums)) / 2

也就是说,我们只需要找到有多少种方法可以从数组中选出若干个元素使得它们的和等于(target + sum(nums)) / 2即可。这就变成了一个经典的01背包问题。

所以我们可以把状态定义为dp【j】,表示用数组中若干个元素组成和为j的方案数。那么状态转移方程就是:

dp【j】 = dp【j】 + dp[j - nums【i】]

这个方程的意思是,如果我们要用若干个元素组成和为j的方案数,那么有两种选择:不选第i个元素或者选第i个元素。如果不选第i个元素,那么原来已经有多少种方案数就不变;如果选第i个元素,那么剩下要组成和为j - nums【i】 的方案数就等于dp[j - nums【i】]。所以两种选择相加就是dp【j】。

但是在实现这个状态转移方程时,有一个细节需要注意:由于每次更新dp【j】都依赖于之前计算过得dp值(也就是说当前行依赖于上一行),所以我们必须从后往前遍历更新dp值(也就是说从右往左更新),否则会覆盖掉之前需要用到得值。

最后返回dp【bag_size】即可。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = accumulate(nums.begin(), nums.end(),0);
        
        // 这个情况是需要考虑的
        if(sum < abs(target)) return 0;

        // 定义最终取的和的集合为 left
        if((target + sum) % 2) return 0; // 这种情况是不存在结果的
        int left = (target + sum) / 2;

        // 此时问题就转换为了,使用 nums 装满和为 left 的背包,一共有多少种方法
        vector<int> dp(left + 1,0); // 状态的定义为,当背包的容量为 i 时,也就是选择 i 个数字,总共有 dp[i] 种方法可以得到 target

        dp[0] = 1; // 装满容量为 0 的背包,需要初始化有一种方法

        for(int i = 0; i < nums.size(); i++){
            for(int j = left; j >= nums[i];j--){
                dp[j] = dp[j] + dp[j - nums[i]];
            }
        }
        return dp[left];
    }
};

        有了评论的这个解析,理解这个状态转移方程就容易一点,这个题在初始化之前,也需要进行处理,否则如果left为负数,初始化就会失败。

        1.3 一和零

        题目链接:. - 力扣(LeetCode)

        上来看题目的时候,先思考一下题目之中涉及的变量有哪些,然后就考虑设计dp数组的维度,这个题目涉及的变量主要是,多少个0,多少个1,以及最后选择进入背包的数量, 相比较之前,也就是在背包的限制上,增加的一个维度。

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        // 遍历的过程中由于是从后向前遍历的,而且是max,而且没有负数,所以初始化为0
        vector<vector<int>> dp(m+1,vector<int>(n+1,0));

        for(int num = 0 ; num < strs.size();num++){
            int m_ = 0;
            int n_ = 0;
            for(char c: strs[num]){
                if(c == '0') m_++;
                else n_++;
            }
            // dp[m_][n_] = max(dp[m_][n_], dp[m - m_][n - n_] + 1);

            for(int j = m ; j >= m_ ; j--){
                for(int k = n ; k >= n_ ; k--){
                    dp[j][k] = max(dp[j][k], dp[j - m_][k - n_] + 1);
                }
            }
        }
        // for(auto & i : dp){
        //     for(auto& j :i){
        //         cout << j << " ";
        //     }
        //     cout <<endl;
        // }
        return dp[m][n];

    }
};

        还是看了视频以后自己做的,最后在递推公式那里,我也不知道是哪里理解的不对,但是我一开始做01背包好像就会犯这个错,就是在选择这个物品的情况下,我会把 dp[j - m_][k - n_] 的 j 和 k 写成 m 和 n,我也不知道我后面的思维是什么,当个坑避开一下吧。

  • 23
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值