动态规划专题
强化动态规划的学习
文章目录
前言
动态规划专题,由简到难
一、连续子数组的最大和
1.题目描述:
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
数据范围:
1 <= n <= 10^5
-100 <= a[i] <= 100
要求:
时间复杂度为 O(n),空间复杂度为 O(n)
进阶:
时间复杂度为 O(n),空间复杂度为 O(1)
示例1
输入:
[1,-2,3,10,-4,7,2,-5]
返回值:
18
说明:
经分析可知,输入数组的子数组[3,10,-4,7,2]可以求得最大和为18
示例2
输入:
[2]
返回值:
2
示例3
输入:
[-10]
返回值:
-10
2.解析
方法一(动态规划):
状态定义:
dp[i]表示以i结尾的连续子数组的最大和。所以最终要求dp[n-1] 状态转移方程:dp[i] = max(array[i], dp[i-1]+array[i]) 解释:如果当前元素为整数,并且dp[i-1]为负数,那么当然结果就是只选当前元素 技巧:这里为了统一代码的书写,定义dp[i]表示前i个元素的连续子数组的最大和,结尾元素为array[i-1]
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
int sz=array.size();
vector<int> dp(sz+1,0);
int ret=INT_MIN;
for(int i=1;i<=sz;++i){
dp[i]=max(array[i-1],dp[i-1]+array[i-1]);
ret=max(dp[i], ret);
}
return ret;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(n)
方法二(动态规划空间优化):
思想很简单,就是对下标为i的元素array[i],先试探的加上array[i], 如果和为负数,显然,以i结尾的元素对整个结果不作贡献。 具体过程:
初始化:维护一个变量tmp = 0
如果tmp+array[i] < 0, 说明以i结尾的不作贡献,重新赋值tmp = 0
否则更新tmp = tmp + array[i] 最后判断tmp是否等于0, 如果等于0, 说明数组都是负数,选取一个最大值为答案
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
int sz=array.size();
int tmp=array[0];
int ret=array[0];
for(int i=1;i<sz;++i){
tmp=max(tmp+array[i],array[i]);
ret=max(ret,tmp);
}
return ret;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
二、连续子数组的最大和(二)
1.题目描述:
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,找到一个具有最大和的连续子数组。
1.子数组是连续的,比如[1,3,5,7,9]的子数组有[1,3],[3,5,7]等等,但是[1,3,7]不是子数组
2.如果存在多个最大和的连续子数组,那么返回其中长度最长的,该题数据保证这个最长的只存在一个
3.该题定义的子数组的最小长度为1,不存在为空的子数组,即不存在[]是某个数组的子数组
4.返回的数组不计入空间复杂度计算
数据范围:
1<=n<=10^5
−100<=a[i]<=100
要求:时间复杂度O(n),空间复杂度O(n)
进阶:时间复杂度O(n),空间复杂度O(1)
示例1
输入:
[1,-2,3,10,-4,7,2,-5]
返回值:
[3,10,-4,7,2]
说明:
经分析可知,输入数组的子数组[3,10,-4,7,2]可以求得最大和为18,故返回[3,10,-4,7,2]
示例2
输入:
[1]
返回值:
[1]
示例3
输入:
[1,2,-3,4,-1,1,-3,2]
返回值:
[1,2,-3,4,-1,1]
说明:
经分析可知,最大子数组的和为4,有[4],[4,-1,1],[1,2,-3,4],[1,2,-3,4,-1,1],故返回其中长度最长的[1,2,-3,4,-1,1]
示例4
输入:
[-2,-1]
返回值:
[-1]
说明:
子数组最小长度为1,故返回[-1]
2.解析
方法一(动态规划):
具体做法:
可以用dp数组表示以下标i为终点的最大连续子数组和,则每次遇到一个新的数组元素,连续的子数组要么加上变得更大,要么它本身就更大,因此状态转移为dp[i]=max(dp[i−1]+array[i],array[i]),这是最基本的求连续子数组的最大和。
但是题目要求需要返回长度最长的一个,我们则每次用left、right记录该子数组的起始,需要更新最大值的时候(要么子数组和更大,要么子数组和相等的情况下区间要更长)顺便更新最终的区间首尾,最后根据区间首尾获取子数组。
复杂度分析:
- 时间复杂度: O(n),两次遍历,最坏情况下遍历两次数组
- 空间复杂度: O(n),动态规划辅助数组长度为n
class Solution {
public:
vector<int> FindGreatestSumOfSubArray(vector<int>& array) {
int sz=array.size();
vector<int> dp(sz+1,0);
int maxnum=array[0];
int left=0,right=0;
int resl=0,resr=0;
vector<int> ret;
for(int i=1;i<=sz;++i){
right++;
dp[i]=max(dp[i-1]+array[i-1],array[i-1]);
if(dp[i-1]+array[i-1]<array[i-1]){
left=right-1;
}
if(dp[i]>maxnum ||(dp[i]==maxnum && right-left>resr-resl)){
maxnum=dp[i];
resl=left;
resr=right;
}
}
for(int i=resl;i<resr;++i){
ret.push_back(array[i]);
}
return ret;
}
};
方法二(动态规划空间优化):
具体做法:
方法一虽然时间复杂度达到了进阶要求,但是使用O(n)的空间。
我们注意到动态规划在状态转移的时候只用到了i−1的信息,因此我们可以使用两个变量迭代来代替数组,状态转移的时候更新变量y,该轮循环结束的再更新x为y即可做到每次迭代都是上一轮的dp。
class Solution {
public:
vector<int> FindGreatestSumOfSubArray(vector<int>& array) {
int sz=array.size();
int y=array[0],x=INT_MIN;
int maxnum=array[0];
int left=0,right=0;
int resl=0,resr=0;
vector<int> ret;
for(int i=1;i<=sz;++i){
right++;
if(x+array[i-1]<array[i-1]){
left=right-1;
x=array[i-1];
}
else {
x+=array[i-1];
}
y=max(x,array[i-1]);
if(y>maxnum ||(y==maxnum && right-left>resr-resl)){
maxnum=y;
resl=left;
resr=right;
}
}
for(int i=resl;i<resr;++i){
ret.push_back(array[i]);
}
return ret;
}
};
复杂度分析:
- 时间复杂度: O(n),两次遍历,最坏情况下遍历两次数组
- 空间复杂度: O(1),常数级变量,无额外空间
三、跳台阶
1.题目描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
数据范围:
0≤n≤40
要求:
时间复杂度:O(n) ,空间复杂度: O(1)
示例1
输入:
2
返回值:
2
说明:
青蛙要跳上两级台阶有两种跳法,分别是:先跳一级,再跳一级或者直接跳两级。因此答案为2
示例2
输入:
7
返回值:
21
示例3
输入:
0
返回值:
0
2.解析
方法一(动态规划):
具体做法:
我们用可以考虑第n级台阶,它可以由第n-1级台阶跳1级而来,也可以由第n-2级台阶跳2级而来,那么第n级台阶的方案就是第n-1级的方案数加上第n-2级的方案。不断往前推,每个台阶的方案数都可以由前两个台阶相加得到,这就是得到了斐波那契数列的递推公式:f(n)=f(n−1)+f(n−2)那我们可以用dp数组动态规划不断相加得到。
class Solution {
public:
int jumpFloor(int n) {
if (n <= 1) //从0开始,第0项是1,第一项是1
return n;
vector<int> dp(n + 1);
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2]; //公式不断相加
return dp[n];
}
};
但是这个方法使用了dp数组,空间复杂度为O(n),不满足要求,因此我们对空间优化一下:注意到每次循环只使用到了第i−1个变量和第i−2个变量,那我们可以用两个变量不断滚动来优化。
class Solution {
public:
int jumpFloor(int number) {
if(number==0 || number==1) return number;
int tmp=1,ret=2;
for(int i=3;i<=number;++i){
int t=ret;
ret+=tmp;
tmp=t;
}
return ret;
}
};
复杂度分析:
- 时间复杂度: O(n),一次遍历
- 空间复杂度: O(1),常数级遍历
方法二:矩阵快速幂
具体做法:
对于斐波那契数列,我们可以有如下的推导:
这样我们只要求得基础矩阵的n-2次方与最初的几个元素相乘,取矩阵第一个元素就是我们要求的F(n)。而矩阵的次方,我们可以采用矩阵快速幂,可以参考这篇文章https://blog.nowcoder.net/n/8d2e1a344ef34645b8a3ee45d173ed1c
对于一个矩阵我们可以用如下的方式计算,缩短时间:
比如我们得到它的2次方,后续的2次方就不用再计算了,我们得到4次方后续的也不用计算了,这样计算时间可以缩短到O(log2n)
class Solution {
public:
vector<vector<int>> base = { //基础矩阵
{1, 1},
{1, 0}
};
vector<vector<int>> Mul(vector<vector<int>>& x, vector<vector<int>>& y){ //x矩阵乘上y矩阵
vector<vector<int>> res(2, vector<int>(2, 0));
for(int i = 0; i < 2; i++){ //遍历相乘相加
for(int j = 0; j < 2; j++){
for(int k = 0; k < 2; k++){
res[i][j] = (res[i][j] + x[i][k] * y[k][j]);
}
}
}
return res;
}
vector<vector<int>> Pow(vector<vector<int>>& x, int k){ //矩阵快速幂
vector<vector<int>> res(2, vector<int>(2, 0));
for(int i = 0; i < 2; i++)
res[i][i] = 1; //初始化为单位矩阵
while(k){
if(k & 1)
res = Mul(res, base);
k = k >> 1;
base = Mul(base, base);
}
return res;
}
int jumpFloor(int n) {
if(n <= 1) //从0开始,第0项是1,第一项是1
return n;
vector<vector<int> > a = base;
vector<vector<int> > b = {{2, 1}, {1, 1}}; //F(2) F(1) F(1) F(0)
vector<vector<int> > c = Pow(a, n - 2); //基础矩阵的n-2次方
return Mul(c, b)[0][0]; //F(n)
}
};
复杂度分析:
- 时间复杂度: O(log2n),计算了矩阵的n−2次方,因为使用了矩阵快速幂,缩短到O(log2n)
- 空间复杂度: O(1),所有的矩阵变量都是常数级的
方法三:公式计算
具体做法:
斐波那契数列除了递推公式,还有数学公式:
需要注意的是这个公式其实F(0)=0、F(1)=1,因此我们排除前两种情况后要对n添加1.
class Solution {
public:
int jumpFloor(int n) {
if(n <= 1) //从0开始,第0项是1,第一项是1
return n;
n = n + 1; //迎合公式
return (sqrt(5)/5)*(pow((1+sqrt(5))/2,n)-pow((1-sqrt(5))/2,n));; //公式
}
};
当然上述公式还可以用快速幂来优化,如果我们要计算5^10,常规的算法是5∗5=25,然后再25∗5=125,如此往下,一共是9次运算,即n−1次。但是我们可以考虑这样:5∗5=25(二次)、25∗25=625(四次)、625∗625=…(八次),这是一个二分的思维,运算次数缩减到了log2n次,公式如下:
class Solution {
public:
double Pow(double x, int y){ //快速幂
double res = 1;
while(y){
if(y & 1) //可以再往上乘一个
res = res * x;
x = x * x; //叠加
y = y >> 1; //减少乘次数
}
return res;
}
int jumpFloor(int n) {
if(n <= 1) //从0开始,第0项是1,第一项是1
return n;
n = n + 1; //迎合公式
return (sqrt(5) / 5) * (Pow((1 + sqrt(5)) / 2, n) - Pow((1 - sqrt(5)) / 2, n));; //公式
}
};
复杂度分析:
- 时间复杂度: O(log2n),Pow函数使用了快速幂缩短了计算时间
- 空间复杂度: O(1),常数级变量
四、正则表达式匹配
1.题目描述:
请实现一个函数用来匹配包括’.‘和’‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配
数据范围:
1.str 可能为空,且只包含从 a-z 的小写字母。
2.pattern 可能为空,且只包含从 a-z 的小写字母以及字符 . 和 *,无连续的 ‘*
’。
3.1 <= str.length <= 20
4.1 <= pattern.length <= 30
要求:空间复杂度 O(1),时间复杂度 O(n)
示例1
输入:
“aaa”,“a*a”
返回值:
true
说明:
中间的* 可以出现任意次的a,所以可以出现1次a,能匹配上
示例2
输入:
“aad”,“cad”
返回值:
true
说明:
因为这里 c 为 0 个,a被重复一次, * 表示零个或多个a。因此可以匹配字符串 “aad”。
示例3
输入:
“”,".*
"
返回值:
true
说明:
“.*
” 表示可匹配零个或多个(’*
’)任意字符(’.
’)
示例4
输入:
“aaab”,“aaa*c”
返回值:
false
2.解析
方法一(动态规划):
具体做法:
与此题类似,此题可以采用动态规划的方法来求解。定义二维递推数组dp,其中dp[i][j]
表示字符串str的前i-1
个字符与字符串pattern的前j-1
个字符是否匹配(设dp数组行、列分别为m+1
、n+1
,其中m、n
分别为字符串str和pattern的长度)。
「动态规划算法的关键是确定递推式」,对于此题有如下几种情况:(为方便递推式的表示,dp数组行、列分别为m+1、n+1
,故对于字符串str和pattern的索引需要减1)
具体事例如图所示。
上述几种情况满足其一则说明当前是匹配成功的,故上述几种情况之间是「或」的关系。
经过上述递推关系,在遍历完两个字符串后,dp[m][n]
即为答案。
根据上述思路,实现的代码如下:
class Solution {
public:
bool match(string str, string pattern) {
int m=str.size(); int n=pattern.size();
vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
dp[0][0]=true;
for(int i=2;i<=n;++i){
if(pattern[i-1]=='*'){
dp[0][i]=dp[0][i-2];
}
}
for(int i=1;i<=m;++i){
for(int j=1;j<=n;++j){
if(pattern[j-1]=='.' || str[i-1]==pattern[j-1]) dp[i][j]=dp[i-1][j-1];
else if(pattern[j-1]=='*'){
if(dp[i][j-1]){
dp[i][j]=dp[i][j-1];
}
else if(j>=2 && dp[i][j-2]){
dp[i][j]=dp[i][j-2];
}
else if(dp[i-1][j] && (str[i-1]==pattern[j-2] || pattern[j-2]=='.')){
dp[i][j]=dp[i-1][j];
}
}
}
}
return dp[m][n];
}
};
复杂度分析:
- 时间复杂度: 该方法需要两层循环分别遍历两个字符串,故算法时间复杂度为O(mn),其中m、n分别为str字符串和pattern字符串的长度;
- 空间复杂度: 该方法需要定义大小为(m+1)*(n+1)的dp数组,故空间复杂度为O(mn)。
方法二(递归解法):
具体做法:
基于上述思路,实现的代码如下:
class Solution {
public:
bool match(string str, string pattern) {
if (str.size() && pattern.empty())
return false;
return helper(str, pattern, 0, 0);
}
bool helper(string str, string pattern, int i, int j) {
if (i >= str.size() && j >= pattern.size()) // 两个字符串都匹配完
return true;
if (i < str.size() && j >= pattern.size()) // pattern字符串匹配完了,但str字符串未匹配完
return false;
if ((j < pattern.size() - 1 && pattern[j + 1] != '*') || j + 1 == pattern.size()) { // pattern下一个字符不为星号
if (i < str.size() && (str[i] == pattern[j] || pattern[j] == '.')) // 当前能够匹配
return helper(str, pattern, i + 1, j + 1); // 下一层递归
else return false; // 当前位置匹配失败,返回false
}
// pattern下一个位置为星号
if (i < str.size() && str[i] != pattern[j] && pattern[j] != '.' || i == str.size()) // 当前位置不能成功匹配,说明pattern[j]字符不能被使用
return helper(str, pattern, i, j + 2);
return helper(str, pattern, i, j + 2) || helper(str, pattern, i + 1, j + 2) || helper(str, pattern, i + 1, j); // 当前位置成功匹配,分为星号匹配0次、1次、多次三种情况
}
};
考虑此方法的时间复杂度:在递归函数helper
的最后一个return
语句,若从i=0,j=0
开始分析,则构建的递归树示意图如下(假设在最坏情况下,最后的return
中的三个「或」语句都被调用了):
复杂度分析:
- 时间复杂度: 若将
(i,j)
看做二维空间中的每个点,则整个递归树在最坏情况下可以遍历到从(0,0)
到(m,n)
的所有点的(其中m、n
分别为str字符串和pattern字符串的长度),故时间复杂度为O(mn)
,也即树的结点个数; - 空间复杂度: 递归的空间复杂度是由函数栈空间的层数确定的(因为
helper
函数在「每一层」递归空间复杂度为O(1)
),由于递归过程在「字符串到达末尾」时便终止,故空间复杂度为O(m+n)
,也就是树的高度。
五、跳台阶扩展问题
1.题目描述:
描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。
数据范围:1≤n≤20
进阶:空间复杂度 O(1) , 时间复杂度 O(1)
示例1
输入:
3
返回值:
4
示例2
输入:
1
返回值:
1
2.解析
方法一(动态规划):
状态定义:
- 容易想到:跳
n+1
级台阶的种数等于跳1~n
级台阶所有种数之和加1
,为什么呢,因为可以看成:青蛙先跳到1
到n
级台阶上,再从1
到n
级台阶上跳到n+1
级台阶上,最后在加上一步直接跳上n+1
级,这包含了所有的跳法;而跳到1
到n
级台阶的跳法就可以递归或者动态规划得到; - 如此可以假设
dp[i]
表示跳i
级台阶有多少种跳法,那么状态转移方程为:dp[i+1]=dp[1]+dp[2]+......dp[i]+1
; - 初始化
dp[0]=0,dp[1]=1,dp[2]=2
,另外将前n项和当成一个变量sum
来维护
因此代码如下:
class Solution {
public:
int jumpFloorII(int number) {
int dp[22];
dp[0]=0;dp[1]=1;dp[2]=2;
int sum=3;
for(int i=3;i<=number;++i){
dp[i]=sum+1;
sum+=dp[i];
}
return dp[number];
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(n)
方法二(动态规划空间优化):
针对方法一种的dp
数组,因为每次遍历更新规律一样,因此可以优化成一个变量来代替;
代码如下:
class Solution {
public:
int jumpFloorII(int number) {
if(number<3) return number;
int sum=3,ret;
for(int i=3;i<=number;++i){
ret=sum+1;
sum+=ret;
}
return ret;
}
};
**
- 时间复杂度: O(n)
- 空间复杂度: O(1)