动态规划-part1

目录

动态规划的基本流程:

状态表示

状态转移方程

初始化

填表顺序

返回值

空间优化:

斐波那契数列模型问题

1、第N个泰波那契数

2、三步问题

3、使用最小花费爬楼梯

4.解码方法

路径问题

1、不同路径1

2、不同路径2

3.礼物的最大价值

4.下降路径最小和

5.最小路径和

6.地下城游戏(难度upup)


动态规划的基本流程:

状态表示

动态规划基本都是要定义一个数组,一维或二维,我们通过某种方式将数组填满,数组的某一个值就是我们需要的答案。比如dp数组

而数组的任意一个值所代表的含义,就是状态表示

怎么看出状态表示?

1、题目要求中,会有关键信息,很有可能就是正确的状态表示

2、经验+题目要求,多刷题结合题目要求。

3、要学会分析问题,从问题中找到子问题。

这三个层次基本就是对动态规划的不同熟练程度了。

状态转移方程

上面说了,我们要将数组填满,那怎么填呢,也就是令任意的dp[i]等于什么值呢?

状态转移,从字面意义上,我们可以理解为将状态进行了转移,对应到dp数组,就是dp[i]等于dp的任意一个或多个不同的数组值在经历了一定的变换后的结果,简单的就是比如全部累加,或者平均值等等。

初始化

上面的状态转移中,我们知道dp[i]是1个或多个值经历一定变化后的结果。我们感性的想想,数组最初开辟出来都是0,那不管怎么操作,大概率每个位置还是0吧,那这时候,我们就必须将数组的一些位置手动填上数字,一方面让整个流程跑起来,另一方面,就是以免中途遇到一些没有初始化的值,导致最后的结果变得莫名其妙了。

填表顺序

填当前dp[i]的时候,我们要保证用到的其他状态,已经是填过的或者已经初始化过的,这样才能让结果正确

返回值

题目要求+状态表示。

通过这两者,将dp数组中代表答案的输出即可,比如dp[n]

空间优化:

主要是节省空间的使用。

斐波那契数列模型问题

1、第N个泰波那契数

1137. 第 N 个泰波那契数 - 力扣(LeetCode)

状态表示:

根据题目要求,dp[i]代表第i个泰波那契数

状态转移方程:

dp[i]=dp[i-3]+dp[i-2]+dp[i-1]

初始化:

dp[0]=0,dp[1]=dp[2]=1

填表顺序:

从下标3开始

返回值:

dp[n],第n个泰波那契数

cpp 复制代码
class Solution {
public:
    int tribonacci(int n) {
        vector<int>mp(n+1,0);
        if(n==0)return 0;
        if(n==1||n==2)return 1;
        mp[0]=0,mp[1]=1,mp[2]=1;
        for(int i=3;i<=n;i++)
        {
            mp[i]=mp[i-3]+mp[i-2]+mp[i-1];
        }
        return mp[n];
    }
};

下面是节省空间的写法。

用滚动数组,只用开4位。

cpp 复制代码
class Solution {
public:
    int tribonacci(int n) {
        int mp[4];
        if(n==0)return 0;
        if(n==1||n==2)return 1;
        mp[0]=0,mp[1]=1,mp[2]=1;
        for(int i=3;i<=n;i++)
        {
            int x=3;
            mp[x]=mp[x-3]+mp[x-2]+mp[x-1];
            mp[0]=mp[1];
            mp[1]=mp[2];
            mp[2]=mp[3];
        }
        return mp[3];
    }
};

2、三步问题

面试题 08.01. 三步问题 - 力扣(LeetCode)

状态表示:

根据题目要求,dp[i]代表台阶[i]一共有多少种方法跳上来

状态转移方程:

dp[i]=dp[i-3]+dp[i-2]+dp[i-1]

初始化:

dp[1]=1,dp[2]=2,dp[3]=4

填表顺序:

从下标4开始

返回值:

dp[n],第n个台阶的方法总数

