模版题-动态规划-斐波那契数

管理员
**题解分析** 1. 状态记录 - a. 确定dp数组的含义 ( 本题可用一维dp,dp[i]表示F(i)的斐波那契数 ) 2. 状态转移 - a. 确定dp的递推公式 ( 递推公式才能够通过已知结果确定后续结果 ) - b. 确定dp的初始化 ( 推动递推公式运行的起点 ) - c. 实现递推公式 ( 有时需要着重注意遍历顺序 ) 3. 结果验证 --- 其实关键就在于,**记录已知状态,通过已知状态确定后续状态。** 关键难点在于,确定递推公式。 --- 基于上述描述,可以看到解法很明显的,只记录了f(n - 1)和f(n - 2),通过这两个前置结果就可以确定f(n),所以没必要将所有的状态都记录下来。 这其实也就是**dp压缩的核心思想 —— 只记录需要的状态**。 所以可以归纳出对于明显的动态规划问题的解题步骤: ``` - 能否用动态规划解决; - dp数组记录的状态是什么含义; - dp数组的递推 ( 确定递推公式 初始化dp数组 实现递推 ); - 验证结果的准确性。 ``` ```python class Solution: # 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。 # 该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: # F(0) = 0,F(1) = 1 # F(n) = F(n - 1) + F(n - 2),其中 n > 1 # 给定 n ,请计算 F(n) 。 def fib(self, n: int) -> int: x = 0 y = 1 if n == 0: return x if n == 1: return y for i in range( 2, n + 1 ): z = x + y x = y y = z return y ``` ## 如何确定可以使用动态规划 **个人觉得,可以考虑动态规划的判断方法:** - 结果是否由过去状态的解组成, - 过去状态的解是否同样由过去的过去的解组成, - 并且组成规则是一致的,就可以考虑一下动态规划了。 具体的如下: ``` 如何确定题目是否可以用动态规划 动态规划(Dynamic Programming,DP)的核心思想是: 将一个复杂问题分解为重叠的子问题,通过存储子问题的解来避免重复计算。 ═══════════════════════════════════════════ 一、判断能否用 DP 的四个关键特征 ═══════════════════════════════════════════ 满足以下两个核心特征,通常就可以用 DP: 1. 最优子结构 - 问题的最优解包含其子问题的最优解 - 换句话说:大问题的最优解可以由小问题的最优解推导出来 - 判断方法:看能否通过子问题的最优解"组合"或"推导"出原问题的最优解 2. 重叠子问题 - 在求解过程中,同一个子问题会被多次遇到 - 如果没有重叠子问题(像分治法那样子问题互不重叠), 通常用递归即可,不需要 DP - 判断方法:画出递归树,看是否有相同的节点被重复计算 此外,还有两个辅助特征: 3. 无后效性 - 某个状态一旦确定,后续的决策不受之前如何到达该状态的影响 - 即"过去只通过状态影响未来,而不直接参与未来决策" - 这是构造 dp 数组和状态转移方程的前提 4. 状态可量化 - 能够定义出清晰的状态表示(通常是一维或多维数组) - 能够写出状态转移方程 - 数据规模通常在 10^2 到 10^5 之间(太大的话可能需要优化) ═══════════════════════════════════════════ 二、常见的 DP 题目"信号词" ═══════════════════════════════════════════ 看到以下关键词,应优先考虑 DP: - "最长" / "最小" / "最大" / "最少" - "方案数" / "组合数" / "路径数" - "是否存在" / "是否可以" - "子序列" / "子数组" / "子串" (连续的或不连续的) - "编辑距离" / "转换" - "背包" / "选择" ═══════════════════════════════════════════ 三、判断流程 ═══════════════════════════════════════════ Step 1:看数据规模 - n ≤ 20 左右 → 可能是状态压缩 DP 或暴力 - n ≤ 100~500 → 可能是 O(n²) 或 O(n³) 的 DP - n ≤ 10^5 → 可能是 O(n) 或 O(n log n) 的 DP - n ≥ 10^6 → DP 可能需要优化(滚动数组、单调队列优化等) Step 2:看题目类型 - 求最优解(最大/最小) → 极大可能是 DP 或贪心 - 求总数(多少种方案) → 极大可能是 DP - 求是否存在(可行解) → 可能是 DP - 求所有解的具体路径 → 可能是 DP + 回溯重建 Step 3:尝试定义状态 - 能否用 f(i) 表示"考虑到第 i 个位置时的答案"? - 能否用 f(i, j) 表示"第一个序列处理到 i,第二个处理到 j"? - 状态维度通常是 1 到 3 维,超过 3 维可能就不是最优解法 Step 4:尝试写出状态转移方程 - 从当前状态出发,可以进行哪些选择/决策? - 该状态如何从前面的状态推导而来? - 如果写不出清晰的状态转移方程,可能不适合用 DP Step 5:画递归树检查重叠子问题 - 把问题写成递归形式,画出递归树 - 如果树中出现大量重复的递归调用,就是 DP 的强信号 ═══════════════════════════════════════════ 四、DP 与其他算法的区分 ═══════════════════════════════════════════ DP vs 贪心: DP:需要考虑多种选择,每个阶段有多个可能的决策 贪心:只需做单一选择(局部最优即全局最优) 区分方法:看是否可以通过"反例"推翻贪心策略 DP vs 回溯/DFS: DP:求最优解/计数,数据规模较大 回溯/DFS:求所有具体解/路径,数据规模较小(n ≤ 20) 区分方法:看数据规模和是否要求列出所有解 DP vs 分治: 分治:子问题互不重叠(如归并排序、快速排序) DP:子问题大量重叠 区分方法:画递归树 DP vs 双指针/滑动窗口: 滑动窗口:通常是连续子数组/子串问题,满足某种单调性 DP:子问题更复杂,窗口法无法覆盖所有情况 ═══════════════════════════════════════════ 五、经典 DP 问题速查 ═══════════════════════════════════════════ 1. 线性 DP - 爬楼梯(Climbing Stairs) - 打家劫舍(House Robber) - 最长递增子序列(LIS) - 最大子数组和(Kadane 算法) 2. 序列 DP - 最长公共子序列(LCS) - 编辑距离(Edit Distance) - 正则表达式匹配 - 通配符匹配 3. 背包 DP - 0-1 背包 - 完全背包 - 多重背包 - 分组背包 4. 区间 DP - 石子合并 - 戳气球(Burst Balloons) - 最长回文子序列 - 多边形三角剖分 5. 状态压缩 DP - 旅行商问题(TSP) - 铺砖问题 - 集合覆盖 6. 树形 DP - 树的最大独立集 - 二叉树中的最大路径和 - 树的最长直径 7. 数位 DP - 数字 1 的个数 - 不含连续 1 的非负整数 - 可被 K 整除的整数 ═══════════════════════════════════════════ 六、总结 ═══════════════════════════════════════════ 题目可以尝试用 DP 的充分非必要条件: - 求最值 / 计数 / 是否存在 - 存在重叠子问题(画出递归树验证) - 存在最优子结构 - 问题规模在 DP 可处理范围内 题目不适合用 DP 的信号: - 要求列出所有具体解且 n 很大 - 子问题完全不重叠(分治即可) - 无法定义明确的状态转移 - 贪心可以直接得到最优解且易于证明 如果在做题时不确定,可以先尝试写出暴力递归, 然后观察递归树中是否有重复计算的节点—— 这是判断 DP 是否适用的最直观方法。 ```
评论 0

发表评论 取消回复

Shift+Enter 换行  ·  Enter 发送
还没有评论,来发表第一条吧