LeetCode 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. 目标和
回溯法
思路:
这道题目需要利用数组里的元素构造表达式使得和等于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.一和零
思路:
其实仔细思考下,就可以发现这道题目完全就是二维版的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];
}
};