7.算法——动态规划,一维,二维,分割类问题,子序列问题,背包问题,字符串问题,股票问题,练习

算法解释

动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算
解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。
同时,我们也可以对动态规划进行空间压缩,起到节省空间消耗的效果。

在一些情况下,动态规划可以看成是带有状态记录(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,我们只需要用这个数反复操作即可。
我们可以简单推一下:

  1. 0
  2. 2
  3. 3
  4. 这里我们数到2的时候发现2可以整除,所以用2复制一次,粘贴一次,2+2得到4
  5. 5
  6. 这里我们数到2的时候发现可以整除,用2复制一次,粘贴两次,2+3得到5
  7. 7
  8. 数到2的时候发现可以整除,用2复制一次,粘贴三次,2+4=6
  9. 数到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题的找中位数那题,那题的难点在于区分奇偶但却合并处理,对于回文串来说其实是一样的。
可以思考一下:

  1. 一个字符是回文串,他作为回文串的中心点,是一个“奇数”;
  2. 当出现两个字符“aa”的时候呢?作为“偶数”情况,其中心点并不真实存在在某个字母上,所以回文串的中心点在最中间的两个字母上。
  3. 由此我们得出:所有的回文串中心必定是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;

    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值