【算法】——动态规划算法及实践应用
目录
前言:
一、什么是动态规划算法?
1. 简单介绍
2. 如何实现
二、实践案例
1. 第n个泰波那契数
2. 三步问题
3. 解码方法
总结:
前言:
本篇文章会介绍什么是动态规划算法,并会给出一些实践例题,方便我们快速掌握。
一、什么是动态规划算法?
1. 简单介绍
动态规划的核心思想是:将复杂的大问题拆解成一系列小问题,通过解决并“记住”这些小问题的答案,来避免重复计算,从而高效地解决大问题。
这过程就用到了迭代的思想,即由小推大,一步步接近最终目标。
2. 如何实现
动态规划算法的实现一般有以下几个步骤:
- 定义状态表示
- 找出状态转移方程(关键步骤)
- 初始化
- 确定计算顺序
- 返回结果
接下我会对这几个步骤进行讲解:
首先动态规划一般会有一个dp表,用于存储历史数据,我们先假设这个dp表是一个一维数组。那么第一步定义状态表示就是定义dp[ i ]到底表示了什么;第二步就是根据已有数据如果推出下一个值dp[ i + 1 ],比如dp[ i +1 ] = dp[ i ] + dp[ i - 1 ];第三步初始化就是确定边界情况,即dp表中前几个数据的值;第四步确定计算顺序就是根据题目具体情况选择从哪边开始计算,再循环遍历;第五步返回dp表中最终的计算结果即可。
二、实践案例
光看概念还是比较抽象,所以我们接下来代入一些例子来进行讲解,让你快速上手动态规划。
1. 第n个泰波那契数
首先从一个简单的题目开始,走一走动态规划的五个步骤:
- 第一步,定义状态表示,先定义一个dp表,dp[ i ]就表示下标为i的泰波那契数的值
- 第二步,状态转移方程,本题题目中已经给出,dp[n+3] = dp[n] + dp[n+1] + dp[n+2]
- 第三步,初始化,由于状态转移方程需要至少三个值,所以前三个泰波那契数都需要进行初始化,分别初始化为0,1,1
- 第四步,本题的计算顺序是从小推大
- 第五步,最终返回dp[n]的值即可
示例代码:
int tribonacci(int n) {// 处理边界情况if(n == 0) return 0;if(n == 1 || n == 2) return 1;vector<int> dp(n+1);dp[0] = 0; dp[1] = 1; dp[2] = 1; // 初始化dp表for(int i = 3; i <= n; i++){ dp[i] = dp[i-1] + dp[i-2] + dp[i-3];// 根据状态转移方程推导dp表}return dp[n];}
2. 三步问题
不同于泰波那契数列,本题就需要我们自己去推一推各个步骤了。
首先我们用dp[n]来表示上到n阶台阶有的方式个数(为了方便数据的映射,我们选择浪费掉dp[0]这个空间),dp[1]只有一种方法,所以dp[1] = 1;dp[2]可以直接从0阶到2阶,也可以从1阶到2阶,所以dp[2] = dp[1] + 1 = 2;dp[3]可以直接从0到3阶,也可以先到1阶再一步跨到3阶,也可以先到2阶再一步跨到3阶,所以dp[3] = dp[1] + dp[2] + 1 = 4。
由此我们可以推断出更广泛的情况,即到第n阶台阶可以有先到n-1或n-2或n-3的台阶后,再跨一步到达n阶台阶,所以状态转移方程为dp[n] = dp[n-1] + dp[n-2] + dp[n-3]。
推导出核心的状态转移方程后遍历,最后输出结果即可。
示例代码:
int waysToStep(int n) {const int mod = 1e9 + 7; // 模数if(n == 1 || n == 2)return n;if(n == 3)return 4;vector<int> dp(n+1);dp[1] = 1; dp[2] = 2; dp[3] = 4;for(int i = 4; i <= n; i++){dp[i] = ((dp[i-1] + dp[i-2])%mod + dp[i-3])%mod;}return dp[n];
由于本题数据较大,需要进行取模运算
3. 解码方法
这道题比前两道都要复杂一点,我们先初步定义dp[i]表示解码到下标为i位置的字符串时解法的个数,那么推导dp[i]的状态转移方程就要分为两种情况,第一种:s[i]作为一位数字解码,如果满足 '1' <= s[i] <= '9',那么就可以编码,dp[i] += dp[i-1]。第二种:s[i-1]和s[i]组合成两位数进行解码,如果两位数组合起来>=10且<=26,那也可以编码,dp[i] += dp[i-2]。
dp[0]取决于第一个字符是否为'0',而dp[1]就相对复杂,和后续的dp[i]状态转移方程的情况类似,但又因为数组范围的问题导致要单独处理。
所以这里在处理初始化时用到了一点小技巧,避免了dp[1]的单独处理,即主动浪费第一个空间,这样dp在用状态转移方程时就不会超出数组范围。但是相应的,s[ ]和dp[ ]的映射关系也要改变,dp[ ]的所有数值都要后移一位,多出来的dp[0]的数值则要根据题目来设置,如本题dp[0]就等于1。
最终示例代码:
int numDecodings(string s) {int n = s.size();vector<int> dp(n+1, 0);dp[0] = 1;dp[1] = s[0] != '0';if(n == 1) return dp[1];for(int i = 2; i <= n; i++){// dp[i]单独解码if(s[i-1] != '0'){dp[i] += dp[i-1];}// dp[i]和dp[i-1]一起解码int val = (s[i-2] - '0')*10 + s[i-1]-'0';if(val >= 10 && val <= 26){dp[i] += dp[i-2];}}return dp[n];}
总结:
这里举了三道例题来讲解动态规划算法,但学会了这三道题也只是对动态规划算法有了初步认识,后续还需要练习更多的动态规划的算法题来进一步掌握。
以上便是本篇文章的所有内容了,如果觉得又帮助的话可以点赞收藏加关注支持一下!