cpp 复制代码
class Solution {
public:
    int waysToStep(int n) {
        vector<int>mp(n+4,0);
        mp[1]=1;
        mp[2]=2;
        mp[3]=4;
        for(int i=4;i<=n;i++)
        {
            if(i-3>=1)mp[i]+=mp[i-3];
            mp[i]%=1000000007;
            if(i-2>=1)mp[i]+=mp[i-2];
            mp[i]%=1000000007;
            if(i-1>=1)mp[i]+=mp[i-1];
            mp[i]%=1000000007;
        }
        return mp[n];
    }
};

滚动数组优化

cpp 复制代码
class Solution {
public:
    int waysToStep(int n) {
        vector<int>mp(5,0);
        mp[1]=1;
        mp[2]=2;
        mp[3]=4;
        if(n==1)return 1;
        if(n==2)return 2;
        if(n==3)return 4;
        const int mod=1000000007;
        for(int i=4;i<=n;i++)
        {
            mp[4]=((mp[1]+mp[2])%mod+mp[3])%mod;
            mp[1]=mp[2];
            mp[2]=mp[3];
            mp[3]=mp[4];
        }
        return mp[4];
    }
};

3、使用最小花费爬楼梯

746. 使用最小花费爬楼梯 - 力扣(LeetCode)

思路1:

状态表示:

根据题目要求,dp[i]代表跳到台阶i的最小花费

状态转移方程:

dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])

初始化:

dp[0]=dp[1]=0

填表顺序:

从下标2开始

返回值:

dp[n],跳到楼顶的最小花费

cpp 复制代码
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n=cost.size();
        vector<int>dp(n+1,0);
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<=n;i++)
        {
            dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
};

思路2:

状态表示:

根据题目要求,dp[i]代表从[i]台阶到楼顶的最小花费

状态转移方程:

dp[i]=min(dp[i+1]+cost[i],dp[i+2]+cost[i])

初始化:

dp[n]=0,dp[n-1]=cost[n-1];

填表顺序:

从下标n-2开始,从右往左

返回值:

min(dp[0],dp[1])跳到楼顶的最小花费

cpp 复制代码
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n=cost.size();
        vector<int>dp(n+1,0);
        dp[n-1]=cost[n-1];
        dp[n]=0;
        for(int i=n-2;i>=0;i--)
        {
            dp[i]=min(dp[i+1]+cost[i],dp[i+2]+cost[i]);
        }
        return  min(dp[0],dp[1]);
    }
};

4.解码方法

状态表示:

根据题目要求,dp[i]代表以i为结尾的方法总数

状态转移方程:

if(s[i]!='0')dp[i]=dp[i-1];//如果当前字符不是0的话,说明可以在前面i-1个字符解码方法的基础上再加个当前字符即可,所以直接赋值

if(s[i-1]!='0'&&(s[i-1]-'0')*10+s[i]-'0'<=26)//如果前一个不是0(有前导0,不能合并解码),并且下标i-1字符和i字符组成的数小于等于26,说明可以合并解码,让dp[i]+=dp[i-2],也就是在前i-2个字符的基础上加上这个合并解码,所以加的方法数就是dp[i-2]

{

if(i-2>=0)//越界了,那说明是前2个字符触发这个了,那就是再加1

dp[i]+=dp[i-2];

else dp[i]+=1;

}

初始化:

dp[0]=1

填表顺序:

从下标1开始,从左往右

返回值:

dp[s.size()-1],以最后一个字符为结尾的解码方法总数

cpp 复制代码
class Solution {
public:
    int numDecodings(string s) {
        if(s[0]=='0')return 0;//如果第一个就是0,那直接返回0即可
        dp[0]=1;//第一个肯定不是0,那肯定可以单独解码,也就是1
        for(int i=1;i<s.size();i++)
        {
            if(s[i]!='0')
            dp[i]=dp[i-1];
            if(s[i-1]!='0'&&(s[i-1]-'0')*10+s[i]-'0'<=26)
            {
                if(i-2>=0)
                dp[i]+=dp[i-2];
                else dp[i]+=1;
            }
        }
        return dp[s.size()-1];
    }
    int dp[101];
};

