1.练习题
1)
力扣https://leetcode.cn/problems/house-robber-ii/解答:
这道题的重点是第一个屋子和最后的一个屋子不能一起偷。
所以可以把屋子分成两组,一组是从第一个屋子到倒数第二个屋子,另一组是从第二个屋子到最后屋子。
针对每组:
dp[i][0]表示不偷第i个屋子时的最大收益,那前一个屋子就是可偷可不偷,取二者中的最大值
dp[i][0] = max(dp[i-1][0], dp[i-1][1])
dp[i][1]表示偷第i个屋子时的最大收益,所以前一个屋子不能偷
dp[i][1] = dp[i-1][1] + nums[i-1]
代码:
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n==0) return 0;
if(n==1) return nums[0];
vector<vector<int>> dp(n+1,vector<int>(2,0));
for(int i=2;i<n+1;i++){
dp[i][0] = max(dp[i-1][0], dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i-1];
}
int res = max(dp[n][0], dp[n][1]);
for(int i=1;i<n;i++){
dp[i][0] = max(dp[i-1][0], dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i-1];
}
int res2 = max(dp[n-1][0], dp[n-1][1]);
return max(res, res2);
}
};
2)
力扣https://leetcode.cn/problems/maximum-subarray/
这题是典型的DP,针对每个元素,考虑是与前面的元素组成连续子数组,或者是放弃前面的元素,当前元素作为连续子数组的开头元素。判断依据就是哪种情况下连续子数组的和更大。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n+1,INT_MIN/2);
for(int i=1;i<=n;i++){
dp[i] = max(dp[i-1]+nums[i-1], nums[i-1]);
}
return *max_element(dp.begin(), dp.end());
}
};
可以看到dp[i]只与dp[i-1]相关,所以可以把一维数组压缩成常量。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
int pre = 0, cur, res = nums[0];
for(int i=1;i<=n;i++){
cur = max(pre+nums[i-1], nums[i-1]);
pre = cur;
res = max(res, cur);
}
return res;
}
};
结果:
3)
力扣https://leetcode.cn/problems/integer-break/看到这道题首先的思路是dp[i]代表i拆分后的最大乘积,所以可以遍历和为n的所有两个数的组合:
- 若拆分成两个数:dp[i]=max(dp[i],j*(i-j))
- 若拆分成两个以上的数:dp[i]=max(dp[i],j*dp[i-j])
这里的dp[i-j]代表i-j这个数再拆分后的最大乘积。
这时候,直觉地会有一个写法dp[i]=max(dp[i],dp[j]*(i-j)),这其实是和第二种情况重复了,i和i-j本质上是等价的。
还有另一个写法dp[i]=max(dp[i],dp[j]*dp[i-j]),我感觉这可以算是第三种情况,就是j和(i-j)都取拆分。但看最后结果,加不加这种情况,不会影响最终结果。大概是因为当把数字拆的过于小了之后,得到的自然不会是最大乘积了。
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n+1,0);
dp[1] = 1;
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++){
dp[i]=max(dp[i],j*(i-j));
dp[i]=max(dp[i], j*dp[i-j]);
}
}
return dp[n];
}
};
4)
力扣https://leetcode.cn/problems/delete-operation-for-two-strings/
这题比之前做的那道字符串编辑的题简单一些,因为这道题只考虑删除。
那么转移方程就很简单了,如果匹配,那最小步数等于前一个字符的情况。
如果不匹配:要么删除字符串1的当前字符,要么删除字符串2的当前字符,要么都删除。
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(int i=0;i<=n;i++){
dp[0][i]=i;
}
for(int i=0;i<=m;i++){
dp[i][0]=i;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(word1[i-1]==word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = min(dp[i-1][j]+1, min(dp[i][j-1]+1, dp[i-1][j-1]+2));
}
}
}
return dp[m][n];
}
};
5)
力扣https://leetcode.cn/problems/maximum-length-of-pair-chain/这题最初,我是这么想的,一个二维数组,每一行表示以该数对结尾的数对链(表示之前的数对都已考虑过,可以不取某个数对),每一列表示要新增的数对。
然后二维数组遍历,来判断合法的数对链。
于是我写出了这一段代码:
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
if(pairs.size()==0 || pairs[0].size()==0){
return 0;
}
sort(pairs.begin(), pairs.end(),
[](const vector<int>& a, const vector<int>& b){
return a[1]<b[1];
}
);
int n = pairs.size();
vector<vector<int>> dp(n,vector<int>(n,1));
int res = 1;
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++){
if(pairs[i][1]<pairs[j][0]){
if(i==0){
dp[i][j] += 1;
}else{
dp[i][j] = dp[i-1][i]+1;
}
}
if(i!=0)
dp[i][j] = max(dp[i][j], dp[i-1][j]);
res = max(res, dp[i][j]);
}
}
return res;
}
};
写完之后,虽然通过了所有case,但很显然我是想复杂了。这个二维数组左下边的一半都是用不到的,每次转移方程也都是只用到上一次以i或者以j结尾的最长数对链。
所有很明显可以压缩成一维数组,每个元素表示以i结尾时的最长数对链:
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
if(pairs.size()==0 || pairs[0].size()==0){
return 0;
}
sort(pairs.begin(), pairs.end(),
[](const vector<int>& a, const vector<int>& b){
return a[1]<b[1];
}
);
int n = pairs.size();
vector<int> dp(n,1);
int res = 1;
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
if(pairs[j][1]<pairs[i][0]){
dp[i] = max(dp[i], dp[j]+1);
res = max(res, dp[i]);
}
}
}
return res;
}
};
6)
力扣https://leetcode.cn/problems/wiggle-subsequence/解题思路:
首先如果相邻元素相等,那是肯定不满足要求的,所以删去重复的相邻元素。
接下来再处理,就不会出现差值为零的情况了,只会大于0或者小于0。
所以对连续的三个数的两个差值相乘,来判断是否是摆动的。
如果小于零,说明是摆动的,序列长度加一。否则序列长度不变。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
vector<int> nums2;
int n = nums.size();
nums2.push_back(nums[0]);
for(int i=1;i<n;i++){
if(nums[i]!=nums[i-1]){
nums2.push_back(nums[i]);
}
}
n = nums2.size();
if(n<3) return n;
vector<int> dp(n,0);
dp[0] = 1;
dp[1] = 2;
for(int i=2;i<n;i++){
if((nums2[i]-nums2[i-1])*(nums2[i-1]-nums2[i-2])<0){
dp[i] = dp[i-1] + 1;
}else{
dp[i] = dp[i-1];
}
}
return dp[n-1];
}
};
因为dp[i]只与dp[i-1]相关,所以可以用两个遍历代替一维数组
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
vector<int> nums2;
int n = nums.size();
nums2.push_back(nums[0]);
for(int i=1;i<n;i++){
if(nums[i]!=nums[i-1]){
nums2.push_back(nums[i]);
}
}
n = nums2.size();
if(n<3) return n;
vector<int> dp(n,0);
int pre = 2, cur;
for(int i=2;i<n;i++){
if((nums2[i]-nums2[i-1])*(nums2[i-1]-nums2[i-2])<0){
cur = pre + 1;
}else{
cur = pre;
}
pre = cur;
}
return cur;
}
};
结果:
7)
力扣https://leetcode.cn/problems/target-sum/解题思路:用一个二维数组来表示状态转移,横坐标i表示考虑数组[0...i]的组合,纵坐标j表示这些组合的和。
因为组合的和存在负数,所以要加上一个数组列数的偏移量
dp[i][j]表示数组[0...i]的组合和为j,所以等于数组组[0...i-1]的组合和为j-nums[i],加上数组组[0...i-1]的组合和为j+nums[i]的和。当然前提是坐标要不越界。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
int len = max(target,-target);
len = max(len,accumulate(nums.begin(), nums.end(),1));
vector<vector<int>> dp(n,vector<int>(2*len+1,0));
dp[0][nums[0]+len] += 1;
dp[0][-nums[0]+len] += 1;
for(int i=1;i<n;i++){
for(int j=0;j<=2*len;j++){
if((j-nums[i])>=0 && (j-nums[i])<=2*len)
dp[i][j] += dp[i-1][j-nums[i]];
if((j+nums[i])>=0 && (j+nums[i])<=2*len)
dp[i][j] += dp[i-1][j+nums[i]];
}
}
return dp[n-1][target+len];
}
};
因为dp[i]只和dp[i-1]相关,所以压缩成一维数组,又因为遍历数组时,会覆盖掉后面的元素要用到的值,所以最后用两个一维数组来实现:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
int len = max(target,-target);
len = max(len,accumulate(nums.begin(), nums.end(),1));
vector<int> pre(2*len+1,0);
vector<int> cur(2*len+1,0);
pre[nums[0]+len] += 1;
pre[-nums[0]+len] += 1;
for(int i=1;i<n;i++){
for(int j=0;j<=2*len;j++){
cur[j] = 0;
if((j-nums[i])>=0 && (j-nums[i])<=2*len)
cur[j] += pre[j-nums[i]];
if((j+nums[i])>=0 && (j+nums[i])<=2*len)
cur[j] += pre[j+nums[i]];
}
pre = cur;
}
return pre[target+len];
}
};
8)
力扣https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/解题思路:考虑手上有股票和没有股票的两种情况
有股票:之前购买的,和当前购买的,这两种情况的最大值
没有股票:直接手上就没有股票,和当前卖掉股票,这两种情况的最大值
转移方程:
dp1[i] = max(dp1[i-1], dp2[i-1]-prices[i]);
dp2[i] = max(dp2[i-1], dp1[i-1]+prices[i]-fee);
代码:
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
vector<int> dp1(n,0);
vector<int> dp2(n,0);
dp1[0] = -prices[0];
for(int i=1;i<n;i++){
dp1[i] = max(dp1[i-1], dp2[i-1]-prices[i]);
dp2[i] = max(dp2[i-1], dp1[i-1]+prices[i]-fee);
}
return dp2[n-1];
}
};