动态规划专题

动态规划专题

强化动态规划的学习



前言

动态规划专题,由简到难

一、连续子数组的最大和

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+1n+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,为什么呢,因为可以看成:青蛙先跳到1n级台阶上,再从1n级台阶上跳到n+1级台阶上,最后在加上一步直接跳上n+1,这包含了所有的跳法;而跳到1n级台阶的跳法就可以递归或者动态规划得到;
  • 如此可以假设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)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值