leedcode 算法刷题第三十一天

发布于:2025-09-11 ⋅ 阅读:(15) ⋅ 点赞:(0)

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

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

示例 1:

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

示例 2:

输入:stones = [31,26,33,21,40]
输出:5

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 100
class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        vector<int> dp(150001,0);
        int sum = 0;
        for(int i=0;i<stones.size();i++){
            sum+=stones[i];
        }
        int target = sum/2;
        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 sum-dp[target]-dp[target];
    }
};

解题思路:

问题背景

问题描述:有一堆石头,每次从中挑选两块石头碰撞,碰撞后会得到一块新石头(重量为两块石头的差值),直到只剩下一块石头或没有石头。求最后可能剩下的最小石头重量。

核心思路转化

这个问题可以转化为:

  • 将石头分成两堆,使两堆的重量尽可能接近
  • 两堆重量的差值就是最后可能剩下的最小重量

这是因为每次碰撞相当于从总重量中减去 twice of 较小的那块石头的重量,最优策略是让两堆石头重量尽可能接近。

代码原理详解

  1. 动态规划数组定义
    dp[j]表示能从石头中选出若干块,使得它们的总重量最大为j

  2. 计算目标值
    sum是所有石头的总重量
    targetsum/2,我们尝试找到不超过这个值的最大子集和

  3. 0-1 背包问题求解

    • 外层循环遍历每块石头(物品)
    • 内层循环从target往当前石头重量遍历(0-1 背包的典型做法,避免重复使用同一物品)
    • dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])表示对于重量j,要么不选当前石头保持dp[j],要么选当前石头得到dp[j - stones[i]] + stones[i]
  4. 计算结果
    sum - dp[target] - dp[target]表示总重量减去两倍的最大子集和(即两堆石头的重量差)

这个解法的时间复杂度是 O (n×target),空间复杂度是 O (target),其中 n 是石头数量,target 是总重量的一半。

494. 目标和

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) sum += nums[i];
        if (abs(target) > sum) return 0; // 此时没有方案
        if ((target + sum) % 2 == 1) return 0; // 此时没有方案
        int bagSize = (target + sum) / 2;
        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        for(int i=0;i<nums.size();i++){
            for(int j = bagSize;j>=nums[i];j--){
                dp[j] += dp[j-nums[i]];
            }
        }
        return dp[bagSize];
    }
};

给每个数字添加+-,本质上是将数组分成两组:

  • 一组数字前面加+(和为left
  • 另一组数字前面加-(和为right

根据题意,我们有:

plaintext

left - right = target  // 表达式结果等于target
left + right = sum     // 所有数字的总和

将两式相加可得:2*left = target + sum,即 left = (target + sum) / 2

因此问题转化为:找到有多少种方法可以从数组中选出一些数字,使它们的和等于 left

代码解析

  1. 边界条件判断

    if (abs(target) > sum) return 0; // 目标值的绝对值大于总和,不可能实现
    if ((target + sum) % 2 == 1) return 0; // 和为奇数,无法被2整除,没有方案
    
  2. 计算背包大小

    int bagSize = (target + sum) / 2;
    
     

    这里的bagSize就是我们要找的left值,即需要选出和为bagSize的数字组合

  3. 动态规划初始化

     
    vector<int> dp(bagSize + 1, 0);
    dp[0] = 1; // 表示凑出和为0的方法有1种(什么都不选)
    
     

    dp[j]的含义是:有多少种方法可以选出和为 j 的数字组合

  4. 填充 DP 数组

     
    for (int i = 0; i < nums.size(); i++) { // 遍历每个数字
        for (int j = bagSize; j >= nums[i]; j--) { // 倒序遍历背包
            dp[j] += dp[j - nums[i]];
        }
    }
    
     
    • 对于每个数字,我们可以选择 "放入背包"(即该数字前面加+)或 "不放入背包"(即该数字前面加-
    • 状态转移方程dp[j] += dp[j - nums[i]]表示:凑出和为 j 的方法数 = 不选当前数字的方法数 + 选当前数字的方法数(此时需要加上凑出 j-nums [i] 的方法数)
    • 倒序遍历是为了保证每个数字只被使用一次(0-1 背包特性)
  5. 返回结果

    return dp[bagSize];
    
     

    dp[bagSize]就是能凑出和为bagSize的方法总数,也就是我们要求的答案

示例说明

nums = [1,1,1,1,1]target = 3为例:

  • 总和sum = 5
  • bagSize = (3 + 5) / 2 = 4
  • 问题转化为:有多少种方法从数组中选出和为 4 的数字
  • 答案是 5 种,对应 5 种不同的表达式

474. 一和零

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> result(m+1,vector<int> (n+1,0));
        for(string str:strs){
            int onenumber = 0,zeronumber = 0;
            for (char c : str) {
                if (c == '0') zeronumber++;
                else onenumber++;
            }
            for(int i=m;i>=zeronumber;i--){
                for(int j=n;j>=onenumber;j--){
                        result[i][j] = max(result[i][j],result[i-zeronumber][j-onenumber]+1);
                    }
                }
            }
            return result[m][n];

        }
        
        
    
};

问题理解

给定一组二进制字符串,每个字符串包含0和1。我们需要选择一些字符串,使得:

  • 所有选中字符串中0的总数不超过 m

  • 所有选中字符串中1的总数不超过 n

  • 选中的字符串数量尽可能多

动态规划思路

1. 状态定义

dp[i][j] 表示:使用最多 i 个0和 j 个1时,能够组成的最大字符串数量

2. 状态转移

对于每个字符串,我们有两种选择:

  • 不选这个字符串dp[i][j] 保持不变

  • 选这个字符串dp[i][j] = dp[i - zeros][j - ones] + 1

状态转移方程:

cpp

dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1);

3. 为什么从后往前遍历?

这是0-1背包问题的核心技巧:

  • 从后往前遍历:确保每个字符串只被使用一次(避免重复选择)

  • 如果从前往后遍历,会变成完全背包问题(每个物品可以选多次)

4. 具体步骤

  1. 初始化:创建 (m+1) × (n+1) 的二维数组,初始值为0

  2. 遍历每个字符串

    • 计算当前字符串的0和1的数量

    • 从 m 到 zeros,从 n 到 ones 反向更新dp表

  3. 返回结果dp[m][n] 就是最大数量

示例说明

假设 strs = ["10", "0001", "111001", "1", "0"]m = 5n = 3

对于字符串 "10":

  • zeros = 1, ones = 1

  • 更新所有 i >= 1 且 j >= 1 的位置

对于字符串 "0001":

  • zeros = 3, ones = 1

  • 更新所有 i >= 3 且 j >= 1 的位置

以此类推...

时间复杂度

  • 外层循环:O(S),S为字符串数量

  • 内层循环:O(M × N)

  • 总复杂度:O(S × M × N)

空间复杂度

  • O(M × N),即dp表的大小

这种解法巧妙地利用了动态规划和背包问题的思想,通过反向遍历确保每个字符串只被考虑一次,从而找到最优解。


网站公告

今日签到

点亮在社区的每一天
去签到