From b7fc501d6e5cf18099eb1a300ce1232b7dcb2951 Mon Sep 17 00:00:00 2001 From: krahets Date: Sun, 2 Jul 2023 03:57:07 +0800 Subject: [PATCH] build --- .../dp_problem_features.md | 6 ---- .../intro_to_dynamic_programming.md | 28 ++++++++----------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/chapter_dynamic_programming/dp_problem_features.md b/chapter_dynamic_programming/dp_problem_features.md index 0fed55c0b..13a814c4f 100644 --- a/chapter_dynamic_programming/dp_problem_features.md +++ b/chapter_dynamic_programming/dp_problem_features.md @@ -8,12 +8,6 @@ comments: true 实际上,动态规划最常用来求解最优方案问题,例如寻找最短路径、最大利润、最少时间等。**这类问题不仅包含重叠子问题,往往还具有另外两大特性:最优子结构、无后效性**。 -在本节中,我们将通过两个例题,一同探究以下几个问题: - -1. 动态规划与分治算法的区别是什么。 -2. 最优子结构在动态规划问题中的表现形式。 -3. 无后效性的含义,其对动态规划的意义是什么。 - ## 13.2.1.   最优子结构 我们对爬楼梯问题稍作改动,使之更加适合展示最优子结构概念。 diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming.md b/chapter_dynamic_programming/intro_to_dynamic_programming.md index 811c4ae9f..aafd0bd8b 100644 --- a/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -4,13 +4,9 @@ comments: true # 13.1.   初探动态规划 -动态规划(Dynamic Programming)是一种用于解决复杂问题的优化算法,它把一个问题分解为一系列更小的子问题,并把子问题的解存储起来以供后续使用,从而避免了重复计算,提升了解题效率。 +「动态规划 Dynamic Programming」是一种用于解决复杂问题的优化算法,它把一个问题分解为一系列更小的子问题,并把子问题的解存储起来以供后续使用,从而避免了重复计算,提升了解题效率。 -在本节中,我们先从一个动态规划经典例题入手,学习动态规划是如何高效地求解问题的,包括: - -1. 如何暴力求解动态规划问题,什么是重叠子问题。 -2. 如何向暴力搜索引入记忆化处理,从而优化时间复杂度。 -3. 从递归解法引出动态规划解法,以及如何优化空间复杂度。 +在本节中,我们先从一个动态规划经典例题入手,了解动态规划是如何高效地求解问题的。 !!! question "爬楼梯" @@ -22,9 +18,7 @@ comments: true

Fig. 爬到第 3 阶的方案数量

-**不考虑效率的前提下,动态规划问题理论上都可以使用回溯算法解决**,因为回溯算法本质上就是穷举,它能够遍历决策树的所有可能的状态,并从中记录需要的解。 - -对于本题,我们可以将爬楼梯想象为一个多轮选择的过程:从地面出发,每轮选择上 $1$ 阶或 $2$ 阶,每当到达楼梯顶部时就将方案数量加 $1$ 。 +本题的目标是求解方案数量,**我们可以考虑通过回溯来穷举所有可能性**。具体来说,将爬楼梯想象为一个多轮选择的过程:从地面出发,每轮选择上 $1$ 阶或 $2$ 阶,每当到达楼梯顶部时就将方案数量加 $1$ ,当越过楼梯顶部时就将其剪枝。 === "Java" @@ -177,9 +171,9 @@ comments: true ## 13.1.1.   方法一:暴力搜索 -然而,爬楼梯并不是典型的回溯问题,更适合从分治的角度进行解析。在分治算法中,原问题被分解为较小的子问题,通过组合子问题的解得到原问题的解。例如,归并排序将一个长数组从顶至底地划分为两个短数组,再从底至顶地将已排序的短数组进行排序。 +回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。 -对于本题,设爬到第 $i$ 阶共有 $dp[i]$ 种方案,那么 $dp[i]$ 就是原问题,其子问题包括: +对于本题,我们可以尝试将问题拆解为更小的子问题。设爬到第 $i$ 阶共有 $dp[i]$ 种方案,那么 $dp[i]$ 就是原问题,其子问题包括: $$ dp[i-1] , dp[i-2] , \cdots , dp[2] , dp[1] @@ -191,13 +185,15 @@ $$ dp[i] = dp[i-1] + dp[i-2] $$ -![方案数量递推公式](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png) +![方案数量递推关系](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png) -

Fig. 方案数量递推公式

+

Fig. 方案数量递推关系

-基于此递推公式,我们可以写出递归代码:以 $dp[n]$ 为起始点,**从顶至底地将一个较大问题拆解为两个较小问题**,直至到达最小子问题 $dp[1]$ 和 $dp[2]$ 时返回。其中,最小子问题的解是已知的,即爬到第 $1$ , $2$ 阶分别有 $1$ , $2$ 种方案。 +也就是说,在爬楼梯问题中,**各个子问题之间不是相互独立的,原问题的解可以由子问题的解构成**。 -以下代码与回溯解法一样,都属于深度优先搜索,但它比回溯算法更加简洁,这体现了从分治角度考虑这道题的优势。 +我们可以基于此递推公式写出暴力搜索代码:以 $dp[n]$ 为起始点,**从顶至底地将一个较大问题拆解为两个较小问题的和**,直至到达最小子问题 $dp[1]$ 和 $dp[2]$ 时返回。其中,最小子问题的解是已知的,即爬到第 $1$ , $2$ 阶分别有 $1$ , $2$ 种方案。 + +观察以下代码,它与回溯解法都属于深度优先搜索,但比回溯算法更加简洁。 === "Java" @@ -486,7 +482,7 @@ $$ **我们也可以直接“从底至顶”进行求解**,得到标准的动态规划解法:从最小子问题开始,迭代地求解较大子问题,直至得到原问题的解。 -由于没有回溯过程,动态规划可以直接基于循环实现。我们初始化一个数组 `dp` 来存储子问题的解,从最小子问题开始,逐步求解较大子问题。在以下代码中,数组 `dp` 起到了记忆化搜索中数组 `mem` 相同的记录作用。 +由于动态规划不包含回溯过程,因此无需使用递归,而可以直接基于递推实现。我们初始化一个数组 `dp` 来存储子问题的解,从最小子问题开始,逐步求解较大子问题。在以下代码中,数组 `dp` 起到了记忆化搜索中数组 `mem` 相同的记录作用。 === "Java"