其实,也可以让下标映射整体往后挪一位,然后让下标0存1,这样就不用特意判断i-2>=0了

路径问题

1、不同路径1

62. 不同路径 - 力扣(LeetCode)

这个在我记忆化搜索里其实写过。

状态表示:

根据题目要求,dp[i][j]代表走到[i][j]位置的不同路径和

状态转移方程:

dp[i][j]=dp[i-1][j]+dp[i][j-1]

初始化:

dp[1][1]=1,并且i、j其中任意一个为0的时候,都是0,这是一种虚拟节点,保证状态转移方程在'越界'的时候结果依旧正确

填表顺序:

从[1][1]位置开始一行行遍历,每行从左往右

返回值:

dp[m][n],即到[m][n]位置的不同路径和

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        f[1][1]=1;
        for(int i=1;i<=m;i++)
        {
            for(int j=1;j<=n;j++)
            {
                if(i==1&&j==1)continue;
                f[i][j]=f[i-1][j]+f[i][j-1];
            }
        }
        return f[m][n];
    }
    long long f[101][101];

};

2、不同路径2

63. 不同路径 II - 力扣(LeetCode)

比上一题多了障碍物的概念

状态表示:

根据题目要求,dp[i][j]代表走到[i][j]位置的不同路径和

状态转移方程:

dp[i][j]=dp[i-1][j]+dp[i][j-1]

初始化:

dp[0][0]=obstacleGrid[0][0]==1?0:1;

填表顺序:

从[0][0]位置开始一行行遍历,每行从左往右

返回值:

dp[m][n],即到[m][n]位置的不同路径和

cpp 复制代码
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        dp[0][0]=obstacleGrid[0][0]==1?0:1;
        int m=obstacleGrid.size();
        int n=obstacleGrid[0].size();
        for(int i=0;i<m;i++)
        {
            for(int j=0;j<n;j++)
            {
                if(j==0&&i==0)continue;
                if(obstacleGrid[i][j]==1)continue;
                if(i-1>=0)dp[i][j]+=dp[i-1][j];
                if(j-1>=0)dp[i][j]+=dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
    int dp[101][101];
};

因为从[0][0]开始走,所以要注意下标越界问题,另外如果起始位置有障碍物,方法是0的,其实这时候可以直接返回0的,但我为了整体代码美观,就没写。

在遍历过程中,如果当前位置有障碍物,那就是0的,直接continue掉即可。

这个代码还可以把下标映射整体往后挪一位,这样不用写这些边界if判断

cpp 复制代码
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m=obstacleGrid.size();
        int n=obstacleGrid[0].size();
        dp[1][0]=1;
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++)
                if(obstacleGrid[i-1][j-1]==0)
                    dp[i][j]=dp[i-1][j]+dp[i][j-1];
        return dp[m][n];
    }
    int dp[101][101];
};

这个看起来更加简洁

3.礼物的最大价值

礼物的最大价值_牛客题霸_牛客网 (nowcoder.com)

这题前面两题的最大区别就是,本题的每个位置都有一个价值

状态表示:

根据题目要求,mp[i][j]代表移动到[i][j]位置的最大礼物价值

状态转移方程:

mp[i][j]=grid[i-1][j-1]+max(mp[i-1][j],mp[i][j-1]);

初始化:

无需特意初始化,只要把二维数组多开一圈,依靠默认的0即可,即凡是下标含0的,值都为0

填表顺序:

从[1][1]开始遍历,逐行变量,每行都从左往右,保证[i-1][j]和[i][j-1]的位置的值比当前位置早处理即可

返回值:

mp[n][m],即到[n][m]位置的最大礼物价值

cpp 复制代码
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param grid int整型vector<vector<>> 
     * @return int整型
     */
    int maxValue(vector<vector<int> >& grid) {
        int n=grid.size(),m=grid[0].size();
        vector<vector<int>>mp(n+1,vector<int>(m+1));
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=m;j++)
            {
                mp[i][j]=grid[i-1][j-1]+max(mp[i-1][j],mp[i][j-1]);
            }
        }
        return mp[n][m];
    }
};

