代码随想录算法训练营Day41|动态规划part04

71 阅读6分钟

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

题目链接:leetcode.cn/problems/la…

文档讲解:programmercarl.com/1049.最后一块石头…

视频讲解:www.bilibili.com/video/BV14M…

思路

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

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

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

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

考虑什么时候,可以没有剩余石头。每次粉碎都是粉碎其中一块石头的两倍重量,所以经过n次粉碎能够粉碎所有石头,即把石头分为两部分,每个部分的重量和相等。于是本题和LeetCode 416 分割等和子集思路相同。 考虑如果按照背包问题思路,dp数组最后的值代表被粉碎过的所有重量的一半。所以最后只需要返回(sum - dp[-1])*2

总之,本题可以抽象为求容量为sum/2的背包,在尽可能装满后,剩余的容量的两倍。 石头的重量和价值相等。sum为所有石头的重量之和。

如果sum为奇数,对背包容量向下取整。在最后返回结果时➕1.

考虑动态规划五部曲:

  1. dp数组和下标含义:dp[j]为容量为j的背包能装下的数字的最大和
  2. 确定递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
  3. dp数组如何初始化:全部初始化为0
  4. 确定遍历顺序:从右向左
  5. 举例推导dp数组:假设stones为[1,2,2]。sum为5,背包容量为2。dp数组依次为
000
011
012
012
所以剩余容量是0,但sum是奇数,所以返回1.

解法

class Solution {
	public int lastStoneWeightII(int[] stones) {	
		int sum = 0;		
		for (int i : stones) {		
			sum += i;		
		}		
		int[] dp = new int[sum / 2 + 1];		
		for (int i = 0; i < dp.length; i++) {		
			dp[i] = 0;		
		}		
		for (int i = 0; i < stones.length; i++) {		
			for (int j = dp.length-1; j >= 0; j--) {			
				if (j >= stones[i]) {				
					dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);				
				}			
			}			
		}		
		int left = sum / 2 - dp[dp.length-1];		
		if (sum % 2 == 1) {		
			return left * 2 + 1;		
		}		
		return left * 2;	
	}
}

LeetCode 494 目标和

题目链接:leetcode.cn/problems/ta…

文档讲解:programmercarl.com/0494.目标和.ht…

视频讲解:www.bilibili.com/video/BV1o8…

思路

设取正的数总和为pos,取负的数为neg,那么neg+pos = sum,即neg=sum-pos 而pos-neg = target。所以pos = (target + sum) / 2,而pos确定了neg也会确定。 所以把本题转化为背包问题,问装满容量为(target + sum) / 2的背包有几种方法。

考虑如果target+sum是奇数,就不存在满足题目的组合。直接返回0. 如果target是负数,可以想到反转全部正负号就可以得到target绝对值得组合。所以对target取绝对值,结果相同

使用二维dp数组

考虑动态规划五部曲:

  1. dp数组和下标含义:dp[i][j]表示用下标为0-i的num装满容量为j的背包有几种方法
  2. 确定递推公式:dp[i][j] = dp[i-1][j]+dp[i-1][j-nums[i]];.前者是不装nums[i]的方法数量,后者是装入nums[i]的方法数量。如果无法装入nums[i],就直接继承前者
  3. dp数组如何初始化:dp[0][0]一定为1.把第一行其余初始化为0。如果存在j==nums[0]dp[0][j]➕1。
  4. 确定遍历顺序:从左到右,从上到下
  5. 举例推导dp数组

使用一维dp数组

考虑动态规划五部曲:

  1. dp数组和下标含义:dp[j]表示装满容量为j的背包有几种方法
  2. 确定递推公式:dp[j] = dp[j]+dp[j-nums[i]];.前者是不装nums[i]的方法数量,后者是装入nums[i]的方法数量。如果无法装入nums[i],就直接继承前者
  3. dp数组如何初始化:dp[0]一定为1.其余为0.
  4. 确定遍历顺序:从右到左
  5. 举例推导dp数组

解法

使用二维dp数组

class Solution {
	public int findTargetSumWays(int[] nums, int target) {	
		int sum = 0;		
		for (int i : nums) {		
			sum += i;		
		}		
		target = Math.abs(target);		
		if ((sum + target) % 2 == 1) {		
			return 0;		
		}		
		int capacity = (sum + target) / 2;		
		int[][] dp = new int[nums.length][capacity+1];		
		// 初始化		
		for (int i = 0; i < dp[0].length; i++) {		
			if (i == 0) {			
				dp[0][0] = 1;				
			}			
			if (i == nums[0]) {			
				dp[0][i] += 1;			
			}		
		}		
		// 递推		
		for (int i = 1; i < dp.length; i++) {		
			for (int j = 0; j <= capacity; j++) {			
				if (j >= nums[i]) {				
					dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]];				
				}				
				else {				
					dp[i][j] = dp[i-1][j];				
				}			
			}		
		}		
		return dp[nums.length-1][capacity];	
	}
}

使用一维dp数组

class Solution {
	public int findTargetSumWays(int[] nums, int target) {	
		int sum = 0;		
		for (int i : nums) {		
			sum += i;		
		}		
		target = Math.abs(target);		
		if ((sum + target) % 2 == 1) {		
			return 0;		
		}		
		int capacity = (sum + target) / 2;		
		int[] dp = new int[capacity+1];		
		// 初始化		
		dp[0] = 1;		
		// 递推		
		for (int i = 0; i < nums.length; i++) {		
			for (int j = capacity; j >= 0; j--) {			
				if (j >= nums[i]) {				
					dp[j] = dp[j] + dp[j-nums[i]];				
				}			
			}		
		}		
		return dp[capacity];	
	}
}

LeetCode 474 一和零

题目链接:leetcode.cn/problems/on…

文档讲解:programmercarl.com/0474.一和零.ht…

视频讲解:www.bilibili.com/video/BV1rW…

思路

本题乍一看要满足两个条件,联想到多重背包。但回顾定义,多重背包问题的定义是,每个物品的数量不同。而本题中每个01字符串都只有一个,所以其实仍然是01背包。只是需要考虑两个维度。

考虑动态规划五部曲:

  1. dp数组和下标含义:dp[i][j]表示最多含有i个0,j个1的最大子集长度
  2. 确定递推公式:设k是对strs的遍历索引。dp[i][j] = max(dp[i][j], dp[i-zero[k]][j-one[k]])+1
  3. dp数组如何初始化:考虑dp的定义,在未开始遍历前,没有str会被考虑放入,全部初始化为0.
  4. 确定遍历顺序:从右到左,从下到上。
  5. 举例推导dp数组

解法

class Solution {
	public int findMaxForm(String[] strs, int m, int n) {	
		// 提取strs中的01个数		
		int[] zero = new int[strs.length];		
		int[] one = new int[strs.length];		
		for (int i = 0; i < strs.length; i++) {		
			for (int j = 0; j < strs[i].length(); j++) {			
				if (strs[i].charAt(j) == '0') {				
					zero[i]++;				
				}				
				else {				
					one[i]++;				
				}			
			}		
		}		
		// m-->0 n-->1		
		int[][] dp = new int[m+1][n+1];		
		for (int k = 0; k < strs.length; k++) {		
			for (int i = m; i >= 0; i--) {			
				for (int j = n; j >= 0; j--) {				
					if (i >= zero[k] && j >= one[k]) {					
						dp[i][j] = Math.max(dp[i][j], dp[i-zero[k]][j-one[k]]+1);					
					}				
				}			
			}		
		}		
		return dp[m][n];	
	}
}

今日收获总结

今日学习时长5小时,今天的背包问题有很多共性但也有一些挑战