算法刷题打卡037 | 动态规划5

文章通过分析LeetCode上的三个问题(1049.最后一块石头的重量II,494.目标和,474.一和零)展示了动态规划和背包问题的解决思路。对于这类问题,关键在于将数组或集合分成两部分,并找到最佳分割策略,以达到特定的目标或最小化剩余量。动态规划的01背包思想在这些题目中得到了应用,通过维护一个dp数组来存储中间状态,从而求解最优解。
摘要由CSDN通过智能技术生成

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

题目链接:1049. 最后一块石头的重量 II - 力扣(Leetcode)

看题目首先想到的是将所有石头放到一个有序集合里,不断取出两块重量接近的石头两相抵消,剩余部分放入集合中继续重复“相撞”的操作,主要就是模拟题目描述的过程:

import queue
class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        s = queue.PriorityQueue()
        c = collections.Counter(stones)  # 去重(相同的两块石头优先一起粉碎,set中只加入奇数的数字)
        for st in stones:
            if c[st] % 2:
                s.put(-st)
        while s.qsize() > 1:
            max_1 = s.get()
            max_2 = s.get()
            if abs(max_2 - max_1) > 0:
                s.put(-abs(max_2 - max_1))
        if s.qsize() == 0:
            return 0
        else:
            return -s.get()

但这样模拟并不能解决问题,看似想用贪心,但并不能从局部最优推出全局最优。

运用动态规划01背包的思想,本质上就是“尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小”,和划分等和子集类似,但不强求划分为相等的两个集合。让两个集合尽可能接近,然后在这两个集合之间进行粉碎操作。这样最终剩余的石头重量就是和sum-2*dp[target](两个集合的差值):

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

LeetCode 494 目标和

题目链接:494. 目标和 - 力扣(Leetcode)

 看题目描述,其实也是在将数组分为两个集合(一个加正号,一个加负号),并且也是每个数只能用一次。不同的是这里求的是,给数值添加不同的符号之后,数组的和为target的数目。这就不能简单套用之前的dp数组定义方式,而是要将一维dp数组的含义dp[j]定义为装满容量为j的背包有多少种方法,同时还要重新考虑如何递推。递推公式有些类似于爬楼梯的方法数,倒序遍历容量j时,只要j能放下nums[i],dp[j-nums[i]]所包含的方法数就能累加到dp[j](累加是因为可以从不同的容量加上当前的nums[i],得到不同的方法)。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        int n = nums.size();
        for(int i=0;i<n;i++){sum += nums[i];}
        if (abs(target) > sum || (sum + target) % 2) return 0;
        int positive = (sum + target) / 2;  // 背包容量,加正号的部分数组
        vector<int> dp(positive+1);  //注意dp数组的含义是装满容量为j的背包有多少种方法
        dp[0] = 1;  //初始化
        for(int i=0; i<n; i++)
        {
            for (int j=positive; j>= nums[i]; j--)
            {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[positive];
    }
};

自己实现时容易忽略一些边界条件(target绝对值大于数组总和,或者sum+target不能被2整除),导致提交出错。总的时间复杂度是O(n*m),空间复杂度是O(m),n为数组长度,m为背包容量。

LeetCode 474 一和零 

题目链接:474. 一和零 - 力扣(Leetcode)

 这一题的解题思路也是类似的,每个字符串相当于物品,只是背包的维度变成了二维:

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        // dp[i][j]:最多包含i个0和j个1的最大子集长度
        vector<vector<int>> dp(m+1, vector<int>(n+1));
        // 初始化,dp全为0即可
        for(int i=0; i < strs.size(); i++){
            // 字符串0 1 计数
            int c0 = 0, c1 = 0;
            for(int x=0; x < strs[i].size(); x++)
            {
                if (strs[i][x] == '0') c0 += 1;
                else c1 += 1;
            }
            for(int j=m; j>=c0; j--)
            {
                for (int k=n; k>=c1; k--)
                {
                    dp[j][k] = max(dp[j][k], dp[j-c0][k-c1] + 1);
                }
            }
        }

        return dp[m][n];

    }
};

背包的两个维度没有先后顺序之分,只需要维度内部保证是倒序遍历。也可以多加一个判断条件,当c0或c1超出m、n的大小时就能跳过,不用进行后续遍历(放不下)。总的时间复杂度是O(N*k*m*n),其中N是字符串的数量,k是字符串的平均长度,空间复杂度是O(mn)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值