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

69 阅读5分钟

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

链接

文章链接

题目链接

第一想法

这道题的要求是石头两两撞碎,问最后一块石头的最小重量,所以是,可以把石头堆分为重量最相近的两堆,差值即为所求,分析到这一点就知道这道题怎么想了,因为刚做了一道把数组分为两个相等的子数组。五部曲

  1. dp数组的含义,这里的dp[j]表示背包容量为j时的最大价值(这里的价值和重量都是石头的大小)
  2. 初始化,很容易想,都是为0的
  3. 递推公式 dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i])
  4. 遍历顺序也是一样的,因为这里是滚动数组,所以必须先是物品后是背包
//一维数组
function lastStoneWeightII(stones: number[]): number {
  let sum: number = 0
  for (let i = 0; i < stones.length; i++) {
    sum += stones[i]
  }
  let dp: number[] = new Array(~~(sum / 2) + 1).fill(0)
  for (let i = 0; i < stones.length; i++) {//物品
    for (let j = dp.length - 1; j >= stones[i]; j--) {//背包
      dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i])
    }
  }
  return Math.abs(sum - 2 * dp[dp.length - 1])
}

看完文章后的想法

文章的讲解更加全面,这里就附一张图来表示手动推导递推公式的过程:

image.png 同时我的return返回值可以为sum - 2 * dp[dp.length - 1],因为dp数组咱们是向下取整的,可以不用有取绝对值的过程。

思考

这道题不算太难,如果你能想到把石头堆分为两个质量最相近的石头堆的话,这道题就是416. 分割等和子集这道题一样了。不同只是最后的返回值不一样罢了

494. 目标和

链接

题目链接

文章链接

第一想法

emmm,刚开始就想错了,我刚开始是选几个数的运算后和为target,结果是每个数都要使用,然后就不会了。。。

看完文章后的想法

很妙,这道题对于我来说确实不容易想,因为运算完和为target,所以可以把数组分为两堆,一堆和为left,一堆和为right(这里假定left>=right),所以就满足这个等式left+right=sum,left-right=target,所以转换为left=(sum+target)/2,所以就需要和为left有多少种方法就可以了,由这个等式可以看出,当(sum+target)%2===1时,一定是不可能达成和为left的,因为nums都是正数。同时如果Math.abs(target) > sum也是不能的,接下来递归五部曲

  1. dp数组的含义为和为j的方法数为dp[j]
  2. 初始化,这个初始化不好想,原因就是dp[0]为0还是为1,这个是为1的,原因和递推公式有关,如果为0的话每一个dp[j]都是为0的
  3. 递推公式为dp[j] += dp[j - nums[i]]原因为假设j=5,则dp[5]的意思为和为5的方法数,所以方法数为5-nums[i]对应的方法数和
  4. 遍历顺序还是为滚动数组的便利顺序是一样的
function findTargetSumWays(nums: number[], target: number): number {
  let sum: number = 0
  for (let i = 0; i < nums.length; i++) {//求和
    sum += nums[i]
  }
  if (Math.abs(target) > sum || (sum + target) % 2 === 1) return 0//left如果携带小数或者Math.abs(target) > sum 则一定不可能有表达式为target
  let dp: number[] = new Array((sum + target) / 2 + 1).fill(0)//dp数组
  dp[0] = 1//初始化 很重要!!!!
  for (let i = 0; i < nums.length; i++) {
    for (let j = (sum + target) / 2; j >= nums[i]; j--) {
      dp[j] += dp[j - nums[i]]
    }
  }
  return dp[dp.length - 1]
}

思考

这道题比较难,确实没想到,核心思想就是要通过left+right=sumleft-right=target两个公式推出left=(sum+target)/2,因为只有推出left为什么,才能想到用0-1背包解决这个问题

474. 一和零

链接

文章链接

题目链接

第一想法

emmmmm,只要题转变一点思路的我就没啥想法了。。看文章了。。。

看完文字的想法

看完文字后,我的想法只有一个字妙!,对于动态规划还是得继续努力,这道题有点奇特,虽然你表面看dp数组是二维数组,但其实是按照滚动数组的解法来解问题的,接下来递归五部曲

  1. dp数组的含义dp[i][j]表示0的容量为i个,1的容量为j个时子集的最大长度
  2. 初始化,很简单 都初始为0就可以了
  3. 递推公式 还是按照滚动数组的写法 dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1) 不算当前字符串的子集长度与算上当前字符串的子集长度取最大值
  4. 遍历顺序,这里的便利顺序很重要,对于strs的遍历顺序是无关的,但是对于i和j的遍历顺序一定是倒序的,原因是防止一个字符串被用了多次
function findMaxForm(strs: string[], m: number, n: number): number {
  let dp: number[][] = new Array(m + 1).fill(0).map((item) => new Array(n + 1).fill(0))//dp数组i为0的容量 j为1的容量
  for (const str of strs) {//物品遍历
    let zeroNum: number = 0
    let oneNum: number = 0
    for (let i = 0; i < str.length; i++) {//这个是计算这个字符串0和1的个数
      if (str[i] == '0') zeroNum++
      else oneNum++
    }
    //背包遍历  这个背包的容量必须要满足两个条件 0的容量大于0的个数 1的容量大于1的个数
    for (let i = m; i >= zeroNum; i--) {
      for (let j = n; j >= oneNum; j--) {
        dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)//这里i - zeroNum和j - oneNum是防止0和1超过背包容量
      }
    }
  }
  return dp[m][n]
}

思考

这道题对我来说,有点难,同时也有点大意,没想到二维数组也是可以看出滚动数组的,所以这道题感觉写不出来就看答案了,有点携带了,这道题如果认认真真分析的话应该是能想到一部分思路的

今日总结

今天三道题,一共耗费3小时,其中后两道没做出来,对我来说确实有点难了。看来动态规划的思路还是得继续学习。