算法刷题打卡037 | 动态规划5
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)。