4.下降路径最小和

931. 下降路径最小和 - 力扣(LeetCode)

本质跟前面几题都是一样的,只是路径的选择稍稍有变化

状态表示:

根据题目要求,mp[i][j]代表移动到[i][j]位置的最小下降路径和

状态转移方程:

mp[i][j]=matrix[i-1][j-1]+min(min(mp[i-1][j],mp[i-1][j-1]),mp[i-1][j+1]);

初始化:

数组先开大一点,比如n+3或者n+2也行,主要是防止越界

数组初始化设为一个很大的值(因为是最小值,所以如果访问到了边界,这些值默认都应该为很大,这样才不会影响正常的路径选择)

[0][1~n+1]都要初始化为0,如果不初始化为0,[0][1~n+1]会被上面所设的很大的值影响,导致结果会被放大很多。

填表顺序:

从[1][1]开始遍历,逐行变量,每行都从左往右,保证[i-1][j]和[i-1][j-1]、[i-1][j+1]的位置的值比当前位置早处理即可

返回值:

当i遍历到最后一行,提前设置变量,对最后一行的每一个结果,进行比较,返回最小值即可

cpp 复制代码
class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int n=matrix.size();
        vector<vector<int>>mp(n+2,vector<int>(n+2,9999999999));
        for(int i=0;i<=n+1;i++)
        {
            mp[0][i]=0;
        }
        int ans=9999999999;
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                mp[i][j]=matrix[i-1][j-1]+min(min(mp[i-1][j],mp[i-1][j-1]),mp[i-1][j+1]);
                if(i==n)
                {
                    ans=min(ans,mp[i][j]);
                }
            }
        }
        return ans;
    }
};

5.最小路径和

64. 最小路径和 - 力扣(LeetCode)

思路没有太多区别,可以用动归解决的路径问题,在转移方程上基本都是大同小异,核心变化反而是对于初始化的问题

状态表示:

根据题目要求,dp[i][j]代表移动到[i][j]位置的最小下降路径和

状态转移方程:

dp[i][j]=min(dp[i][j-1],dp[i-1][j])+grid[i-1][j-1];

初始化:

数组先开n+1

数组初始化设为一个很大的值(因为是最小值,所以如果访问到了边界,这些值默认都应该为很大,这样才不会影响正常的路径选择)

因为对于[i][j]来说,只能从[i][j-1]和[i-1][j]走过来,所以要保证边界值不影响[i][j]的判断,对于第1行来说,他们只能从左边获取值,不能从上面获取值,而由于上面默认都设置了大值,导致[1][1]的位置无法正常设置,因为对于[1][1]来说,[0][1]和[1][0]都是一个大值,选哪一个都会导致结果异常,所以[1][1]必须提前设置,并且后续不能循坏到[1][1]

填表顺序:

从[1][1]开始遍历,逐行变量,每行都从左往右,保证[i-1][j]和[i][j-1]的位置的值比当前位置早处理即可

返回值:

返回dp[m][n]即可,即到达[m][n]位置的最小路径和

cpp 复制代码
class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int m=grid.size();
        int n=grid[0].size();
        vector<vector<int>>dp(m+1,vector<int>(n+1,INT_MAX));
        dp[1][1]=grid[0][0];
        for(int i=1;i<=m;i++)
        {
            for(int j=1;j<=n;j++)
            {
                if(i==1&&j==1)continue;
                dp[i][j]=min(dp[i][j-1],dp[i-1][j])+grid[i-1][j-1];
            }
        }
        return dp[m][n];
    }
};

6.地下城游戏(难度upup)

174. 地下城游戏 - 力扣(LeetCode)

虽然同样是路径问题,但是在状态表示上有很大的不同。

状态表示:

