✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–优快云博客
✨ 文章所属专栏:动态规划篇–优快云博客
一.什么是动态规划
动态规划(
Dynamic Programming , DP
)是一种解决复杂问题的算法思想,核心是通过分解子问题,存储中间结果,避免重复计算来优化效率。通常用于解决具有重复子问题和最优子结构的问题。
1.动态规划的核心思想
-
最优子结构:
问题的最优解通过子问题的最优解推导出来。例如,最短路问题中,A->B->c的最短路径包含A->B的最短路径。
-
重复子问题:
子问题会被重复计算多次。例如斐波那契数列中,计算fib(5)需要多次计算fib(2)。
-
状态转移方程:
定义如何从子问题的解推导出原问题的解(数学递推式)。
2.动态规划的步骤
这里以最经典的斐波那契数列为例来讲解动态规划的步骤,具体分为五大步骤:
-
1.状态表示:
明确问题的标量,如何用一维数组dp[i]或者二维数组dp[i] [j]来表示问题中的某种状态。该步骤过于抽象,只用一两句话较难说清,简单来说就是理解dp[i]或者dp[i] [j]中表示的含义是什么。
在斐波那契数列中,T(n)=T(n-1)+T(n-2),由前两个数推导出当前位置的数,属于线性问题,因此表示状态时用dp[i]即可,dp[i]表示的就是以i为结尾的,下标为i的斐波那契数是多少。
状态表示是第一个步骤,因此也是最重要的步骤,只有明确好状态表示,才能推导出接下来的状态转移方程。
-
2.状态转移方程:
该步骤是最难的一步,通常就是找到子问题之间的关系,然后由子问题推导出原问题的解。
在斐波那契数列中,dp[i]表示下标为i的斐波那契数,而求下标为i的数,需要先知道i-1和i-2的数是多少(重复子问题),两者相加就是下标为i的数,根据状态表示,下标为i-1的数可以用dp[i-1]表示,下标为i-2的数可以用dp[i-2]表示,因此dp[i]=dp[i-1]+dp[i-2]。
状态转移方程简单来说就是根据子问题来找到dp[i]的值,而子问题同样也可以用状态表示,具体是什么需要根据问题来分析。
-
3.初始化:
该步骤一般是推导出状态转移方程后得出,防止越界。
斐波那契数列的状态转移方程:dp[i]=dp[i-1]+dp[i-2],观察方程可以发现,最小的下标是i-2,数组中下标是从0开始,因此i-2必须满足大于等于0的条件,否则就会出现越界,因此i需要大于等于2,也就是说需要单独处理下标为0和下标为1的数,dp[0]=0,dp[1]=1。
-
4.填表顺序:
填表顺序也是在推导出状态转移方程后得出,根据方程来决定。
斐波那契数列的状态转移方程:dp[i]=dp[i-1]+dp[i-2]由小的下标推导出大的下标的值,对应状态表中由前状态推导出后状态,所以填表顺序是从左往右。
-
5.返回值:
根据题意要求的返回值来决定,通常返回dp[n]或者dp[n] [m]。
本示例中要返回下标为n的斐波那契数,由状态表示可以得返回的是dp[n]。
int fib(int n) {
int dp[31];
// 先初始化状态转移方程
dp[0] = 0;
dp[1] = 1;
// 填表
for (int i = 2; i < 31; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
3.总结
由上面的斐波那契数为例简单地讲解了什么是动态规划,在开始学习动态规划时,一定要掌握好对基础题的理解,因为动态规划属于较难的算法,特别是对状态表示的理解以及状态转移方程的推导,两个都是较难点,特别的抽象,大多都是根据一些经验和题目要求来得出,因此在做基础题时一定要理解清它们的状态表示含义和状态转移方程的推导,这些都是后面做较难题的经验。
二.斐波那契数列模型例题
1.第N个泰波纳契数
题目:
算法原理:
本道题是斐波那契数列的变形题,根据题意中的公式T(n+3)=T(n)+T(n+1)+T(n+2),可以推导出,第n个位置的数T(n)=T(n-3)+T(n-2)+T(n-1),相较于斐波那契数列,本题需要知道前三个数的值才能推导当前位置的值。
状态表示:以i为结尾,dp[i]表示第i个泰波纳契数是多少。
状态转移方程:求i位置的泰波纳契数需要知道i-1和i-2和i-3位置的值,然后相加,因此状态转移方程就是dp[i]=dp[i-1]+dp[i-2]+dp[i-3]。
初始化:求i位置所需要的最小的是i-3位置,由i-3>=0(数组下标从0开始)可以得到,i至少是大于等于3时才能使用状态转移方程,因此0,1,2三位需要单独处理,由题意可得,0,1,2三位分别对应,0,1,1。
顺序填表:从较低位推导出较高位,对应状态表中由前状态推出后状态,从左往右的顺序。
返回值:题意要求返回第n个泰波纳契数,由状态表示可以得到,dp[n]就是第n个数。
代码实现:
int tribonacci(int n){
//状态表示
long dp[38];
//初始化
dp[0] = 0, dp[1] = 1, dp[2] = 1;
//填表
for (int i = 3; i <= n;i++){
//状态转移方程
dp[i] = dp[i - 3] + dp[i - 2] + dp[i - 1];
}
//返回值
return dp[n];
}
//空间优化
int tribonacci(int n){
long dp[3];
dp[0] = 0, dp[1] = 1, dp[2] = 1;
for (int i = 3; i <= n;i++){
//滚动数组优化空间复杂度
dp[i % 3] = dp[0] + dp[1] + dp[2];
}
return dp[n % 3];
}
2.三步问题
题目:
算法原理:
假设当前有一个台阶i,想要知道到达台阶i共有多少种方式,根据题意要求,每次可以上1个,2个或者3个台阶,因此想要到达i位置有三种情况,第一种:从i-1位置上1个台阶到i位置;第二种:从i-2位置上2个台阶到i位置;第三种:从i-3位置上3个台阶到i位置。
状态表示:以i为结尾,dp[i]表示到达第i台阶共有多少种方式。
状态转移方程:由上面的三种情况可以知道,求到达i位置有多少种方式时,需要先知道i-1位置和i-2位置以及i-3位置,三个位置的每个方式个数,转换成相同子问题,分别对应dp[i-1]和dp[i-2]和dp[i-3],三种情况相加就是dp[i],因此可以推导出:dp[i]=dp[i-1]+dp[i-2]+dp[i-3],注意题意要求模上1000000007。
初始化:第0台阶时,相当于地面,只有一种方式,dp[0]=1;第1台阶时,只有从第0台阶上1个到达,所以dp[1]=1;第2台阶时,可以从第0台阶上2个,也可以从第1台阶上1个,所以dp[2]=2。
填表顺序:从低台阶到高台阶,对应状态表中由前状态推出后状态,从左往右顺序。
返回值:题意要求返回到达第n个台阶的方式总数,由状态表示可以知道,返回dp[n]。
代码实现:
int waysToStep(int n){
long long dp[1000001];
//初始化
dp[0] = 1, dp[1] = 1, dp[2] = 2;
//填表
for (int i = 3; i <= n;i++){
//状态转移方程
dp[i] = (dp[i - 1] + dp[i - 2] + dp[i - 3]) % 1000000007;
}
//返回值
return dp[n];
}
3.使用最小花费爬楼梯
题目:
算法原理:
本道题和上面那道爬楼梯题相似,只不过在这道题中需要处理的是最小花费。
状态表示:以i位置为结尾,dp[i]表示到达第i个台阶的最小花费。
状态转移方程:到达第i个台阶有两种情况,第一种:从i-1位置上1个台阶,花费就是到达i-1位置的最小花费(重复子问题用dp[i-1]表示)加上从i-1位置跳跃的费用(对应cost[i-1]);第二种:从i-2位置上2个台阶,花费就是到达i-2位置的最小花费(重复子问题用dp[i-2]表示)加上从i-2位置跳跃的费用(对应cost[i-2])。至于是两种情况中的哪一种,取最小值。因此状态转移方程就是:dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])。
初始化:因为题意中说明可以从第0台阶或者第1台阶为起点,所以到达这两个台阶的最小花费都是0,dp[0]=dp[1]=0。
填表顺序:从低台阶下标到高台阶下标,对应状态表中由前状态推出后状态,从左往右顺序。
返回值:本题有一个细节点就是楼梯顶部的位置,以示例一为例,数组中有三个数,说明存在三个台阶,分别对应下标0,下标1和下标2的台阶,因此楼梯顶部的位置就是下标3的台阶,对应的是费用数组的个数,求出数组的个数n,返回到达楼梯顶部的最小花费dp[n]。
代码实现:
int minCostClimbingStairs(vector<int>& cost){
int n = cost.size();
//状态表示,dp[i]表示以i为终点,到达i位置的最小花费
int dp[1001];
// 初始化
dp[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];
}
//方式二(感兴趣可以看):
int minCostClimbingStairs(vector<int>& cost){
int n = cost.size();
//状态表示,dp[i]表示以i为起点,从i位置到达顶点的最小花费
int dp[n];
//初始化
dp[n - 1] = cost[n - 1], dp[n - 2] = cost[n - 2];
//从右往左填表
for (int i = n - 3; i >= 0; i--){
//状态转移方程
dp[i] = min(dp[i + 1] + cost[i], dp[i + 2] + cost[i]);
}
//返回值
return min(dp[0], dp[1]);
}
4.解码方法
题目:
算法原理:
根据题意要求,因为字母总共有26个,所以最多只能有两个数字来表示一个字母。两种映射情况:要么一个数字表示一个字母并且该数字大于等于1小于等于9,0不表示任何字母;要么两个数字表示一个字母,两个数字的组合大于等于10小于等于26,为什么要从10开始,因为可能会出现02,06这种前导零的情况,不能映射到字母,所以要排除。
状态表示:dp[i]表示以i下标为结尾的解码总数。
状态转移方程:假设当前下标为i,求当前下标i为结尾的解码总数dp[i],根据上面的两种情况:如果是一个数字表示一个字母,只需找到以i-1下标为结尾的所有解码方式(重复子问题dp[i-1]表示),然后在最后加上一个单独的s[i]即可,满足第一种映射要求,dp[i]就加上dp[i-1]的值,不满足就上0;如果是两个数字表示一个字母,只需找到i-2下标为结尾的所有解码方式(重复子问题dp[i-2]表示),然后在最后加上一个s[i]和s[i-1]两个数字组合表示的字母即可,满足第二种映射要求,dp[i]就再加上dp[i-2]的值,不满足就加上0。
初始化:状态转移方程中需要用到i-2下标,为了防止越界,需要单独处理前两个下标的情况,以下标0为结尾的解码总数,因为只有一个数字所以只有第一种情况,一个数字表示一个字母,如果满足映射要求,dp[0]就等于1,不满足就是0;以下标1为结尾的解码总数,两个数字两种情况都有,满足就加上1,不满足就加上0。
填表顺序:从低下标到高下标,对应状态表中的前状态推导出后状态,从左往右顺序。
返回值:因为数组下标从0开始,所以最后一个下标是n-1,返回dp[n-1]。
代码实现:
int numDecodings(string s){
int n = s.size();
//状态表示,dp[i]表示以i下标为结尾的解码总数
int dp[101];
//初始化
int x = s[0] - '0', y = s[1] - '0';
dp[0] = 0;
if (x >= 1 && x <= 9){
dp[0] += 1;
}
dp[1] = 0;
//判断两个单独解码是否成功
if (x >= 1 && x <= 9 && y >= 1 && y <= 9){
dp[1] += 1;
}
//判断两个组合是否解码成功
if (x * 10 + y >= 10 && x * 10 + y <= 26){
dp[1] += 1;
}
//填表,从左往右
for (int i = 2; i < n; i++){
x = s[i - 1] - '0', y = s[i] - '0';
dp[i] = 0;
//判断i位置单独解码是否成功
if (y >= 1 && y <= 9){
//找i-1位置结尾的解码总数
dp[i] += dp[i - 1];
}
//判断i位置和i-1位置组合解码是否成功
if (x * 10 + y >= 10 && x * 10 + y <= 26){
//找i-2位置结尾的解码总数
dp[i] += dp[i - 2];
}
}
return dp[n - 1];
}
//优化
int numDecodings(string s){
int n = s.size();
//状态表示,dp[i]表示以i位置为结尾的解码总数
int dp[n + 1];
//初始化
dp[0] = 1;
dp[1] = 0;
if(s[0]>='0'&&s[0]<='9'){
dp[1] += 1;
}
for (int i = 2; i <= n; i++){
//下标映射
int x = s[i - 2] - '0', y = s[i - 1] - '0';
dp[i] = 0;
if (y >= 1 && y <= 9){
dp[i] += dp[i - 1];
}
if (x * 10 + y >= 10 && x * 10 + y <= 26){
dp[i] += dp[i - 2];
}
}
return dp[n];
}
以上就是关于动态规划的简单讲解以及斐波那契数模型的练习题讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!