本题是198题的变形题。把线性的一维数组变成了环形数组,这导致第0个房间和第n-1个房间不能同时抢劫。因此我们可以分成两种情况讨论:
1)抢0号房间,然后对[1, n-2]进行动态规划过程;
2)不抢0号房间,然后对[1, n-1]进行动态规划过程。
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
int pre1=nums[0], pre2=nums[0], cur1=nums[0],cur2=0;
//抢第一个房间
for(int i=2;i<n-1;i++){
cur1 = max(pre1 + nums[i], pre2);
pre1 = pre2;
pre2 = cur1;
}
//不抢第一个房间
pre1 = 0, pre2 = 0;
for(int i=1;i<n;i++){
cur2 = max(pre1+nums[i],pre2);
pre1 = pre2;
pre2 = cur2;
}
return max(cur1,cur2);
}
};
设dp[i]表示以nums[i]结尾的最大连续子数组的和,则dp[i] = max(dp[i-1] +nums[i], nums[i])
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n);
dp[0] = nums[0];
int ans = nums[0];
for(int i=1; i<n; i++){
dp[i] = max(dp[i-1] + nums[i], nums[i]);
ans = max(ans,dp[i]);
}
return ans;
}
};
空间压缩
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
int ans = nums[0], pre = nums[0], cur;
for(int i=1; i<n; i++){
cur = max(nums[i], pre + nums[i]);
pre = cur;
ans = max(ans,cur);
}
return ans;
}
};
动态规划方法:
设dp[i]表示i拆分得到整数积的最大值。我们很自然想到把 i 拆分成 j 和 i-j,那么dp[i] = max(dp[j]*dp[i-j]) 1<=j<=i/2。测试后,发现这样有个漏洞,所有结果都为1,因为j和i-j会被一直拆分下去,所以相当于把n拆分成n个1相乘。对于j和i-j可以不继续拆分,直接用j或i-j乘。因此正确的状态转移方程为
dp[i] = max( max(j, dp[j]) * max(i-j, dp[i-j]) ) 1<=j<=i/2
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/2; j++){
dp[i] = max(dp[i], max(j,dp[j])*max(i-j,dp[i-j]));
}
}
return dp[n];
}
};
删除操作数 = m + n - 2*l(m和n为两字符串长度,l为LCS长度)。因此可以直接用LCS方法求出最长公共子序列长度,再用上述公式。使用空间压缩减少空间复杂度。
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.length(), n = word2.length();
if(m<n){
swap(m,n);
swap(word1,word2);
}
vector<int> dp(n+1,0);
for(int i=1;i<=m;i++){
int pre = 0;
for(int j=1;j<=n;j++){
int next = dp[j];
if(word1[i-1] == word2[j-1]){
dp[j] = pre + 1;
}else{
dp[j] = max(dp[j-1],dp[j]);
}
pre = next;
}
}
return m+n-2*dp[n];
}
};
也可以直接针对本题列出状态转移方程,
dp[i][j] = dp[i-1][j-1] if word1[i-1] == word[j-1]
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1 if word1[i-1] != word[j-1]
注意初值为dp[i][0] = i, dp[0][j] = j;
同样进行空间压缩。
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.length(), n = word2.length();
if(m<n){
swap(m,n);
swap(word1,word2);
}
vector<int> dp(n+1);
//初值dp[0][j] = j, dp[i][0] = i;
for(int j=0;j<=n;j++){
dp[j] = j;
}
for(int i=1;i<=m;i++){
int pre = i-1;//pre保存dp[i-1][j-1]
dp[0] = i;//dp[i][0]初值为i
for(int j=1;j<=n;j++){
int next = dp[j];
if(word1[i-1] == word2[j-1]){
dp[j] = pre;
}else{
dp[j] = min(dp[j-1],dp[j]) + 1;
}
pre = next;
}
}
return dp[n];
}
};
本题是300题最长递增子序列的变种题,只不过把单个数字换成了数对,且允许以任何顺序使用数对。
方法一:简单动态规划
先将数对按第一个数字从小到大排序。设dp[i]表示以pairs[i]结尾的最长数对长度。则dp[i] = max(dp[j] + 1) , if pairs[j][1] < pairs[i][0] && 0<=j<i。
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
int n = pairs.size();
vector<int> dp(n,1);
sort(pairs.begin(),pairs.end(),[](vector<int> a, vector<int> b){
if(a[0]!=b[0]) return a[0] < b[0];
else return a[1]<b[1];
});
for(int i=1;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);
}
}
}
return dp[n-1];
}
};
方法二:动态规划+二分法
设dp[k]存储长为k+1的数对链最小的最后一个数对的后一个数字。之后遍历一遍数组,如果pairs[i][0] > dp.back(),则直接添加到dp末尾;否则找到pairs[i]能插入的位置,并更新对应最小值。排序过程,实际只需按pair[i][1]排序即可,使第2个数字尽量小,更容易插入新数对。
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
int n = pairs.size();
vector<int> dp;//dp[k]存储长为k+1的数对链最后一个数对的后一个数字
sort(pairs.begin(),pairs.end(),[](vector<int> a, vector<int> b){
return a[1]<b[1]
});
dp.push_back(pairs[0][1]);
for(int i=1;i<n;i++){
//int size = dp.size()-1;
if(pairs[i][0] > dp.back()){
dp.push_back(pairs[i][1]);
}else{
auto it = lower_bound(dp.begin(), dp.end(), pairs[i][0]);
if(*it > pairs[i][1]) *it = pairs[i][1];//排序后,*it肯定小于pairs[i][1],根本不执行
}
}
return dp.size();
}
};
方法三:贪心
进一步思考,发现dp[k]存储的中间结果,实际上是不需要的。在最长递增子序列问题中,由于数组本身是无序的,所以当前最长的子序列可能在后序遍历中被更短的子序列超过长度,因此需要始终维护每个长度结果。而这里我们先排序了,后面进来的元素pairs[i][1]肯定大于之前的,因此只要执行pairs[i][0] > dp.back()这一部分即可。
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
sort(pairs.begin(), pairs.end(), [](vector<int> &a, vector<int>b) {
return a[1] < b[1];
});
int n = pairs.size(), ans = 1, r = pairs[0][1];
for (int i = 1; i < n; i++) {
if (pairs[i][0] > r) {
ans++;
r = pairs[i][1];
}
}
return ans;
}
};
方法一:
设dp[i]表示以nums[i]结尾的最长摆动子序列长,diff[i]以nums[i]结尾的最长摆动子序列的最后一个差值,则dp[i] = max(dp[j] + 1), if 0<=j<i && nums[i] - nums[j] 与 diff[j]异号。时间复杂度为O(n)
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n,1), diff(n,0);
int ans = 1;
for(int i=1; i<n; i++){
for(int j=0; j<i; j++){
int dij = nums[i] - nums[j];
if((j==0 && dij!=0) || (dij<0 && diff[j]>0) || (dij>0 && diff[j]<0)){//与上一结尾差值异号
//(j==0 && dij!=0) 与首元素不等时也要更新
dp[i] = max(dp[i], dp[j]+1);
diff[i] = dij;
}
}
ans = max(ans,dp[i]);
}
return ans;
}
};
方法二:
考虑到摆动序列是正负交替出现,那么可以分开讨论,减少复杂度。
摆动序列可分为:最后一个元素上升的上升摆动序列和最后一个元素下降的摆动序列。
设up[i]和down[i]分别表示前i个元素中最长的上升摆动序列和下降摆动序列长度,这样一来up[i]和down[i] 都只取决于up[i-1]和dowm[i-1],不需要从0开始遍历。
对up[i]:
1) 当nums[i] <= nums[i-1]时,如果需要nums[i]结尾,同样可以用nums[i-1]代替,故up[i] = up[i-1];
2) 当nums[i] > nums[i-1]时,由摆动序列定义,up[i] = max(up[i-1], down[i-1]+1);
同理对down[i]:
1) 当nums[i] >= nums[i-1]时,down[i] = down[i-1];
2) 当nums[i] < nums[i-1]时,down[i] = max(up[i-1]+1, down[i-1]);
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n = nums.size();
vector<int> up(n), down(n);
up[0] = down[0] = 1;
for(int i=1; i<n; i++){
if(nums[i]<nums[i-1]){
up[i] = up[i-1];
down[i] = max(up[i-1]+1, down[i-1]);
}else if(nums[i]>nums[i-1]){
down[i] = down[i-1];
up[i] = max(down[i-1]+1, up[i-1]);
}else{
up[i] = up[i-1];
down[i] = down[i-1];
}
}
return max(up[n-1], down[n-1]);
}
};
总结:从本题可以看出,对序列或字符串(设为A)动态规划问题,在状态设计时,有两种思路:
1)令dp[i]表示以A[i]结尾或开头的XX,这样设计状态转移方程一般比较简单,但可能要考察i前面多项的关系,如从[0,i-1]中选出符合条件的最优值;
2)令dp[i]表示A[i]的前i项范围中满足XX,这样设计状态转移方程比较复杂,但通常就dp[i]就只与前面dp[i-1]有关,复杂度较低。
本题是0-1背包问题的变式,0-1背包问题是在给定体积下使物品价值尽量大,我们要把握好体积和价值这两个量。对于本题,体积限制对应目标和target,价值对应方案数。因此我们可以设dp[i][j]表示前i项中组合得到j的方案数,对第i个数,在组合时有+和-两种选择,因此状态转移方程为:
dp[i][j] = dp[i-1][j-w] + dp[i-1][j+w]; 其中w=nums[i]
由于和范围可能在[-sum, sum]之间,整体平移sum,对应于j范围是[0,2*sum]
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
int sum = accumulate(nums.begin(),nums.end(),0);
target += sum;
if(target>2*sum || target < 0) return 0;
vector<vector<int>> dp(n, vector<int>(2*sum + 1,0));
int w = nums[0];
++dp[0][w+sum], ++dp[0][sum-w];//w=0时,sum位置应为2,故采用自加赋值
for(int i=1; i<n;i++){
w = nums[i];
for(int j=0; j<2*sum+1;j++){
if((j-w)>=0 && (j-w)<(2*sum+1)) dp[i][j] += dp[i-1][j-w];
if((j+w)<(2*sum+1)) dp[i][j] += dp[i-1][j+w];
}
}
return dp[n-1][target];
}
};
同样进行空间压缩
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
int sum = accumulate(nums.begin(),nums.end(),0);
target += sum;
if(target>2*sum || target < 0) return 0;
vector<int> dp(2*sum + 1,0);
int w = nums[0];
++dp[w+sum], ++dp[sum-w];//w=0时,sum位置应为2,故采用自加赋值
for(int i=1; i<n;i++){
w = nums[i];
vector<int> temp(2*sum+1,0);
for(int j=0; j<2*sum+1;j++){
if((j-w)>=0 && (j-w)<(2*sum+1)) temp[j] = dp[j-w];
if((j+w)<(2*sum+1)) temp[j] += dp[j+w];
}
swap(temp,dp);
}
return dp[target];
}
};
当然本题还可进一步改进。
设所有元素的和为sum,取负号的元素和为neg,则题目要求为sum - 2*neg = target,即
neg = (sum - target)/2
若sum - target小于0或不是2的倍数则一定不能实现,直接return 0.
若在此范围内,则题目变为从nums数组中选出若干数字,使之和为neg,一个标准的0-1背包问题。设dp[i][j]表示前i个数中选取若干数得到和为j的方案数。
边界条件:当没有任何元素可以选取时,元素和只能是 0,对应的方案数是 1。
使用空间压缩
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
int sum = accumulate(nums.begin(),nums.end(),0);
if(sum<target || (sum-target)%2!=0) return 0;
int neg = (sum - target)/2;
vector<int> dp(neg+1,0);
dp[0] = 1;//dp[0][0]=1,0个数得到0,方案为1
for(int i=0; i<n;i++){
int w = nums[i];
for(int j=neg; j>=0;j--){
if(j>=w) dp[j] = dp[j-w] + dp[j];
}
}
return dp[neg];
}
};
动态规划总结
常用的动态规划模型问题:
1.序列、字符串性质类问题
这是最常见的动态规划问题。如最长递增子序列,最长公共子序列LCS等问题,一般是从序列中选出满足性质的子序列。一般思路为:
1)令dp[i]表示以A[i]结尾、或前i项中满足性质的XXX,
2)令dp[i][j]表示A[i]到A[j]范围内的XXX
在设计状态转移方程时,如果dp[i]设成前i项中XXX,则一般要考虑前面多项;如果dp[i]表示以A[i]结尾的XXX则一般考虑相邻项;另外,如果是分割类型的问题,则一般考虑分割位置。
2、背包问题
包括0-1背包问题和完全背包问题两大类。背包问题可以表述为:给定总体积V下,从n项物品做出价值W最大的选择。关键在于找准问题中的体积V和价值W是什么。同时学会使用空间压缩的方法减少空间复杂度。
3、股票交易问题
通解情况参看文章:https://leetcode-cn.com/circle/article/qiAgHn/