代码随想录算法训练营第四十三天|1049. 最后一块石头的重量 II、494. 目标和、474.一和零

LeetCode 1049. 最后一块石头的重量 II

链接:1049. 最后一块石头的重量 II

思路:

416. 分割等和子集的思路完全一样,也是需要找到两个和相等的子集,这样的话最后一块石头重量为0,如果没有找到和相等的子集,则需要尽量平衡两个子集的和的大小,使得剩下的最后一块石头重量最小。

还是01背包的做法,递推公式不变,先遍历物品再倒序遍历背包,最后所得的dp[target]一定是最接近target的,如果dp[target]正好等于sum的一半,则说明数组正好被分成两个和相等的子集,最后一块石头重量为0,否则的话最后一块石头重量等于两倍的dp[target]减去sum的重量。

代码:

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for (int i = 0; i < stones.size(); i++)
            sum += stones[i];
        int target = sum / 2;
        vector<int> dp(target + 1, 0);
        for (int i = 0; i < stones.size(); i++)
            for (int j = target; j >= stones[i]; j--)
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
        return abs(dp[target] * 2 - sum);
    }
};

LeetCode 494. 目标和

链接:494. 目标和

回溯法

思路:

这道题目需要利用数组里的元素构造表达式使得和等于target,表达式仅限于使用加法和减法。既然只有两种符号,且不用考虑计算的先后顺序(如乘法的优先级,括号之类的),所以可以用暴力搜索的办法把所有的组合都搜一遍,把符合条件的组合累加起来。

暴力搜索的办法就是使用回溯法。从idx开始遍历数组,首先尝试使用加法,然后递归调用,因为数组中的元素不能取重复值,所以每次递归的时候需要idx + 1,然后回溯,再尝试用减法。当sum等于target并且idx在数组的末端,说明当前组合符合目标。

代码:

class Solution {
public:
    int count = 0;
    int findTargetSumWays(vector<int>& nums, int target) {
        backtracking(nums, 0, target, 0);
        return count;
    }
    void backtracking(vector<int>& nums, int sum, int target, int idx)
    {
        if (idx == nums.size() && sum == target)
        {
            count++;
            return;
        }
        else if (idx == nums.size())
            return;

        sum += nums[idx];
        backtracking(nums, sum, target, idx + 1);
        sum -= nums[idx];

        sum -= nums[idx];
        backtracking(nums, sum, target, idx + 1);
        sum += nums[idx];
    }

};

动态规划

思路:

但其实回溯的方法是非常费时间的,而且由于我们不需要把每次符合条件的组合记录下来,所以可以使用动态规划。这里需要一点计算,假设数组所有元素的和为sum,把数组分为两部分left和right,一定有left + right = sum。这个时候如果left有一个值,使得left - right = S,那么说明只要构造出一部分元素和等于left的表达式就可以符合要求。所以这就变成了一个01背包问题,就是利用数组的元素,寻找能够恰好装满背包大小为left的方法种类。

首先确认背包大小left,left = (S + sum) / 2,具体数学证明就不说了,稍微思考以下就可以知道。然后S + sum必须能够被2整除,否则计算出来的left不是一个整数,这说明无论如何构造表达式也不可能凑到S。然后定义下标:dp[i]表示有多少种方法正好放满大小为i的背包。接下来需要确认递推公式,本题的递推公式和96. 不同的二叉搜索树十分类似,都是需要构造一种组合,然后问有多少种不同的构造方法。当我们需要求dp[j]的时候,构造背包为j的组合数量就等同于构造背包大小为j-nums[i],然后遍历nums的组合数量之和。所以dp[j] += dp[j-nums[i]]。

dp[j-nums[i]]初始化数组dp[0] = 1。表示凑到和为0的表达式有一种。为什么可以这样初始化?因为nums都是非负数,而想要凑到和为0,nums里的元素一定都是0,但是在遍历nums数组之前,我们并不知道究竟有多少个元素在里面,但是可以肯定至少有1个0,否则是无法凑到0的。然后遍历nums的时候,根据递推公式dp[j] += dp[j-nums[i]],就会逐渐累加凑成0的组合数量了。

代码:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = 0;
        for (int i =  0; i < nums.size(); i++)
            sum += nums[i];
        // 如果和小于S的绝对值,则无论怎么加都不可能到S
        if (sum < abs(S))
            return 0;
        // 如果S和sum的和没办法被2整除,也不能到S
        if ((S + sum) % 2 == 1)
            return 0;
        int target = (S + sum) / 2;
        // dp数组
        vector<int> dp(target + 1, 0);
        // 确认下标,dp[i]表示有多少种方法正好放满大小为i的背包
        // dp[0] 初始化为1,因为放满大小为0的背包的方法有1种
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++)
            for (int j = target; j >= nums[i]; j--)
                // 递推公式
                dp[j] += dp[j - nums[i]];
        return dp[target];
    }
};

LeetCode 474.一和零

链接:474.一和零

思路:

其实仔细思考下,就可以发现这道题目完全就是二维版的01背包,和一维的01背包不同的是,一维01背包只有背包大小这一个维度,而这道题目背包大小相当于有两个维度,分别为m和n,所以这就告诉了我们在遍历背包的时候,一定要两个维度都要遍历。

首先还是先定义下标:dp[j][k]表示背包大小为j个0和k个1时能装的最大子集,然后都初始化为0,因为背包大小为0的时候是装不下任何子集的。因为还是用滚动数组的办法,先遍历物品再遍历背包,这里先遍历m或是n都是一样的。每次遍历物品时,计算0和1的数量,然后再遍历背包的时候尝试把物品放进去,递推公式dp[j][k] = max(dp[j][k], dp[j-countZero][k-countOne] + 1),表示是否放strs[i]进入背包。

代码:

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        // 01背包,背包大小有m和n两个维度
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        // 定义下标:dp[j][k]表示背包大小为j个0和k个1时能装的最大子集
        for (int i = 0; i < strs.size(); i++)
        {
            int countZero = 0, countOne = 0;
            for (char str:strs[i])
            {
                if (str == '0')
                    countZero++;
                else
                    countOne++;
            }
            for (int j = m; j >=countZero; j--)
                for (int k = n; k >= countOne; k--)
                    dp[j][k] = max(dp[j][k], dp[j-countZero][k-countOne] + 1);

        }
        return dp[m][n];
            
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值