算法解释
动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算。
解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。
同时,我们也可以对动态规划进行空间压缩,起到节省空间消耗的效果。
在一些情况下,动态规划可以看成是带有状态记录(memoization)的优先搜索。状态记录的意思为,如果一个子问题在优先搜索时已经计算过一次,我们可以把它的结果储存下来,之后遍历到该子问题的时候可以直接返回储存的结果。
- 动态规划是自下而上的,即先解决子问题,再解决父问题;如果题目需求的是最终状态,那么使用动态搜索比较方便;
- 而用带有状态记录的优先搜索是自上而下的,即从父问题搜索到子问题,若重复搜索到同一个子问题则进行状态记录,防止重复计算。如果题目需要输出所有的路径,那么使用带有状态记录的优先搜索会比较方便。
1.基本动态规划
一维
70.爬楼梯
首先明确我们需要的是最终状态,即多少种路径。用动态规划。
不知道各位有没有玩儿过一个游戏叫做“抢三十”,和这个题目是一样的,每个人每次按顺序喊一个数或者两个数,谁先喊到30谁就输。
这其实是一个斐波拉契数列的问题,定义数组x,x[i]表示走到第i阶台阶的方法数,自然很快可以得到
- x[i]=x[i-1]+x[i-2]
class Solution {
public:
int climbStairs(int n)
{
if(n<=2) return n;//x[1]=1,x[2]=2
//设一个数组x[n+1],即从0到n全预设为1,里面的每一个数,x[i]=x[i-1]+x[i-2]
vector<int> x(n+1,1);
for(int i=2;i<=n;++i)
{
x[i]=x[i-1]+x[i-2];
}
return x[n];
}
};
这样写发现了不对劲的地方
上述代码是拿空间换时间,如果我们想要空间,很明显应该有优化空间的地方
其实我们只需要两个数来存等式右边的就可
class Solution {
public:
int climbStairs(int n)
{
if(n<=2) return n;
int a1=1,a2=2,a3;
for(int i=2;i<n;++i)
{
a3=a1+a2;//每次算完一次所有数都得左移
a1=a2;
a2=a3;
}
return a3;
}
};
198.打劫
我们考虑,当我们到编号为i的房子,x[i]代表,数到i房子时,最大利益为多少:
- 如果我们抢这个房子,我们有x[i-2]+ nums[i]
- 如果我们不抢这个房子,我们只有x[i-1]
所以这两个数得取max
我们得到的方程为: x[i]=max(x[i-2]+nums[i],x[i-1]);
初始化的时候得注意:
x[0]=nums[0],因为我们的x代表数到i时最大利益为多少,第0间房我们数到的时候,得抢的,但具体抢不抢却是由nums【0】和nums【1】谁大决定的
如果我们抢1号的话就不能抢0号
所以
x[1]=max(nums[0],nums[1]);
从2号开始循环,一直跑到n-1刚好n个房间
class Solution {
public:
int rob(vector<int>& nums)
{
if(nums.empty()) return 0;
int n=nums.size();
if(n==1) return nums[0];
if(n==2) return max(nums[0],nums[1]);
vector<int> x(n,0);
x[0]=nums[0];
x[1]=max(nums[0],nums[1]);
for(int i=2;i<n;++i)
{
x[i]=max(x[i-2]+nums[i],x[i-1]);
}
return x[n-1];
}
};
740.删除数 (同打劫)
class Solution {
public:
int deleteAndEarn(vector<int>& nums)
{
sort(nums.begin(),nums.end());
int n=nums.size();
vector<int> n_sum(nums[n-1]+1,0);
//一旦选择x,即获得n_sum[x],不可选n_sum[x-1]
//dp[x]=max(dp[x],dp[x-1],dp[x+1])
for(int i=0;i<n;++i)
{
n_sum[nums[i]]+=nums[i];
}
//遍历n_sum即可
int nsize=n_sum.size();
if(nsize<=2)
{
if(nsize==1) return n_sum[0];
else return max(n_sum[0],n_sum[1]);
}
vector<int> dp(nsize,0);
dp[0]=n_sum[0];
dp[1]=max(dp[0],n_sum[1]);
for(int i=2;i<nsize;++i)
{
dp[i]=max(dp[i-2]+n_sum[i],dp[i-1]);
}
return dp[nsize-1];
}
};
413.等差数列划分
首先我们第一步找到三个数组成等差数列,这个很好找,只要,nums[i]-nums[i-1]=nums[i-1]-nums[i-2] 即可,比如说【1,2,3】。这是第一个数组,记x[2]=1。
那么第二个问题在于,如果在此基础上,我们又找到了一个数[1,2,3,4],此时比刚才多出了几个等差数列呢?[1,2,3,4]本身是一个,【2,3,4】又是一个,即多出了2个,记x[3]=2.
如果再增加一个数呢?[1,2,3,4,5],比上一个,多出了[1,2,3,4,5],[1,3,5]和[3,4,5],多出了3个,记x[4]=3
推出:每增加一个数nums[i],我的x[i]比x[i-1]多1.即x[i]=x[i-1]+1
我们最后只需要把所有的x[i]加在一块儿就行。
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums)
{
if(nums.empty()) return 0;
int n=nums.size();
if(n<3) return 0;
int sum=0;
vector<int> x(n,0);
for(int i=2;i<n;++i)
{
if(nums[i]-nums[i-1]==nums[i-1]-nums[i-2])
{
x[i]=x[i-1]+1;
sum+=x[i];
}
}
return sum;
}
};
这里有一个问题
我一开始写的时候,为了省空间,写了如下错误的代码:
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums)
{
if(nums.empty()) return 0;
int n=nums.size();
if(n<3) return 0;
int sum=0;
int duo=0;
for(int i=2;i<n;++i)
{
if(nums[i]-nums[i-1]==nums[i-1]-nums[i-2])
{
++duo;
sum+=duo;
}
}
return sum;
}
};
这个错在哪里,问题出在我想用一个数duo来替代x这个全0的数组,但是duo在实际上并不是一直递增的,比如[1,2,3,8,9,10]这个情况,【123】是一个等差数组,【8910】也是一个,但是他俩并不同源,这就导致了如果我们如果在检测到[8910]时,如果加2,就默认了其与【123】是同源的等差数列,实际上这个时候duo这个值应该归零从0开始加,才符合。
二维
64.最短路径问题
class Solution {
public:
int minPathSum(vector<vector<int>>& grid)
{
int m=grid.size(),n=grid[0].size();
//创建数组x[i][j]表示到[i,j]点最短距离
//则x[i][j]=min(x[i-1][j],x[i][j-1])+grid[i][j];
vector<vector<int>> x(m,vector<int>(n,0));
for(int i=0;i<m;++i)
{
for(int j=0;j<n;++j)
{
if(i==0 && j==0)
{
x[0][0]=grid[0][0];
}
else if(i==0) //走第一行
{
x[0][j]=x[0][j-1]+grid[0][j];
}
else if(j==0) //走第一列
{
x[i][0]=x[i-1][0]+grid[i][0];
}
else
{
x[i][j]=min(x[i-1][j],x[i][j-1])+grid[i][j];
}
}
}
return x[m-1][n-1];
}
};
542.01矩阵
如果使用广度优先搜索,在全是1的情况下,一个mn的数组,最坏情况的时间复杂度(即全是 1)会达到恐怖的 O(m 2 n 2 )。
另一种更简单的方法是,我们从左上到右下进行一次动态搜索,再从右下到左上进行一次动态搜索。两次动态搜索即可完成四个方向上的查找。
class Solution {
public:
vector<vector<int>> updateMatrix(vector<vector<int>>& mat)
{
if(mat.empty()) return {};
int m=mat.size(),n=mat[0].size();
vector<vector<int>> ans(m,vector<int>(n,100));
//如果一次遍历分上下左右四个方向查找,则很难判断出界的情况
//分两次,第一次从左上到右下更新,第二次从右下到左上更新
for(int i=0;i<m;++i)
{
for(int j=0;j<n;++j)
{
if(mat[i][j]==0) //0
{
ans[i][j]=0;
}
else //1,往左上两个方向找,此时[0,0]位置越界,我们暂时不找,但是可以遍历到[m-1,n-1]
{
if(i>0) //除去第一行
{
ans[i][j]=min(ans[i][j],ans[i-1][j]+1); //上
}
if(j>0) //除去第一列
{
ans[i][j]=min(ans[i][j],ans[i][j-1]+1);//左
}
//至此,除了[0,0],第一行往左找了,第一列往上找了,其他元素往左上两个方向找了
}
}
}
for(int i=m-1;i>=0;--i)
{
for(int j=n-1;j>=0;--j)
{
if(mat[i][j]!=0) //1,往右下两个方向找,此时[m-1,n-1]位置越界,我们刚才找过了,但是可以遍历到[0,0]
{
if(i<m-1) //除去最后一行
{
ans[i][j]=min(ans[i][j],ans[i+1][j]+1); //下
}
if(j<n-1) //除去最后一列
{
ans[i][j]=min(ans[i][j],ans[i][j+1]+1);//右
}
//至此,除了[m-1,n-1],最后一行往右找了,最后一列往下找了,其他元素往右下两个方向找了
}
}
}
//至此,所有元素完成方向遍历
return ans;
}
};
221最大正方形
这题最主要的原理在于这个:
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix)
{
if(matrix.empty() || matrix[0].empty()) return 0;
int m=matrix.size(),n=matrix[0].size();
vector<vector<int>> x(m,vector<int>(n,0)); //x[i,j]表示以[i,j]为右下角的正方形的边长
int length=0;
for(int i=0;i<m;++i)
{
for(int j=0;j<n;++j)
{
if(matrix[i][j]=='1')
{
if(i==0 || j==0)
{
x[i][j]=1;
length=max(1,length);
continue;
}
x[i][j]=min(x[i-1][j-1],min(x[i-1][j],x[i][j-1]))+1;
length=max(length,x[i][j]);
}
}
}
return length*length;
}
};
2.分割类问题
279. 完全平方数
用一个数组x[i]来记录,数字i最少可由多少个完全平方数构成,首先x[1] x[4] x[9] x[16] 我们清楚都等于1,因为它本身就是i完全平方数,我们可以想一下这个其他的x[i]到底跟什么有关。
我们看例1,12这个数x[12]=?
12以下的完全平方数,只有149,有以下两种方法构成12
9+1+1+1
和
4+4+4
可见如果能只用一种完全平方数构成,应该是比用多种构成数量需要的少些。
x[12]=min(x[12-1]+1,x[12-4]+1,x[12-9]+1)=min(x[11]+1,x[8]+1,x[3]+1)=min(3+1,2+1,3+1)=3
例2;
x[13]=min(x[12]+1,x[9]+1,x[4]+1)=x[9]+1=x[4]+1=2;
可见一个数由多少个完全平方数构成,跟x[i-1] 和x[i-4] x[i-9]…有关,并且比其中最小的数大1
class Solution {
public:
int numSquares(int n)
{
vector<int> dp(n+1,INT_MAX);//因为待会儿要取min,所以初值赋max
dp[0]=0;
for(int i=1;i<=n;++i)//对于每个dp[i]
{
for(int j=1;j*j<=i;++j)//j从1开始,找到i以下的最大平方数,依次看i-1,i-4,i-9....找到最小的+1,加1是因为,在完全平方数以上的每个数,比上一个数多dp[1]=1。直到找到下一个完全平方数,即i-j*j==0
{
dp[i]=min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
};
91.解码方法
关键就是把字符串映射成数字,而且每个数字不能以0开头,也不能大于26。
第一种情况,数字x只能以一个数的形式出现:(x非零)
- dp[i]+=dp[i-1]
第二种情况,只能两个数形式出现(x加上上一位乘10在10到26之间)
- dp[i]+=dp[i-2]
第三种情况,两者都能(x非零且x加上上一位乘10在10到26之间)
- dp[i]=dp[i-1]+dp[i-2]
所以合并情况,只要写两个if,第三种情况会跳入两个if
class Solution {
public:
int numDecodings(string s)
{
int n = s.size();
if(s[0]=='0') return 0;
if(n==1) return 1;
vector<int> dp(n+1,0);
dp[0]=1;
s=" "+s;
for(int i=1;i<=n;++i)
//如果其只能单独一个数存在,dp[i]=dp[i-1],只能两个数一块儿,dp[i]=dp[i-2],两者皆可dp[i]=dp[i-1]+dp[i-2]
{
int a=s[i]-'0',b=a+(s[i-1]-'0')*10;
if(a!=0)
dp[i]=dp[i-1];//如果其单独一位有效(即非0)dp[i]=dp[i-1]
if(b>=10 && b<=26) //如果其能组成两位数,这里当i==1时,i-1为空,0-48显然为负数
dp[i]+=dp[i-2];
}
return dp[n];
}
};
139单词拆分
这题注意看示例3,如果我们以dp[i]表示从0到i位置可以拆(在s前先加上空格“ ”),
cat匹配成功dp[3]=true,
cats匹配成功dp[4]=true,
catsand匹配成功,dp[7]=true,
可是最后,如果我们想匹配dog这个单词,前面的catssan就不能true,所以我们每次
dp[i]=dp[i] || dp[i-length]
即,如果i-length不能成,i必定不成。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict)
{
int n=s.size();
vector<bool> dp(n+1,false);//dp[i]表示从0到i-1位置,可以拆成>=1个word,
dp[0]=true;
s=" "+s;
for(int i=1;i<=n;++i)
{
for(auto word:wordDict)
{
int length=word.size();
if(i>=length && s.substr(i-length+1,length)==word )
{
dp[i]=dp[i] || dp[i-length];//比如sandog这个单词,我们在匹配sand的时候假设dp[4]为true,匹配到dog的时候,我们得检查san是不是能够作为一个单词,如果不能,dog就不能单独作为一个单词
}
}
}
return dp[n];
}
};
3.子序列问题
300. 最长递增子序列
最简单的方法:
i从0开始遍历到n-1,每次找i之前的j
即j从0遍历到i
如果nums[i]>nums[j],代表此时他们构成一个递增的子列,
dp[i]=max(dp[i],dp[j]+1)
即如果,dp[i]本身已经在前面找到了更长的子列,就不需要把其放在短子列后面。
class Solution {
public:
int lengthOfLIS(vector<int>& nums)
{
int max_length=1,n=nums.size();
if(n==1) return max_length;
vector<int> dp(n,1);
for(int i=0;i<n;++i)
{
for(int j=0;j<i;++j)
{
if(nums[i]>nums[j])
{
dp[i]=max(dp[i],dp[j]+1);
}
}
max_length=max(max_length,dp[i]);
}
return max_length;
}
};
这种方法这个击败度,显然比较垃圾。因为其时间复杂度为O(n^2)
我们优化一下。
方法二:
我们要维护一个数组dp,时刻保证其是最长子列。如何保证?我们用贪心的思想,要想子列最长,就要其增加的越缓慢,即最好每前后两个数差值最小,且最后一个数也最小。
所以,我们的策略是:
一开始第一个数为dp[0]
遍历所有数,如果nums[i]>dp数组的最后一个数,就把其放到dp最后去
反之,这个数比dp最后一个数小,我们要找到dp中第一个比nums[i]大的数,将其换为nums[i]。每次查找dp时可使用二分查找,这样的时间复杂度为O(N* log N)
class Solution {
public:
int lengthOfLIS(vector<int>& nums)
{
int n=nums.size();
if(n==1) return 1;
vector<int> dp;
dp.push_back(nums[0]);
for(int i=1;i<n;++i)
{
if(nums[i]>dp.back())
{
dp.push_back(nums[i]);
}
else//二分查找dp中第一个比nums[i]大的数,替换
{
int l=0,r=dp.size()-1;
while(l<r)
{
int mid=(l+r)/2;
if(dp[mid]<nums[i]) l=mid+1;
else r=mid;
}
dp[l]=nums[i];
}
}
return dp.size();
}
};
1143.最长公共子序列
class Solution {
public:
int longestCommonSubsequence(string text1, string text2)
{
int m=text1.size(),n=text2.size();
if(text1==text2) return m;
text1=" "+text1;
text2=" "+text2;
//dp[i][j]表示到text1的i位置和text2的j位置,两者最长公共子序列为多长
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(int i=1;i<=m;++i)
{
for(int j=1;j<=n;++j)
{
if(text1[i]==text2[j])
{
dp[i][j]=dp[i-1][j-1]+1;
//cout<<"= "<<"dp "<<i<<' '<<j<<"="<<dp[i][j]<<endl;
}
else
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
//cout<<"!= "<<"dp "<<i<<' '<<j<<"="<<dp[i][j]<<endl;
}
}
}
return dp[m][n];
}
};
4.背包问题(推荐先看416问题,理解了累加和的可能性,背包问题会简单理解一些)
背包问题是一种组合优化的 NP 完全问题:有 N 个物品和容量为 W 的背包,每个物品都有自己的体积 w 和价值 v,求拿哪些物品可以使得背包所装下物品的总价值最大。如果限定每种物品只能选择 0 个或 1 个,则问题称为 0-1 背包问题;如果不限定每种物品的数量,则问题称为无界背包问题或完全背包问题。
4.1.1 0-1背包问题——dp[i][j]=max(dp[i-1][j],dp[i-1][[j-w]+v);
以 0-1 背包问题为例。我们可以定义一个二维数组 dp存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。在我们遍历到第 i 件物品时,在当前背包总容量为 j 的情况下,如果我们不将物品 i 放入背包,那么 dp[i][j]= dp[i-1][j],即前 i 个物品的最大价值等于只取前 i-1 个物品时的最大价值;如果我们将物品 i 放入背包,假设第 i 件物品体积为 w,价值为 v,那么我们得到 dp[i][j] = dp[i-1][j-w] + v。我们只需在遍历过程中对这两种情况取最大值即可,总时间复杂度和空间复杂度都为 O(NW)。
int packquestion(vecor<int> weights,vector<int> values,int n,int total_w)
{
vector<vector<int>> dp(n+1,vector<int>(total_w+1,0));
for(int i=1;i<=n;++i)//遍历到第i件物品
{
int w=weights[i-1],v=values[i-1];
for(int j=1;j<=total_w;++j)//当前背包总容量为j,即讨论前i个物品的总容量可不可能是j
{
if(j>=w)//当j增长到w,这个时候我们讨论可不可能现在的总容量为j,如果我不拿,则前i-1个物品质量为j即可,如果我要拿这个v,必须要前i-1个物品的质量为j-w,所以这个j-w务必要大于等于0(j==w即只放一个当前物品的情况)
{
dp[i][j]=max(dp[i-1][j],dp[i-1][[j-w]+v);
}
else//当前物品的重量是w,在j增长到w之前,所有的可能性都照抄上一行
{
dp[i][j]=dp[i-1][j];
}
}
return dp[n][total_w];
}
4.1.2 空间压缩0-1背包问题
我们可以进一步对 0-1 背包进行空间优化,将空间复杂度降低为 O(W)。
如图所示,假设我们目前考虑物品 i = 2,且其体积为 w = 2,价值为 v = 3;对于背包容量 j,我们可以得到 dp[2][j]= max(dp[1][j], dp[1][j-2] + 3)。
这里可以发现我们永远只依赖于上一排 i = 1 的信息,之前算过的其他物品都不需要再使用。
因此我们可以去掉 dp 矩阵的第一个维度,在考虑物品 i 时变成 dp[j]= max(dp[j], dp[j-w] + v)。
注意:
这里要注意我们在遍历每一行的时候必须逆向遍历,这样才能够调用上一行物品 i-1 时 dp[j-w] 的值;若按照从左往右的顺序进行正向遍历,则 dp[j-w] 的值在遍历到j 之前就已经被更新成物品 i 的值了。
注意:这里如果你不太理解为什么需要逆序遍历,我们来解释一下,意思是说,我们用dp[j]= max(dp[j], dp[j-w] + v)这个式子,其中左右两边的dp[j]和dp[j]并不是相等的,为什么?
因为左值中,其是表示第i时刻的值
而在右值中,dp[i]表示的是第“i-1”时刻的值,他俩并不是表示同一时刻的值,且dp[j-w]这个值也是第i-1时刻的,如果你顺序遍历的话,当你遍历到第i时刻,右边的两个值是不是在上一时刻已经被更新了?所以其是不是已经时第i时刻的值了?是不是和我们想要的上一个时刻不符?所以你顺序遍历就导致了右值是第i时刻的值并不是我们想要的第i-1时刻的值。
int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
vector<int> dp(W + 1, 0);
for (int i = 1; i <= N; ++i)
{
int w = weights[i-1], v = values[i-1];
for (int j = W; j >= w; --j)
{
dp[j] = max(dp[j], dp[j-w] + v);
}
}
return dp[W];
}
总结:0-1 背包对物品的迭代放在外层,里层的体积或价值逆向遍历;
4.2.1 完全背包问题dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v)
在完全背包问题中,一个物品可以拿多次。
假设我们遍历到物品 i = 2,且其体积为 w = 2,价值为 v = 3;对于背包容量 j = 5,最多只能装下 2 个该物品。
那么我们的状态转移方程就变成了 dp[2][5] = max(dp[1][5], dp[1][3] + 3, dp[1][1] + 6)。如果采用这种方法,假设背包容量无穷大而物体的体积无穷小,我们这里的比较次数也会趋近于无穷大,远超 O(NW) 的时间复杂度。如下图所示。
实际上,我们来看一下dp[2][3],dp[2][3]等于什么呢,其=max(dp[1][3],dp[2][1]+3)
也就是说,我们在考虑23的时候,其实13和21已经被考虑过了,我们深度优先继续看,21的时候其实11也被考虑过了。
因此,如下图所示,对于拿多个物品的情况,我们只需考虑 dp[2][3] 即可,即 dp[2][5] = max(dp[1][5], dp[2][3] + 3)。
这样,我们就得到了完全背包问题的状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v),其与 0-1 背包问题的差别仅仅是把状态转移方程中的第二个 i-1 变成了 i。
int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; ++i)
{
int w = weights[i-1], v = values[i-1];
for (int j = 1; j <= W; ++j)
{
if (j >= w)
{
dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v);
}
else
{
dp[i][j] = dp[i-1][j];
}
}
}
return dp[N][W];
}
4.2.2 空间压缩完全背包问题
同样的,我们也可以利用空间压缩将时间复杂度降低为 O(W)。
这里要注意我们在遍历每一行的时候必须正向遍历,因为我们需要利用当前物品在第 j-w 列的信息。
int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
vector<int> dp(W + 1, 0);
for (int i = 1; i <= N; ++i)
{
int w = weights[i-1], v = values[i-1];
for (int j = w; j <= W; ++j)
{
dp[j] = max(dp[j], dp[j-w] + v);
}
}
return dp[W];
}
总结:完全背包对物品的迭代放在里层,外层的体积或价值正向遍历。
例题:0-1背包
416.分割等和子集
显然是0-1背包问题。因为分割的话不可能复用。
算出所有数总和为sum,我们要选出几个物品使其累加和为target=sum/2.所以如果这个sum是一个奇数,其必定不能被分成两个相等的数。此时返回false。
我们定义一个二维的bool型数组dp[i][j]来表示,前i个数有没有累加和为j的子数组。
方法一:
按照官方题解,最简单的理解方式为:
对于每一个j,在遍历到j之前的数时,我们当前结果照抄
等于j时相当于只选一个,直接置为true
大于j时我们就看之前的i的累加情况
即得到下图这样的转移表:
由此写出这样的代码:
class Solution {
public:
bool canPartition(vector<int>& nums)
{
int sum=accumulate(nums.begin(),nums.end(),0);
int target=sum/2,n=nums.size();
if(sum%2 ||n==1) return false;
//dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j的可能性。
vector<vector<bool>> dp(n,vector<bool>(target+1,false));
for(int i=0;i<n;++i)
{
if(i==0)
{
if(nums[i]<=target)
{
dp[0][nums[i]]=true;
}
continue;
}
for(int j=1;j<=target;++j)
{
if(j<nums[i])
dp[i][j]=dp[i-1][j];
else if(nums[i]==j)
dp[i][j]=true;
else //nums[i]<j
dp[i][j]=dp[i-1][j] ||dp[i-1][j-nums[i]];
}
}
return dp[n-1][target];
}
};
方法二:
空间压缩,我们上面解释过了,01背包空间压缩的话,内循环需要逆序遍历。
class Solution {
public:
bool canPartition(vector<int>& nums)
{
int sum=accumulate(nums.begin(),nums.end(),0);
int target=sum/2,n=nums.size();
if(sum%2 ||n==1) return false;
//dp[j]表示当前和为j的可能性。
vector<bool> dp(target+1,false);
dp[0]=true;
for(int i=0;i<n;++i)
{
for(int j=target;j>=nums[i];--j)
{
dp[j]=dp[j] ||dp[j-nums[i]];
}
}
return dp[target];
}
};
可以看见,由此带来的优化是肉眼可见的。
474. 一和零
我们的物品是每一个字符串,而每个物品有两个维度的重量,0维度的重量小于m,1维度的重量小于n,每个物品的价值都是1.
我们每到一个物品,只有选或者不选两种可能性,所以要么等于上一次的值,要么等于拿了之前的值+1
dp[i][j]=max(dp[i][j],dp[i-num0][j-num1]+1)
注意这里我们是已经降维的结果,即已经空间压缩过的结果,因为我们是逆序遍历mn的,不然得是个三维数组(再次注意这里的物品有两个维度,所以这个式子和上一题的一维数组是一样的)
我们依次遍历每个str,对每个str进行两层for,第一个for从m遍历到num0,第二层for从n遍历到num1.不断更新dp[i][j]的值。
这里dp[i][j]即我们用了i个0和j个1之后,所选子集的最大size。
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
int s=strs.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));//dp[i][j]表示用了i个0和j个1最大子集的大小。
int num0,num1;
for(int i=0;i<s;++i)//物品
{
auto [num0,num1]=findnum(strs[i]);
for(int n0=m;n0>=num0;--n0)//每个物品有两个维度的重量,每个物品的value为1,对于每个物品,我们都逆序遍历他的两个维度
{
for(int n1=n;n1>=num1;--n1)
{
dp[n0][n1]=max(dp[n0][n1],dp[n0-num0][n1-num1]+1);
}
}
}
return dp[m][n];
}
pair<int, int> findnum(string s)
{
int num0=0,num1=0;
for(auto i:s)
{
if(i=='0') ++num0;
else if(i=='1') ++num1;
}
return make_pair(num0,num1);
}
};
例题:完全背包
322. 零钱兑换
我们定义一个一维数组,直接空间压缩,因为这是一个完全背包问题,且不需要考虑组合顺序,所以我们外层循环数组,内层循环target(如果我们需要考虑顺序需要交换内外层),且内层循环要正序。
dp[i]=min(dp[i],dp[i-c]+1)注意这里右式中的dp[i]为上一次的状态,但dp[i-c]为当前状态,不懂可以回看4.1-4.2
我们来考虑一些特殊情况,如果数组是空的直接返回-1,我们在初始化数组的时候,dp【0】肯定是要置为0的(如果你不知道为什么,你跑第一个数就知道了,因为第一个数i-c==0)
我们在初始化数组的时候,最好将所有数初始化为int_max-1,而不是int_max,因为万一你取到哪个没有被更新的dp,他此时的值为int_max的化,你将他加一很明显溢出了。
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
if(coins.empty()) return -1;
vector<int> dp(amount+1,INT_MAX-1);
dp[0]=0;
for(auto c:coins)
{
for(int i=c;i<=amount;++i)
{
dp[i]=min(dp[i],dp[i-c]+1);
}
}
return dp[amount]==INT_MAX-1? -1:dp[amount];
}
};
5.字符串问题
72. 编辑距离
这和1143的最长公共子序列一样,我们用dp[i][j]表示word1到i位置,word2到j位置位置最多需要编辑几步。
每次指向的两个元素,只有相同和不相同两种可能:
-
相同,则dp[i][j]=dp[i-1][j-1]
-
不相同,则需要进行操作,因为每次我们都必定只需要考虑末位带来的影响,所以最后一位:
-
修改、替换操作 dp[i][j]=dp[i-1][j-1]+1
-
插入位置i操作和删除位置j操作其实是一样的:dp[i][j]=dp[i][j-1]+1 (因为此时还没有i+1)
-
插入位置j和删除位置i操作一样:dp[i][j]=dp[i-1][j]+1
class Solution {
public:
int minDistance(string word1, string word2)
{
int m=word1.size(),n=word2.size();
word1=" "+word1;
word2=" "+word2;
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(int i=0;i<=m;++i)
{
for(int j=0;j<=n;++j)
{
if(i==0)
{
dp[0][j]=j;
}
else if(j==0)
{
dp[i][0]=i;
}
else if(word1[i]==word2[j])
{
dp[i][j]=dp[i-1][j-1];
}
else
{
dp[i][j]=1+min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j]));
}
}
}
return dp[m][n];
}
};
650. 只有两个键的键盘
除了n==1的情况,其他所有的情况,第一步必定是copy all,所以我们可以从2开始讨论。最坏的情况我们每次都复制这一个A,所以n最多需要n次。实际肯定是不会超过n 的.我们建立dp数组,对于每个n我们从2开始考虑(因为2是最小的质数),一旦发现某个数可以整除这个n,我们只需要用这个数反复操作即可。
我们可以简单推一下:
- 0
- 2
- 3
- 这里我们数到2的时候发现2可以整除,所以用2复制一次,粘贴一次,2+2得到4
- 5
- 这里我们数到2的时候发现可以整除,用2复制一次,粘贴两次,2+3得到5
- 7
- 数到2的时候发现可以整除,用2复制一次,粘贴三次,2+4=6
- 数到3的时候发现3可以整除,用3复制一次,粘贴两次,3+3=6
所以对于所有的i我们先设dp[i]=i,然后j从2开始找,一直找到根号i向下取整。
如果能找到i%j==0,则我们用j复制就可以得到i,且用较小的j复制就可以结束了,没必要用2j,4j去算。
我们发现,dp[i]=dp[j]+一个数
这个数其实等于dp[i/j],这是怎么发现的?
我们看:
假设我们现在知道了dp[2]=2,我们是怎么知道dp[6]或者dp[8]的?
2个:AA
6个:AA AA AA
8个:AA AA AA AA
这其实和从一个A 到三个和4个的过程是一样的:
1个:A
3个:A A A
4个:AA AA
由此我们知道,我们要的这个数和从1变到i/j这个数,需要的次数是一样的。
class Solution {
public:
int minSteps(int n) {
if(n==1) return 0;
vector<int> dp(n+1);
int g_n=sqrt(n);
for(int i=2;i<=n;++i)
{
dp[i]=i;
for(int j=2;j<=g_n;++j)
{
if(i%j==0)
{
dp[i]=dp[j]+dp[i/j];
break;
}
}
}
return dp[n];
}
};
6.股票问题
121. 买卖股票的最佳时机
class Solution {
public:
int maxProfit(vector<int>& prices)
{
int n=prices.size();
if(n==1) return 0;
int buy=prices[0],money=0;
for(int i=1;i<n;++i)
{
money=max(prices[i]-buy,money);//先看今天能不能卖,能卖的话最大利润是多少
buy=min(buy,prices[i]); //再看今天能不能买,如果今天比买价低就能买
}
return money;
}
};
188. 买卖股票的最佳时机 IV
我们之前做过和这题类似的,但是不同点在于没有限制交易次数,如果我们可以疯狂操作,那么我们就可以吃到所有上涨,就不需要用动态规划,只需要找到所有上涨曲线就行,但是这里限制了交易次数我们需要重新考虑一下怎么操作利润最大。
我一开始的想法是这样的,想着如果第二天跌我们今天就抛售,赚取所有的上涨,之后再按利润排序,取操作次数个涨幅,但是这样做有问题,在于如果其第二天跌了,第三天反而涨的比第一天还高,且我们的k刚好少了这一次操作次数,那就会导致我们实际上“无效操作了一波”
所以以下代码是错的:
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n=prices.size();
if(k==0 || n<2) return 0;
vector<int> money;
int buy=prices[0];
for(int i=1;i<n;++i)
{
if(prices[i]<prices[i-1] &&(i<n-1 && prices[i+1]<prices[i-1]))
{
money.push_back(prices[i-1]-buy);
buy=prices[i];
}
if(i==n-1 &&prices[n-1]>buy)
{
money.push_back(prices[n-1]-buy);
}
}
sort(money.begin(),money.end());
int m=money.size();
if(k>=m)
return accumulate(money.begin(),money.end(),0);
int x=0;
for(int i=m-1;i>m-1-k;--i)
{
x+=money[i];
}
return x;
}
};
官方题解解释的比较清楚:
我们发现可以去掉第一维度,即所有右式种的sell 和buy都代表[i-1]状态的
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n=prices.size();
if(k==0 || n<2) return 0;
vector<int> buy(k+1,INT_MIN),sell(k+1,0);//buy[j] 表示在第 j 次买入时的最大收益,sell[j] 表示在第 j 次卖出时的最大收益。
for(int i=0;i<n;++i)
{
for(int j=1;j<=k;++j)
{ //我们的转移方程要时刻保持利益最大化
buy[j]=max(buy[j],sell[j-1]-prices[i]);
//不买此时就是buy[i-1][j],降维为buy[j],
//买就代表现在手头没有,所以上一次的操作就是卖出,用上一次的利润减去现在的价格即买了之后的利润,sell[j-1]-price[i]
sell[j]=max(sell[j],buy[j]+prices[i]);
//卖就代表手头必定有,上一次的操作必定是买,用上次剩下的利润加上今天卖出price[i]即得到总利润
//不卖就说明之前卖出的价格更好
}
return sell[k];
}
};
714 买卖股票的最佳时机含手续费
class Solution {
public:
int maxProfit(vector<int>& prices, int fee)
{
vector<int> dp(2);
dp[0]=-prices[0];//0代表今天结束有股票在手的利润
dp[1]=0;//无
for(int i=1;i<prices.size();++i)
{
dp[0]=max(dp[0],dp[1]-prices[i]); //有分两种可能,要么昨天就有了,要么昨天没有今天刚买的
dp[1]=max(dp[1],dp[0]+prices[i]-fee);//没有也两种,要么昨天就没有,要么昨天有今天刚卖。
}
return dp[1];
}
};
309最佳买卖股票时机含冷冻期
这题应该涉及到三个状态之间的转换,持有股票,不持有股票且在冷冻期,不持有且不在冷冻期。
分别记为f0,f1,f2,定义为某天结束之后其处于哪个状态的最大利润。
则:
f0=max(f0,f2-price);//要么前一天已经有的,或者是今天买的,今天买的意味着前一天处于状态f2,所以用f2减去今天的开销price
f1=f0+price;//今天结束了开始冷却必定是因为今天卖出了,意味着前一天处于f0所以用f0+今天的利润price
f2=max(f1,f2);//说明今天无操作,如果昨天卖出今天处于冷冻期,则今天利润为昨天的f1,如果不在冷冻期,则为上次的f2
//最后我们要返回的是
return max(f1,f2);
//因为如果最后一天你还有股票必然没有意义,最后一天必然得清仓
完整代码:(注意上面伪代码中,左式为今天状态而右式表示的是昨天的状态,所以需要两组变量来更新,不然f2在取值的时候会取到今天已经更新过的f1)
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n=prices.size();
if(n<=1) return 0;
int f0=-prices[0],f1=0,f2=0,nf0,nf1,nf2;
for(int i=1;i<n;++i)
{
nf0=max(f0,f2-prices[i]);
nf1=f0+prices[i];
nf2=max(f1,f2);
f0=nf0;
f1=nf1;
f2=nf2;
//cout<<"第"<<i<<"天:"<<f0<<" "<<f1<<" "<<f2<<endl;
}
return max(f1,f2);
}
};
练习
213. 打家劫舍 II
这一题最主要的问题是其与198相比出现了环,即我们如果打劫第一家就不能打劫最后一家,反之亦然,那么我们可以分两次拆分这个问题,第一次从第一家数到倒数第二家,第二次从第二家数到最后一家,这样就不会出现环的问题
class Solution {
public:
int rob(vector<int>& nums)
{
int n=nums.size();
if(n==0) return 0;
else if(n==1) return nums[0];
int max_money=max(startrob(0,n-2,nums),startrob(1,n-1,nums));
return max_money;
}
int startrob(int start,int end,vector<int> nums)
{
if((end-start)<=1) return max(nums[start],nums[end]);
vector<int> dp(end-start+2,0);//dp[i]=max(dp[i-1],dp[i-2]+nums[i])
dp[start]=nums[start];
dp[start+1]=max(nums[start],nums[start+1]);
for(int i=start+2;i<=end;++i)
{
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[end];
}
};
可见内存有优化的空间。
因为这个dp只跟前两个状态有关,所以我们三个数就可以代替dp数组,由此我们把空间复杂度从O(N)降到了O(1).
class Solution {
public:
int rob(vector<int>& nums)
{
int n=nums.size();
if(n==0) return 0;
else if(n==1) return nums[0];
int max_money=max(startrob(0,n-2,nums),startrob(1,n-1,nums));
return max_money;
}
int startrob(int start,int end,vector<int> nums)
{
if((end-start)<=1) return max(nums[start],nums[end]);
int a=nums[start];
int b=max(nums[start],nums[start+1]);
for(int i=start+2;i<=end;++i)
{
int x=max(b,a+nums[i]);
a=b;b=x;
}
return b;
}
};
53. 最大子序和
基本思路:
class Solution {
public:
int maxSubArray(vector<int>& nums)
{
int n=nums.size();
if(n==1) return nums[0];
vector<int> dp(n,0);
dp[0]=nums[0];
int maxnum=dp[0];
for(int i=1;i<n;++i)
{
dp[i]=max(dp[i-1]+nums[i],nums[i]);
maxnum=max(dp[i],maxnum);
}
return maxnum;
}
};
优化空间从O(N)到O(1):因为每个dp只与上一个有关,所以两个变量即可替代一维dp数组:
class Solution {
public:
int maxSubArray(vector<int>& nums)
{
int n=nums.size();
if(n==1) return nums[0];
int qian=nums[0],hou;
int maxnum=qian;
for(int i=1;i<n;++i)
{
hou=max(qian+nums[i],nums[i]);
maxnum=max(hou,maxnum);
qian=hou;
}
return maxnum;
}
};
343. 整数拆分
class Solution {
public:
int integerBreak(int n)
{
vector<int> dp(n+1);
dp[1]=1;
int s;
for(int i=2;i<=n;++i)
{
int b=i/2;
for(int j=1;j<=b;++j)//第一个数是j第二个数为i-j=s
{
s=i-j;
dp[i]=max(j*s,max(dp[i],j*dp[s]));
}
}
return dp[n];
}
};
583. 两个字符串的删除操作
class Solution {
public:
int minDistance(string word1, string word2)
{
int s1=word1.size(),s2=word2.size();
word1=" "+word1;
word2=" "+word2;
vector<vector<int>> dp(s1+1,vector<int>(s2+1,0));
//边界处理
for(int i=0;i<=s2;++i)
{
dp[0][i]=i;
}
for(int j=1;j<=s1;++j)
{
dp[j][0]=j;
}
for(int i=1;i<=s1;++i)
{
for(int j=1;j<=s2;++j)
{
if(word1[i]==word2[j])
{
dp[i][j]=dp[i-1][j-1];
}
else
{
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+1;
}
}
}
return dp[s1][s2];
}
};
646. 最长数对链
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(),sum=1,last1=pairs[0][1];
for(int i=1;i<n;++i)
{
if(pairs[i][0]>last1)
{
++sum;
last1=pairs[i][1];
}
}
return sum;
}
};
494 目标和
我们记:
数组nums所有数的和为sum(这里注意sum是理论上的最大值)
加负号的元素所代表的的负数和为 neg(neg>=0),即选某几个数前面加“-”
那么剩下的自然是当正数(前面加“+”),和为sum-neg
我们要求的target是什么,你得用正数和减去负数和(再强调一遍neg>=0)吧
target=(sum-neg)-neg=sum-2*neg
换算一下,我们推出neg=(sum-target)/2.
即我们的sum-target必须是个非负整数,且其必须是个偶数。
- 如何理解其必须是个非负整数,你想一下如果target比sum还大你必然是得不到的啊,因为最大值就是sum了
- 为什么一定是个偶数,这个其实不难理解,比如给你三个1,前面必须都加上正负号,你无论怎么加,都得不到2对吧。1个1只能得到正负1,2个1能得到正负2和0,3个1只能得到正负1和3,还有0
好,那么现在我们回想一下之前的最简单的两数之和的问题,之前的那个题目是给你一个target,你每找一个nums[i],是不是转换成在数组中找存不存在另一个target-nums[i]?
同理,我们现在是不是就在找数组中有没有几个元素 的和为neg存在。
这个时候这个问题就变成了一个0-1背包问题:
定义数组dp表示前i个元素的和为j的方案数量。我们最终的答案,就是dp[neg],这里直接进行了0-1背包的空间压缩(代表着内层循环需要逆序)
转移方程好理解,就是看我们能不能从j-nums[i]能转换过来,我们来考虑一下边界问题,即考虑j-nums[i]会不会越界的问题。
如果j比这个Nums[i]还小,那么我们取不到j-nums[i]这个值,dp[j]只能等于上一次循环的值。
反之,我们首先继承上次的值,而且还要加上从j-nums[i]转换过来的情况,即dp[j]=dp[j]+dp[j-nums[i]]
再回过头,dp[0]到底是多少,我们按照我们的定义,元素和为0,即没有元素可选的时候,即数组开头的前面一位,方案只有一个,就是不选。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(),nums.end(),0);
int vip=sum-target;
if(vip<0 || vip%2!=0) return 0;
int neg=vip/2;//neg就是负数和,我们对于每一个target其实就是在找neg存不存在,即数组中有没有几个数的和为neg
vector<int> dp(neg+1);
dp[0]=1;
for(auto i:nums)
{
cout<<i<<endl;
for(int j=neg;j>=i;--j)
{
dp[j]+=dp[j-i];
}
}
return dp[neg];
}
};
2022.3.8加更 5. 最长回文子串
这里其实重点在理解什么叫回文串:
大家可以参考一下第4题的找中位数那题,那题的难点在于区分奇偶但却合并处理,对于回文串来说其实是一样的。
可以思考一下:
- 一个字符是回文串,他作为回文串的中心点,是一个“奇数”;
- 当出现两个字符“aa”的时候呢?作为“偶数”情况,其中心点并不真实存在在某个字母上,所以回文串的中心点在最中间的两个字母上。
- 由此我们得出:所有的回文串中心必定是1个字母/2个相同的字母。
我们先给出通俗解法:用动态规划
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if(n==1) return s;
int longest_size_of_ans=1,begin_of_ans=0;
vector<vector<bool>> dp(n,vector<bool>(n));
for(int i=0;i<n;++i)
{
dp[i][i]=true; //长度为1的回文串
if(i<n-1 && s[i]==s[i+1])
{
dp[i][i+1]=true; //长度为2的回文串
begin_of_ans = i;
longest_size_of_ans =2;
}
}
int j;
for(int length=3;length<=n;++length)
{
for(int i=0;i<n;++i)
{
j=i+length-1;
if(j>=n) break;
if(s[i]==s[j] &&dp[i+1][j-1] )//说明是回文串
{
dp[i][j]=true;
if(length>longest_size_of_ans)
{
longest_size_of_ans =length;
begin_of_ans=i;
}
}
}
}
return s.substr(begin_of_ans,longest_size_of_ans);
}
};
刚才我们说了,回文串其实和中位数一样存在奇偶中心的问题,我们可不可以从中心往外扩展?因为所有的回文串中心必定是1个字母/2个相同的字母。
注意:偶数中心是不一定有的,只有连续两个相同字符才能作为偶数中心。
class Solution {
public:
pair<int,int> expand(const string &s,int left,int right)
{
int n =s.size();
while(left>=0 && right<n && s[left]==s[right])
{
--left;
++right;
}
return {left+1,right-1};
}
string longestPalindrome(string s) {
int n =s.size();
int start=0,end=0;
for(int i=0;i<n;++i)
{
auto [l1,r1]=expand(s,i,i);//奇数中心
if(i<n-1 && s[i]==s[i+1])
{
auto [l2,r2] =expand(s,i,i+1);
if(r2-l2>end-start)
{
start=l2;
end =r2;
}
}
if(r1-l1>end-start)
{
start=l1;
end =r1;
}
}
return s.substr(start,end-start+1);
}
};
2022.3.11加更 10. 正则表达式匹配
这个题目建议大家学习这个思路,置一张表:
即在s和p前面加个空格表示初始状态。这是我们在很多字符串动态规划的常用技巧。
这里对初始化做一个解释:
- 首先空和空匹配这个很好理解,所以00是true
- 而后第一列,代表s从A到aaa…ab与空对比,显然全false
- 主要看第一行,即空与p匹配的问题,我们看空和英文字母必定是不匹配的。那么我们思考两个特殊字符
- 空和点.: 这个好理解,点代表一个字符,这也是不匹配的
- 空和星*呢:出现 星,就要看操作了,可以同行往左看两个,也可同列(在s[i]和p[j-1]相同的情况下)往上看一个因为星可以代表0个或者多个。即可以把A星变成A,也可以是AAAAA。。。我个人理解可以A星可以代表A这个字母没有出现过,这一种情况,所以A星,一共有三种情况,1.代表空,2.代表一个A,3.代表多个A
最终的状态转移过程每一步的判度应该和上表一致。
所以思路: - 建立横竖+1的表
- 初始化第0行和第0列
- 进行状态转移,如果s和p字母相等/p刚好是点,这两种情况下,状态从左上角转移过来。
- 如果字母不等必定不等
- 如果p是星,往左看两个,代表A没有出现的情况,如果仍然是false,还有另一种可能将其变为true
- 当s[i]与p[j-1]相同的情况,A星可以==A。此时往上数一个(注意循环横竖都从1开始,因为初始化已经做过了,不存在越界情况)
class Solution {
public:
bool isMatch(string s, string p) {
int m=s.size(),n=p.size();
vector<vector<bool>> dp(m+1,vector<bool>(n+1));
//p出现*,第一种情况舍弃*前面的字母,代表字母没有出现的情况(即把A* -> 空),此时同行往前倒两个,看是不是True
//如果上述为false还有一种可能将其变true,即把A*-> A
dp[0][0]=true;
for(int j=1;j<n+1;++j) //初始化第0行情况,只有p[j]为*能变true
{
if(p[j-1]=='*')
{
if(j==1) dp[0][j]=true;
else
{
dp[0][j]=dp[0][j-2];
}
}
}
//初始化第0列省略,因为空和所有的p都不会匹配
char ch1,ch2;
for(int i=1;i<m+1;++i)
{
ch1 =s[i-1];
for(int j=1;j<n+1;++j)
{
ch2 =p[j-1];
if(ch1 == ch2 ||ch2=='.') dp[i][j]=dp[i-1][j-1];//字符匹配情况,状态继承左上角
else if (ch2 =='*')//出现*的情况
{
if(j>1)//可以往左倒2的情况,代表将A*变成空
{
if(dp[i][j-2]) dp[i][j]=true;
else
{
if(p[j-2]==ch1 || p[j-2]=='.') dp[i][j]=dp[i-1][j];
}
}
}
}
}
return dp[m][n];
}
};
2022.4.3加更 32. 最长有效括号
这一题读题很重要不是让你数有多少合规的括号,而是最长连续有多少括号
一看最长连续,很明显的思路是动态规划,但这题动态规划还是比较难的。我们思路整理如下:
- 常规思路设size为n
- s前面加空格,从新s的s[2]开始,默认dp[0]=dp[1]=0。dp表示以
- 明确,只有s[i]出现右括号)的时候才有可能匹配到有效左括号。
- 又因为我们从2开始,不存在为空或者i-2越界问题。
- 第一种情况 ,很容易想到,i的前一个位置i-1就是左括号。那么此时i和i-1组成合规匹配。dp[i]=dp[i-2]+2.即为左括号左边一个位置的有效数目+2
- 第二种情况,很容易忽略 ,i的前一个位置是)右括号,也有可能合规,因为会存在**(())这样的情况,所以此时我们记最右边的)为i,我们得看前一个i-1匹配到什么位置,即i-dp[i-1]-1** 这个位置是不是左括号。接下来非常关键了
- 首先成功匹配一个左括号对于总量来说+2,这一点同第一种情况(注意这里左括号左边一个位置为i-dp[i-1]-2),但是对于最长量来说还应当加上内部右括号的有效值,即dp[i-1]
- 所以对于(())的情况,dp[i]=dp[i-dp[i-1]-2]+2+dp[i-1]
class Solution {
public:
int longestValidParentheses(string s)
{
int n=s.size();
if(n<2) return 0;
s=" "+s;
vector<int> temp(n+1);
temp[0]=temp[1]=0;
for(int i=2;i<=n;++i)
{
if(s[i]==')'&&s[i-1]=='(')
{
temp[i]=temp[i-2]+2;
}
if(s[i]==')' &&s[i-1]==')' &&s[i-temp[i-1]-1]=='(')
{
temp[i]=temp[i-temp[i-1]-2]+temp[i-1]+2;
}
}
int ans=-1;
for(auto &i:temp)
{
ans=max(ans,i);
}
return ans;
}
};