根据题目要求,dp[i][j]代表进入[i][j]前,并且一定以[i][j]为起点,移动到[m][n]位置的最小健康值

注意,这跟前面有很大的区别,前面基本都是移动到[i][j],以[i][j]结尾,而这里是以[i][j]开始。

之所以这样,是因为本题后面的值,是会影响最初的健康值的。简而言之,要以不变往变走。

状态转移方程:

dp[i][j]=min(dp[i+1][j],dp[i][j+1])-dungeon[i-1][j-1];

假设dp[i][j]==x,dp[i-1][j+1]==y,dungeon[i-1][j-1]==mp[i-1][j-1],那么dp[i][j+1]只可能是x+mp[i-1][j-1]或y+mp[i-1][j-1],因此x>=dp[i][j+1]-mp[i-1][j-1],同理x>=dp[i+1][j]-mp[i-1][j-1];所以x==min(dp[i+1][j],dp[i][j+1])-dungeon[i-1][j-1]

dp[i][j]=max(1,dp[i][j]);

然而考虑到一个问题,那就是mp[i-1][j-1]可能大于min(dp[i+1][j],dp[i][j+1]),也就是某个房间加了大量的健康值,有人可能疑问为什么,因为在我们这个dp的第一个关系式中,每一个位置的关系是取决于后面房间的,也就是说,前一个房间和当前房间是没有关系的,这样的话mp[i-1][j-1]就完全有可能大于min(dp[i+1][j],dp[i][j+1]),因此遇到这种情况,说明只要以最小正整数1进入[i][j]位置,就可以保证后面可以走到[m][n]

初始化:

初始开[m+2][n+2]

全部填大值,然后让dp[m][n]=dungeon[m-1][n-1]>0?1:abs(dungeon[m-1][n-1])+1;

填表顺序:

从[m][n-1]开始遍历,逐行变量,每行都从右往左,保证[i+1][j]和[i][j+1]的位置的值比当前位置早处理即可

返回值:

返回dp[1][1]即可,即进入[1][1]前,并且一定以[1][1]为起点,移动到[m][n]位置的最小健康值

cpp 复制代码
class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int m=dungeon.size();
        int n=dungeon[0].size();
        vector<vector<int >>dp(m+2,vector<int>(n+2,INT_MAX));
        dp[m][n]=dungeon[m-1][n-1]>0?1:abs(dungeon[m-1][n-1])+1;
        for(int i=m;i>=1;i--)
        {
            for(int j=n;j>=1;j--)
            {
                if(i==m&&j==n)continue;
                dp[i][j]=min(dp[i+1][j],dp[i][j+1])-dungeon[i-1][j-1];
                dp[i][j]=max(1,dp[i][j]);
            }
        }
        return dp[1][1];
    }
};

本题难度是有的,主要是状态表示和状态转移方程上面有一定难度。

相关推荐
CHANG_THE_WORLD1 小时前
9.Python 条件语句和循环语句
java·python·算法
earthzhang20212 小时前
《深入浅出HTTPS》读书笔记(20):口令和PEB算法
开发语言·网络协议·算法·https·1024程序员节
武昌库里写JAVA2 小时前
Java面试之单例模式浅谈
数据结构·vue.js·spring boot·算法·课程设计
Qiled2 小时前
Shopee算法分析 - x-sap-ri
算法
爱穿西装的C先生2 小时前
C++学习日记---第19天
c++·学习·程序人生·算法·蓝桥杯
CHANG_THE_WORLD2 小时前
3.python运算符
java·python·算法
The博宇2 小时前
机器学习:监督学习、无监督学习
大数据·人工智能·算法·机器学习
蚂蚁没问题s2 小时前
图像处理 - 车道线检测:智能驾驶的“眼睛”
图像处理·人工智能·算法·机器学习·计算机视觉
爱吃西瓜的小菜鸡2 小时前
【C语言】输入数字后的重要getchar()
c语言·开发语言·学习·算法
Catherinemin3 小时前
剑指Offer|day4 LCR 004. 只出现一次的数字 II
javascript·算法