【入门级-算法-9、动态规划:动态规划的基本思路】

动态规划是算法中非常重要且实用的思想,需要我们掌握其核心思路。

一、概念

动态规划的核心思想是通过巧妙地利用已经计算过的结果,来避免重复计算,从而高效地解决复杂问题。其本质是通过拆分问题、缓存中间结果,避免重复计算,最终高效求解全局最优解。核心思路可概括为"化整为零、逐个击破、缓存复用"。

重叠子问题:在求解过程中,同一个子问题会被多次计算。动态规划通过"记忆化"来存储这些子问题的解。

最优子结构:一个问题的最优解包含了其子问题的最优解。我们可以通过组合子问题的最优解来构造原问题的最优解。

二、动态规划的基本思路

我们以经典的 "爬楼梯" 问题为例来说明:

问题:假设你正在爬楼梯,需要走n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶?

解决一个动态规划问题,通常需要五个步骤。

第1步:定义 dp 数组的含义

我们定义一个数组(变量)来记录状态,这个数组就叫 dp (Dynamic Programming)。首先明确 dp[i] 代表什么是至关重要的一步。

对于爬楼梯问题:我们定义 dp[i] 为 "爬到第 i 阶楼梯有多少种方法"。

那么我们的目标就转化为:求 dp[n]。

第2步:将问题抽象为数学公式,确定状态转移方程(递推公式)

这是动态规划需要重点解决的问题,也是最难的一步。

我们需要找出问题各个状态之间的关系,抽象出数学表达式。简单来说,就是思考 dp[i] 如何由前面的状态(如 dp[i-1], dp[i-2] 等)推导出来,即如何利用已经计算过的结果。

对于爬楼梯问题:思考如何爬到第 i 阶?(有如下2中情况)

从第 i-1 阶爬 1 个台阶上来。

从第 i-2 阶爬 2 个台阶上来。

由于这两种方式是互斥的,并且覆盖了所有可能性,所以到达第 i 阶的方法数就是这两种方式的方法数之和。

到达第 i-1 阶有 dp[i-1] 种方法,从这里爬1步就到 i。

到达第 i-2 阶有 dp[i-2] 种方法,从这里爬2步就到 i。

因此,状态转移方程为:dp[i] = dp[i-1] + dp[i-2]

第3步:初始化 dp 数组

递推公式需要基础值才能启动。我们需要给 dp 数组一些初始值,即"底座"。

对于爬楼梯问题:

dp[0]:爬到第0阶(也就是起点)有几种方法?只有1种,就是不爬。所以 dp[0] = 1。(这个定义有时为了逻辑清晰可以调整)

dp[1]:爬到第1阶,只有一种方法(爬1个台阶),所以 dp[1] = 1。

有了 dp[0] 和 dp[1]之后,我们就可以根据 dp[i] = dp[i-1] + dp[i-2] 计算出 dp[2], dp[3] ... 直到 dp[n]。

(PS:另一种常见的初始化是 dp[1] = 1, dp[2] = 2,逻辑上是等价的)

第4步:确定遍历顺序

我们需要确定如何遍历整个状态空间,以保证在计算 dp[i] 时,它所依赖的 dp[i-1] 和 dp[i-2] 都已经被计算出来了。

对于爬楼梯问题:我们的状态转移方程依赖于 i-1 和 i-2,所以我们需要从前往后遍历。

即 i 从 2 开始,一直循环到 n。

第5步:举例推导 dp 数组

这一步是为了验证我们的思路和代码是否正确。我们可以手动计算 dp 数组的前几项。

对于爬楼梯问题 (假设 n=5):

dp[0] = 1 (起点)

dp[1] = 1 (1)

dp[2] = dp[1] + dp[0] = 1 + 1 = 2 (1+1, 2)

dp[3] = dp[2] + dp[1] = 2 + 1 = 3 (1+1+1, 1+2, 2+1)

dp[4] = dp[3] + dp[2] = 3 + 2 = 5 (1+1+1+1, 1+1+2,1+2+1,2+1+1,2+2)

dp[5] = dp[4] + dp[3] = 5 + 3 = 8 (1+1+1+1+1, 1+1+1+2,1+1+2+1,1+2+1+1,2+1+1+1,2+1+2,1+2+2,2+2+1)

所以,爬到5阶楼梯有8种方法。这个序列(1, 1, 2, 3, 5, 8...)其实就是斐波那契数列。

三、代码实现(爬楼梯)

int climbStairs(int n) {

if (n <= 2) return n;

int dp[n + 1];

dp[1] = 1; // 到第1阶:1种方法

dp[2] = 2; // 到第2阶:2种方法(1+1 或 2)

for (int i = 3; i <= n; i++) {

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

}

return dp[n];

}

// 空间优化版

int climbStairs_opt(int n) {

if (n <= 2) return n;

int prev2 = 1; // dp[i-2]

int prev1 = 2; // dp[i-1]

int curr;

for (int i = 3; i <= n; i++) {

curr = prev1 + prev2;

prev2 = prev1;

prev1 = curr;

}

return curr;

}

四、总结与要点

核心思想:空间换时间,记录子问题的解,避免重复计算。

关键两步:

定义状态 (dp[i] 是什么):这是解决问题的基石。

状态转移方程 (dp[i] 怎么来):这是思考的难点和核心。

练习建议:从简单题开始(如斐波那契数列、爬楼梯),逐步过渡到经典问题(如背包问题、最长公共子序列、编辑距离等),严格按照五步法思考,并养成手动推导 dp 数组的习惯。

把这个"五步法"作为你的思维框架,再通过练习去填充各种具体问题的状态定义和转移方程,你就能逐渐掌握动态规划的精髓。

相关推荐
那个村的李富贵1 小时前
CANN加速下的AIGC“即时翻译”:AI语音克隆与实时变声实战
人工智能·算法·aigc·cann
power 雀儿1 小时前
Scaled Dot-Product Attention 分数计算 C++
算法
琹箐1 小时前
最大堆和最小堆 实现思路
java·开发语言·算法
renhongxia12 小时前
如何基于知识图谱进行故障原因、事故原因推理,需要用到哪些算法
人工智能·深度学习·算法·机器学习·自然语言处理·transformer·知识图谱
坚持就完事了2 小时前
数据结构之树(Java实现)
java·算法
算法备案代理2 小时前
大模型备案与算法备案,企业该如何选择?
人工智能·算法·大模型·算法备案
赛姐在努力.2 小时前
【拓扑排序】-- 算法原理讲解,及实现拓扑排序,附赠热门例题
java·算法·图论
野犬寒鸦4 小时前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
霖霖总总4 小时前
[小技巧66]当自增主键耗尽:MySQL 主键溢出问题深度解析与雪花算法替代方案
mysql·算法
rainbow68894 小时前
深入解析C++STL:map与set底层奥秘
java·数据结构·算法