diff --git a/404.html b/404.html index be74aafdc..c9cbd8551 100644 --- a/404.html +++ b/404.html @@ -2885,6 +2885,8 @@ + + @@ -3020,6 +3022,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_appendix/contribution/index.html b/chapter_appendix/contribution/index.html index d6c856fee..12025fedf 100644 --- a/chapter_appendix/contribution/index.html +++ b/chapter_appendix/contribution/index.html @@ -2896,6 +2896,8 @@ + + @@ -3031,6 +3033,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_appendix/index.html b/chapter_appendix/index.html index 140520134..97368990a 100644 --- a/chapter_appendix/index.html +++ b/chapter_appendix/index.html @@ -15,7 +15,7 @@ - + @@ -2896,6 +2896,8 @@ + + @@ -3031,6 +3033,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + @@ -3336,7 +3366,7 @@ diff --git a/chapter_array_and_linkedlist/array/index.html b/chapter_array_and_linkedlist/array/index.html index 09b893590..0fb8b50cc 100644 --- a/chapter_array_and_linkedlist/array/index.html +++ b/chapter_array_and_linkedlist/array/index.html @@ -2966,6 +2966,8 @@ + + @@ -3101,6 +3103,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_array_and_linkedlist/index.html b/chapter_array_and_linkedlist/index.html index 88b57d588..44d40ed46 100644 --- a/chapter_array_and_linkedlist/index.html +++ b/chapter_array_and_linkedlist/index.html @@ -2898,6 +2898,8 @@ + + @@ -3033,6 +3035,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_array_and_linkedlist/linked_list/index.html b/chapter_array_and_linkedlist/linked_list/index.html index 70ee88514..d9d4ea1c5 100644 --- a/chapter_array_and_linkedlist/linked_list/index.html +++ b/chapter_array_and_linkedlist/linked_list/index.html @@ -2973,6 +2973,8 @@ + + @@ -3108,6 +3110,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_array_and_linkedlist/list/index.html b/chapter_array_and_linkedlist/list/index.html index 396cf89a5..0c7ca0b25 100644 --- a/chapter_array_and_linkedlist/list/index.html +++ b/chapter_array_and_linkedlist/list/index.html @@ -2952,6 +2952,8 @@ + + @@ -3087,6 +3089,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_array_and_linkedlist/summary/index.html b/chapter_array_and_linkedlist/summary/index.html index 8f5a545a0..adf37328a 100644 --- a/chapter_array_and_linkedlist/summary/index.html +++ b/chapter_array_and_linkedlist/summary/index.html @@ -2945,6 +2945,8 @@ + + @@ -3080,6 +3082,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_backtracking/backtracking_algorithm/index.html b/chapter_backtracking/backtracking_algorithm/index.html index 56417a39f..7ef87da4c 100644 --- a/chapter_backtracking/backtracking_algorithm/index.html +++ b/chapter_backtracking/backtracking_algorithm/index.html @@ -2980,6 +2980,8 @@ + + @@ -3115,6 +3117,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_backtracking/index.html b/chapter_backtracking/index.html index ae305585b..8619d5969 100644 --- a/chapter_backtracking/index.html +++ b/chapter_backtracking/index.html @@ -2898,6 +2898,8 @@ + + @@ -3033,6 +3035,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_backtracking/n_queens_problem/index.html b/chapter_backtracking/n_queens_problem/index.html index 8ddb75d9c..3d1293e46 100644 --- a/chapter_backtracking/n_queens_problem/index.html +++ b/chapter_backtracking/n_queens_problem/index.html @@ -2611,8 +2611,29 @@ @@ -3270,8 +3321,29 @@ @@ -2959,6 +2999,8 @@ + + @@ -3094,6 +3136,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + @@ -3285,23 +3355,63 @@
  • - 13.2.1.   无重复的情况 + 13.2.1.   无相等元素的情况 + +
  • - 13.2.2.   考虑重复的情况 + 13.2.2.   考虑相等元素的情况 + + + +
  • @@ -3354,7 +3464,7 @@ -

    13.2.1.   无重复的情况

    +

    13.2.1.   无相等元素的情况

    Question

    输入一个整数数组,数组中不包含重复元素,返回所有可能的排列。

    @@ -3365,6 +3475,7 @@

    全排列的递归树

    Fig. 全排列的递归树

    +

    代码实现

    想清楚以上信息之后,我们就可以在框架代码中做“完形填空”了。为了缩短代码行数,我们不单独实现框架代码中的各个函数,而是将他们展开在 backtrack() 函数中。

    @@ -3656,12 +3767,13 @@
    +

    重复选择剪枝

    需要重点关注的是,我们引入了一个布尔型数组 selected ,它的长度与输入数组长度相等,其中 selected[i] 表示 choices[i] 是否已被选择。我们利用 selected 避免某个元素被重复选择,从而实现剪枝。

    如下图所示,假设我们第一轮选择 1 ,第二轮选择 3 ,第三轮选择 2 ,则需要在第二轮剪掉元素 1 的分支,在第三轮剪掉元素 1, 3 的分支。此剪枝操作可将搜索空间大小从 \(O(n^n)\) 降低至 \(O(n!)\)

    全排列剪枝示例

    Fig. 全排列剪枝示例

    -

    13.2.2.   考虑重复的情况

    +

    13.2.2.   考虑相等元素的情况

    Question

    输入一个整数数组,数组中可能包含重复元素,返回所有不重复的排列。

    @@ -3672,10 +3784,12 @@

    那么,如何去除重复的排列呢?最直接地,我们可以借助一个哈希表,直接对排列结果进行去重。然而这样做不够优雅,因为生成重复排列的搜索分支是没有必要的,应当被提前识别并剪枝,这样可以进一步提升算法效率。

    观察发现,在第一轮中,选择 \(1\) 或选择 \(\hat{1}\) 是等价的,因为在这两个选择之下生成的所有排列都是重复的。因此,我们应该把 \(\hat{1}\) 剪枝掉。同理,在第一轮选择 \(2\) 后,第二轮选择中的 \(1\)\(\hat{1}\) 也会产生重复分支,因此也需要将第二轮的 \(\hat{1}\) 剪枝。

    +

    本质上看,我们的目标是实现在某一轮选择中,多个相等的元素仅被选择一次

    重复排列剪枝

    Fig. 重复排列剪枝

    -

    本质上看,我们的目标是实现在某一轮选择中,多个相等的元素仅被选择一次。因此,在上一题的代码的基础上,我们考虑在每一轮选择中开启一个哈希表 duplicated ,用于记录该轮中已经尝试过的元素,并将重复元素剪枝。

    +

    代码实现

    +

    在上一题的代码的基础上,我们考虑在每一轮选择中开启一个哈希表 duplicated ,用于记录该轮中已经尝试过的元素,并将重复元素剪枝。

    @@ -3983,6 +4097,7 @@
    +

    两种剪枝对比

    注意,虽然 selectedduplicated 都起到剪枝的作用,但他们剪掉的是不同的分支:

    @@ -2959,6 +2999,8 @@ + + @@ -3094,6 +3136,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + @@ -3285,23 +3355,63 @@
  • - 13.3.1.   从全排列引出解法 + 13.3.1.   无重复元素的情况 + +
  • - 13.3.2.   重复子集剪枝 + 13.3.2.   考虑重复元素的情况 + + + +
  • @@ -3328,12 +3438,13 @@

    13.3.   子集和问题

    +

    13.3.1.   无重复元素的情况

    Question

    给定一个正整数数组 nums 和一个目标正整数 target ,请找出所有可能的组合,使得组合中的元素和等于 target 。给定数组无重复元素,每个元素可以被选取多次。请以列表形式返回这些组合,列表中不应包含重复组合。

    例如,输入集合 \(\{3, 4, 5\}\) 和目标整数 \(9\) ,由于集合中的数字可以被重复选取,因此解为 \(\{3, 3, 3\}, \{4, 5\}\) 。请注意,子集是不区分元素顺序的,例如 \(\{4, 5\}\)\(\{5, 4\}\) 是同一个子集。

    -

    13.3.1.   从全排列引出解法

    +

    从全排列引出解法

    类似于上节全排列问题的解法,我们可以把子集的生成过程想象成一系列选择的结果,并在选择过程中实时更新“元素和”,当元素和等于 target 时,就将子集记录至结果列表。

    而与全排列问题不同的是,本题允许重复选取同一元素,因此无需借助 selected 布尔列表来记录元素是否已被选择。我们可以对全排列代码进行小幅修改,初步得到解题代码。

    @@ -3574,7 +3685,7 @@

    子集搜索与越界剪枝

    Fig. 子集搜索与越界剪枝

    -

    13.3.2.   重复子集剪枝

    +

    重复子集剪枝

    为了去除重复子集,一种直接的思路是对结果列表进行去重。但这个方法效率很低,因为:

    diff --git a/chapter_computational_complexity/index.html b/chapter_computational_complexity/index.html index d357e4e97..950d1f69d 100644 --- a/chapter_computational_complexity/index.html +++ b/chapter_computational_complexity/index.html @@ -2898,6 +2898,8 @@ + + @@ -3033,6 +3035,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_computational_complexity/performance_evaluation/index.html b/chapter_computational_complexity/performance_evaluation/index.html index 905632e12..4742d04dd 100644 --- a/chapter_computational_complexity/performance_evaluation/index.html +++ b/chapter_computational_complexity/performance_evaluation/index.html @@ -2979,6 +2979,8 @@ + + @@ -3114,6 +3116,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_computational_complexity/space_complexity/index.html b/chapter_computational_complexity/space_complexity/index.html index b731c8f46..0613822cb 100644 --- a/chapter_computational_complexity/space_complexity/index.html +++ b/chapter_computational_complexity/space_complexity/index.html @@ -3007,6 +3007,8 @@ + + @@ -3142,6 +3144,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_computational_complexity/summary/index.html b/chapter_computational_complexity/summary/index.html index 6b9b49691..a65e97527 100644 --- a/chapter_computational_complexity/summary/index.html +++ b/chapter_computational_complexity/summary/index.html @@ -2945,6 +2945,8 @@ + + @@ -3080,6 +3082,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_computational_complexity/time_complexity/index.html b/chapter_computational_complexity/time_complexity/index.html index 6e8d376ad..b77ea58d2 100644 --- a/chapter_computational_complexity/time_complexity/index.html +++ b/chapter_computational_complexity/time_complexity/index.html @@ -3055,6 +3055,8 @@ + + @@ -3190,6 +3192,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_data_structure/basic_data_types/index.html b/chapter_data_structure/basic_data_types/index.html index 72a857f66..3de3ecc30 100644 --- a/chapter_data_structure/basic_data_types/index.html +++ b/chapter_data_structure/basic_data_types/index.html @@ -2908,6 +2908,8 @@ + + @@ -3043,6 +3045,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_data_structure/character_encoding/index.html b/chapter_data_structure/character_encoding/index.html index f14ae1440..42523cbe8 100644 --- a/chapter_data_structure/character_encoding/index.html +++ b/chapter_data_structure/character_encoding/index.html @@ -2973,6 +2973,8 @@ + + @@ -3108,6 +3110,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_data_structure/classification_of_data_structure/index.html b/chapter_data_structure/classification_of_data_structure/index.html index 3cc7a5012..7e12977c5 100644 --- a/chapter_data_structure/classification_of_data_structure/index.html +++ b/chapter_data_structure/classification_of_data_structure/index.html @@ -2952,6 +2952,8 @@ + + @@ -3087,6 +3089,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_data_structure/index.html b/chapter_data_structure/index.html index b31026ee5..96a27795d 100644 --- a/chapter_data_structure/index.html +++ b/chapter_data_structure/index.html @@ -2898,6 +2898,8 @@ + + @@ -3033,6 +3035,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_data_structure/number_encoding/index.html b/chapter_data_structure/number_encoding/index.html index 8893be48f..e66367284 100644 --- a/chapter_data_structure/number_encoding/index.html +++ b/chapter_data_structure/number_encoding/index.html @@ -2952,6 +2952,8 @@ + + @@ -3087,6 +3089,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_data_structure/summary/index.html b/chapter_data_structure/summary/index.html index be5d3d8aa..3af60579d 100644 --- a/chapter_data_structure/summary/index.html +++ b/chapter_data_structure/summary/index.html @@ -2945,6 +2945,8 @@ + + @@ -3080,6 +3082,34 @@ + + + + + +
  • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
  • + + + + diff --git a/chapter_divide_and_conquer/binary_search_recur/index.html b/chapter_divide_and_conquer/binary_search_recur/index.html index e621237f9..37dae7580 100644 --- a/chapter_divide_and_conquer/binary_search_recur/index.html +++ b/chapter_divide_and_conquer/binary_search_recur/index.html @@ -2386,8 +2386,15 @@ @@ -3278,8 +3315,15 @@
    @@ -3255,12 +3387,14 @@

    构建二叉树的示例数据

    Fig. 构建二叉树的示例数据

    +

    判断是否为分治问题

    原问题定义为从 preorderinorder 构建二叉树。我们首先从分治的角度分析这道题:

    +

    如何划分子树

    根据以上分析,这道题是可以使用分治来求解的,但问题是:如何通过前序遍历 preorder 和中序遍历 inorder 来划分左子树和右子树呢

    根据定义,preorderinorder 都可以被划分为三个部分:

    @@ -3226,6 +3315,35 @@ + + + @@ -3262,7 +3380,8 @@

    Fig. 汉诺塔问题示例

    在本文中,我们将规模为 \(i\) 的汉诺塔问题记做 \(f(i)\) 。例如 \(f(3)\) 代表将 \(3\) 个圆盘从 A 移动至 C 的汉诺塔问题。

    -

    先考虑最简单的情况:对于问题 \(f(1)\) ,即当只有一个圆盘时,则将它直接从 A 移动至 C 即可。

    +

    考虑基本情况

    +

    对于问题 \(f(1)\) ,即当只有一个圆盘时,则将它直接从 A 移动至 C 即可。

    @@ -3296,6 +3415,7 @@
    +

    子问题分解

    对于问题 \(f(3)\) ,即当有三个圆盘时,情况变得稍微复杂了一些。由于已知 \(f(1)\)\(f(2)\) 的解,我们可以从分治角度思考,A 顶部的两个圆盘看做一个整体,并执行以下步骤:

    1. B 为目标柱、C 为缓冲柱,将两个圆盘从 A 移动至 B
    2. @@ -3330,6 +3450,7 @@

      汉诺塔问题的分治策略

      Fig. 汉诺塔问题的分治策略

      +

      代码实现

      在代码实现中,我们声明一个递归函数 dfs(i, src, buf, tar) ,它的作用是将柱 src 顶部的 \(i\) 个圆盘借助缓冲柱 buf 移动至目标柱 tar

      @@ -3521,8 +3642,11 @@

      汉诺塔问题的递归树

      Fig. 汉诺塔问题的递归树

      -

      有趣的是,汉诺塔问题源自一种古老的传说故事。在古印度的一个寺庙里,僧侣们有三根高大的钻石柱子,以及 \(64\) 个大小不一的金圆盘。僧侣们不断地移动原盘,他们相信在最后一个圆盘被正确放置的那一刻,这个世界就会结束。

      +
      +

      Quote

      +

      汉诺塔问题源自一种古老的传说故事。在古印度的一个寺庙里,僧侣们有三根高大的钻石柱子,以及 \(64\) 个大小不一的金圆盘。僧侣们不断地移动原盘,他们相信在最后一个圆盘被正确放置的那一刻,这个世界就会结束。

      然而根据以上分析,即使僧侣们每秒钟移动一次,总共需要大约 \(2^{64} \approx 1.84×10^{19}\) 秒,合约 \(5850\) 亿年,远远超过了现在对宇宙年龄的估计。所以,倘若这个传说是真的,我们应该不需要担心世界末日的到来。

      +
      diff --git a/chapter_divide_and_conquer/index.html b/chapter_divide_and_conquer/index.html index 61d6da7f0..eeafdc97f 100644 --- a/chapter_divide_and_conquer/index.html +++ b/chapter_divide_and_conquer/index.html @@ -2898,6 +2898,8 @@ + + @@ -3033,6 +3035,34 @@ + + + + + +
    3. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    4. + + + + diff --git a/chapter_divide_and_conquer/summary/index.html b/chapter_divide_and_conquer/summary/index.html index 1d813a0bb..6ecc493eb 100644 --- a/chapter_divide_and_conquer/summary/index.html +++ b/chapter_divide_and_conquer/summary/index.html @@ -2908,6 +2908,8 @@ + + @@ -3043,6 +3045,34 @@ + + + + + +
    5. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    6. + + + + diff --git a/chapter_dynamic_programming/dp_problem_features/index.html b/chapter_dynamic_programming/dp_problem_features/index.html index 3f2703bfc..3f145cc72 100644 --- a/chapter_dynamic_programming/dp_problem_features/index.html +++ b/chapter_dynamic_programming/dp_problem_features/index.html @@ -2960,6 +2960,8 @@ + + @@ -3095,6 +3097,34 @@ + + + + + +
    7. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    8. + + + + @@ -3323,7 +3353,13 @@

      14.2.   动态规划问题特性

      在上节中,我们学习了动态规划问题的暴力解法,从递归树中观察到海量的重叠子问题,以及了解到动态规划是如何通过记录解来优化时间复杂度的。

      -

      实际上,动态规划最常用来求解最优方案问题,例如寻找最短路径、最大利润、最少时间等。这类问题不仅包含重叠子问题,往往还具有另外两大特性:最优子结构、无后效性

      +

      总的看来,子问题分解是一种通用的算法思路,在分治、动态规划、回溯中各有特点

      +
        +
      • 分治算法将原问题划分为几个独立的子问题,然后递归解决子问题,最后合并子问题的解得到原问题的解。
      • +
      • 动态规划也是将原问题分解为多个子问题,但与分治算法的主要区别是,动态规划中的子问题往往不是相互独立的,原问题的解依赖于子问题的解,而子问题的解又依赖于更小的子问题的解。
      • +
      • 回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支。原问题的解由一系列决策步骤构成,我们可以将每个决策步骤之前的子序列看作为一个子问题。
      • +
      +

      实际上,动态规划最常用来求解最优化问题。这类问题不仅包含重叠子问题,还具有另外两大特性:最优子结构、无后效性

      14.2.1.   最优子结构

      我们对爬楼梯问题稍作改动,使之更加适合展示最优子结构概念。

      diff --git a/chapter_dynamic_programming/dp_solution_pipeline/index.html b/chapter_dynamic_programming/dp_solution_pipeline/index.html index 7b647791d..b46059428 100644 --- a/chapter_dynamic_programming/dp_solution_pipeline/index.html +++ b/chapter_dynamic_programming/dp_solution_pipeline/index.html @@ -2812,30 +2812,43 @@
    9. - 14.3.2.   问题求解 + 14.3.2.   问题求解步骤 + + + +
    10. @@ -2981,6 +2994,8 @@ + + @@ -3116,6 +3131,34 @@ + + + + + +
    11. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    12. + + + + @@ -3314,30 +3357,43 @@
    13. - 14.3.2.   问题求解 + 14.3.2.   问题求解步骤 + + + +
    14. @@ -3384,7 +3440,7 @@
    15. 问题描述中有明显的排列组合的特征,需要返回具体的多个方案。
    16. 如果一个问题满足决策树模型,并具有较为明显的“加分项“,我们就可以假设它是一个动态规划问题,并尝试求解它。

      -

      14.3.2.   问题求解

      +

      14.3.2.   问题求解步骤

      动态规划的解题流程可能会因问题的性质和难度而有所不同,但通常遵循以下步骤:描述决策,定义状态,建立 \(dp\) 表,推导状态转移方程,确定边界条件等。

      为了更形象地展示解题步骤,我们使用一个经典问题「最小路径和」来举例。

      @@ -3432,7 +3488,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]

      边界条件即初始状态,在搜索中用于剪枝,在动态规划中用于初始化 \(dp\) 表。状态转移顺序的核心是要保证在计算当前问题时,所有它依赖的更小子问题都已经被正确地计算出来。

      接下来,我们就可以实现动态规划代码了。然而,由于子问题分解是一种从顶至底的思想,因此按照“暴力搜索 \(\rightarrow\) 记忆化搜索 \(\rightarrow\) 动态规划”的顺序实现更加符合思维习惯。

      -

      14.3.3.   方法一:暴力搜索

      +

      方法一:暴力搜索

      从状态 \([i, j]\) 开始搜索,不断分解为更小的状态 \([i-1, j]\)\([i, j-1]\) ,包括以下递归要素:

      • 递归参数:状态 \([i, j]\)返回值:从 \([0, 0]\)\([i, j]\) 的最小路径和 \(dp[i, j]\)
      • @@ -3580,7 +3636,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]

        Fig. 暴力搜索递归树

        每个状态都有向下和向右两种选择,从左上角走到右下角总共需要 \(m + n - 2\) 步,所以最差时间复杂度为 \(O(2^{m + n})\) 。请注意,这种计算方式未考虑临近网格边界的情况,当到达网络边界时只剩下一种选择。因此实际的路径数量会少一些。

        -

        14.3.4.   方法二:记忆化搜索

        +

        方法二:记忆化搜索

        为了避免重复计算重叠子问题,我们引入一个和网格 grid 相同尺寸的记忆列表 mem ,用于记录各个子问题的解,提升搜索效率。

        @@ -3753,7 +3809,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]

        记忆化搜索递归树

        Fig. 记忆化搜索递归树

        -

        14.3.5.   方法三:动态规划

        +

        方法三:动态规划

        动态规划代码是从底至顶的,仅需循环即可实现。

        @@ -3967,6 +4023,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
        +

        状态压缩

        如果希望进一步节省空间使用,可以考虑进行状态压缩。每个格子只与左边和上边的格子有关,因此我们可以只用一个单行数组来实现 \(dp\) 表。

        由于数组 dp 只能表示一行的状态,因此我们无法提前初始化首列状态,而是在遍历每行中更新它。

        diff --git a/chapter_dynamic_programming/edit_distance_problem/index.html b/chapter_dynamic_programming/edit_distance_problem/index.html index 7d246a52d..19a4de533 100644 --- a/chapter_dynamic_programming/edit_distance_problem/index.html +++ b/chapter_dynamic_programming/edit_distance_problem/index.html @@ -2835,6 +2835,25 @@ + + @@ -2853,6 +2872,39 @@ + + + + @@ -2908,6 +2960,8 @@ + + @@ -3043,6 +3097,34 @@ + + + + + +
      • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
      • + + + +
      @@ -3226,6 +3308,28 @@ + + +
      @@ -3294,6 +3398,7 @@ dp[i, j] = dp[i-1, j-1]

      第三步:确定边界条件和状态转移顺序

      当两字符串都为空时,编辑步数为 \(0\) ,即 \(dp[0, 0] = 0\) 。当 \(s\) 为空但 \(t\) 不为空时,最少编辑步数等于 \(t\) 的长度,即 \(dp[0, j] = j\) 。当 \(s\) 不为空但 \(t\) 为空时,等于 \(s\) 的长度,即 \(dp[i, 0] = i\)

      观察状态转移方程,解 \(dp[i, j]\) 依赖左方、上方、左上方的解,因此通过两层循环正序遍历整个 \(dp\) 表即可。

      +

      代码实现

      @@ -3532,6 +3637,7 @@ dp[i, j] = dp[i-1, j-1]
      +

      状态压缩

      下面考虑状态压缩,将 \(dp\) 表的第一维删除。由于 \(dp[i,j]\) 是由上方 \(dp[i-1, j]\) 、左方 \(dp[i, j-1]\) 、左上方状态 \(dp[i-1, j-1]\) 转移而来,而正序遍历会丢失左上方 \(dp[i-1, j-1]\) ,倒序遍历无法提前构建 \(dp[i, j-1]\) ,因此两种遍历顺序都不可取。

      为解决此问题,我们可以使用一个变量 leftup 来暂存左上方的解 \(dp[i-1, j-1]\) ,这样便只用考虑左方和上方的解,与完全背包问题的情况相同,可使用正序遍历。

      diff --git a/chapter_dynamic_programming/index.html b/chapter_dynamic_programming/index.html index 697f33672..dc973ee5b 100644 --- a/chapter_dynamic_programming/index.html +++ b/chapter_dynamic_programming/index.html @@ -2898,6 +2898,8 @@ + + @@ -3033,6 +3035,34 @@ + + + + + +
    17. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    18. + + + + diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming/index.html b/chapter_dynamic_programming/intro_to_dynamic_programming/index.html index 81103069b..c75d5cb51 100644 --- a/chapter_dynamic_programming/intro_to_dynamic_programming/index.html +++ b/chapter_dynamic_programming/intro_to_dynamic_programming/index.html @@ -2766,6 +2766,13 @@ 14.1.3.   方法三:动态规划 + + +
    19. + + 14.1.4.   状态压缩 + +
    20. @@ -2967,6 +2974,8 @@ + + @@ -3102,6 +3111,34 @@ + + + + + +
    21. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    22. + + + + @@ -3310,6 +3347,13 @@ 14.1.3.   方法三:动态规划 + + +
    23. + + 14.1.4.   状态压缩 + +
    24. @@ -4041,6 +4085,7 @@ dp[i] = dp[i-1] + dp[i-2]

      爬楼梯的动态规划过程

      Fig. 爬楼梯的动态规划过程

      +

      14.1.4.   状态压缩

      细心的你可能发现,由于 \(dp[i]\) 只与 \(dp[i-1]\)\(dp[i-2]\) 有关,因此我们无需使用一个数组 dp 来存储所有子问题的解,而只需两个变量滚动前进即可。如以下代码所示,由于省去了数组 dp 占用的空间,因此空间复杂度从 \(O(n)\) 降低至 \(O(1)\)

      @@ -4155,12 +4200,6 @@ dp[i] = dp[i-1] + dp[i-2]

      我们将这种空间优化技巧称为「状态压缩」。在许多动态规划问题中,当前状态仅与前面有限个状态有关,不必保存所有的历史状态,这时我们可以应用状态压缩,只保留必要的状态,通过“降维”来节省内存空间。

      -

      总的看来,子问题分解是一种通用的算法思路,在分治、动态规划、回溯中各有特点

      -
        -
      • 分治算法将原问题划分为几个独立的子问题,然后递归解决子问题,最后合并子问题的解得到原问题的解。例如,归并排序将长数组不断划分为两个短子数组,再将排序好的子数组合并为排序好的长数组。
      • -
      • 动态规划也是将原问题分解为多个子问题,但与分治算法的主要区别是,动态规划中的子问题往往不是相互独立的,原问题的解依赖于子问题的解,而子问题的解又依赖于更小的子问题的解。
      • -
      • 回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支。原问题的解由一系列决策步骤构成,我们可以将每个决策步骤之前的子序列看作为一个子问题。
      • -
      diff --git a/chapter_dynamic_programming/knapsack_problem/index.html b/chapter_dynamic_programming/knapsack_problem/index.html index b22452a69..5232888a6 100644 --- a/chapter_dynamic_programming/knapsack_problem/index.html +++ b/chapter_dynamic_programming/knapsack_problem/index.html @@ -2832,22 +2832,29 @@ @@ -3292,22 +3329,29 @@
      -

      14.4.1.   方法一:暴力搜索

      +

      方法一:暴力搜索

      搜索代码包含以下要素:

      @@ -3253,11 +3283,17 @@
    25. 不考虑时间的前提下,所有动态规划问题都可以用回溯(暴力搜索)进行求解,但递归树中存在大量的重叠子问题,效率极低。通过引入记忆化列表,可以存储所有计算过的子问题的解,从而保证重叠子问题只被计算一次。
    26. 记忆化递归是一种从顶至底的递归式解法,而与之对应的动态规划是一种从底至顶的递推式解法,就像是在“填写表格”一样。由于当前状态仅依赖于某些局部状态,因此我们可以消除 \(dp\) 表的一个维度,从而降低空间复杂度。
    27. 动态规划问题的三大特性:重叠子问题、最优子结构、无后效性。如果原问题的最优解可以从子问题的最优解构建得来,则此问题就具有最优子结构。无后效性指对于一个状态,其未来发展只与该状态有关,与其所经历的过去的所有状态无关。许多组合优化问题都不具有无后效性,无法使用动态规划快速求解。
    28. + +

      背包问题

      + +

      编辑距离问题

      + @@ -2967,6 +3027,8 @@ + + @@ -3102,6 +3164,34 @@ + + + + + +
    29. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    30. + + + + @@ -3296,6 +3386,26 @@ 14.5.1.   完全背包问题 + +
    31. @@ -3303,6 +3413,26 @@ 14.5.2.   零钱兑换问题 + +
    32. @@ -3310,6 +3440,26 @@ 14.5.3.   零钱兑换问题 II + +
    33. @@ -3359,6 +3509,7 @@
      \[ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \]
      +

      代码实现

      对比两道题目的动态规划代码,状态转移中有一处从 \(i-1\) 变为 \(i\) ,其余完全一致。

      @@ -3512,6 +3663,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
      +

      状态压缩

      由于当前状态是从左边和上边的状态转移而来,因此状态压缩后应该对 \(dp\) 表中的每一行采取正序遍历,这个遍历顺序与 0-1 背包正好相反。请通过以下动画来理解为什么要改为正序遍历。

      @@ -3719,7 +3871,8 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \]

      第三步:确定边界条件和状态转移顺序

      当目标金额为 \(0\) 时,凑出它的最少硬币个数为 \(0\) ,即所有 \(dp[i, 0]\) 都等于 \(0\) 。当无硬币时,无法凑出任意 \(> 0\) 的目标金额,即是无效解。为使状态转移方程中的 \(\min()\) 函数能够识别并过滤无效解,我们考虑使用 \(+ \infty\) 来表示它们,即令所有 \(dp[0, a]\) 都等于 \(+ \infty\)

      -

      以上做法仅适用于 Python 语言,因为大多数编程语言并未提供 \(+ \infty\) 变量,所以只能使用整型 int 的最大值,而这又会导致大数越界:\(dp[i, a - coins[i-1]]\) 是无效解时,再执行 \(+ 1\) 操作会发生溢出

      +

      代码实现

      +

      然而,大多数编程语言并未提供 \(+ \infty\) 变量,因此只能使用整型 int 的最大值来代替,而这又会导致大数越界:\(dp[i, a - coins[i-1]]\) 是无效解时,再执行 \(+ 1\) 操作会发生溢出

      为解决该问题,我们采用一个不可能达到的大数字 \(amt + 1\) 来表示无效解,因为凑出 \(amt\) 的硬币个数最多为 \(amt\) 个。

      在最后返回前,判断 \(dp[n, amt]\) 是否等于 \(amt + 1\) ,若是则返回 \(-1\) ,代表无法凑出目标金额。

      @@ -3957,6 +4110,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
      +

      状态压缩

      由于零钱兑换和完全背包的状态转移方程如出一辙,因此状态压缩方式也相同。

      @@ -4144,6 +4298,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \]

      当目标金额为 \(0\) 时,无需选择任何硬币即可凑出目标金额,因此应将所有 \(dp[i, 0]\) 都初始化为 \(1\) 。当无硬币时,无法凑出任何 \(>0\) 的目标金额,因此所有 \(dp[0, a]\) 都等于 \(0\)

      +

      代码实现

      @@ -4319,6 +4474,7 @@ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
      +

      状态压缩

      状态压缩处理方式相同,删除硬币维度即可。

      diff --git a/chapter_graph/graph/index.html b/chapter_graph/graph/index.html index d5fbdf38b..04df15f2a 100644 --- a/chapter_graph/graph/index.html +++ b/chapter_graph/graph/index.html @@ -2986,6 +2986,8 @@ + + @@ -3121,6 +3123,34 @@ + + + + + +
    34. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    35. + + + + diff --git a/chapter_graph/graph_operations/index.html b/chapter_graph/graph_operations/index.html index c21373f5f..c4409a122 100644 --- a/chapter_graph/graph_operations/index.html +++ b/chapter_graph/graph_operations/index.html @@ -2959,6 +2959,8 @@ + + @@ -3094,6 +3096,34 @@ + + + + + +
    36. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    37. + + + + diff --git a/chapter_graph/graph_traversal/index.html b/chapter_graph/graph_traversal/index.html index ace40082f..237aad392 100644 --- a/chapter_graph/graph_traversal/index.html +++ b/chapter_graph/graph_traversal/index.html @@ -2992,6 +2992,8 @@ + + @@ -3127,6 +3129,34 @@ + + + + + +
    38. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    39. + + + + diff --git a/chapter_graph/index.html b/chapter_graph/index.html index 0e588f7f6..c1c286731 100644 --- a/chapter_graph/index.html +++ b/chapter_graph/index.html @@ -2898,6 +2898,8 @@ + + @@ -3033,6 +3035,34 @@ + + + + + +
    40. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    41. + + + + diff --git a/chapter_graph/summary/index.html b/chapter_graph/summary/index.html index 10a99907a..c4e4fae0e 100644 --- a/chapter_graph/summary/index.html +++ b/chapter_graph/summary/index.html @@ -2945,6 +2945,8 @@ + + @@ -3080,6 +3082,34 @@ + + + + + +
    42. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    43. + + + + diff --git a/chapter_greedy/fractional_knapsack_problem/index.html b/chapter_greedy/fractional_knapsack_problem/index.html index 93fdce973..de4637643 100644 --- a/chapter_greedy/fractional_knapsack_problem/index.html +++ b/chapter_greedy/fractional_knapsack_problem/index.html @@ -2898,6 +2898,8 @@ + + @@ -2992,6 +2994,25 @@ + + @@ -3010,6 +3031,53 @@ + + + + @@ -3043,6 +3111,34 @@ + + + + + +
    44. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    45. + + + + @@ -3226,6 +3322,42 @@ + + +
      @@ -3256,7 +3388,7 @@

      分数背包问题的示例数据

      Fig. 分数背包问题的示例数据

      -

      第一步:问题分析

      +

      第一步:问题分析

      本题和 0-1 背包整体上非常相似,状态包含当前物品 \(i\) 和容量 \(c\) ,目标是求不超过背包容量下的最大价值。

      不同点在于,本题允许只选择物品的一部分,我们可以对物品任意地进行切分,并按照重量比例来计算物品价值,因此有:

        @@ -3266,7 +3398,7 @@

        物品在单位重量下的价值

        Fig. 物品在单位重量下的价值

        -

        第二步:贪心策略确定

        +

        第二步:贪心策略确定

        最大化背包内物品总价值,本质上是要最大化单位重量下的物品价值。由此便可推出本题的贪心策略:

        1. 将物品按照单位价值从高到低进行排序。
        2. @@ -3276,6 +3408,7 @@

          分数背包的贪心策略

          Fig. 分数背包的贪心策略

          +

          代码实现

          我们构建了一个物品类 Item ,以便将物品按照单位价值进行排序。循环进行贪心选择,当背包已满时跳出并返回解。

          @@ -3436,7 +3569,7 @@

          最差情况下,需要遍历整个物品列表,因此时间复杂度为 \(O(n)\) ,其中 \(n\) 为物品数量。由于初始化了一个 Item 对象列表,因此空间复杂度为 \(O(n)\)

          -

          第三步:正确性证明

          +

          第三步:正确性证明

          采用反证法。假设物品 \(x\) 是单位价值最高的物品,使用某算法求得最大价值为 \(res\) ,但该解中不包含物品 \(x\)

          现在从背包中拿出单位重量的任意物品,并替换为单位重量的物品 \(x\) 。由于物品 \(x\) 的单位价值最高,因此替换后的总价值一定大于 \(res\)这与 \(res\) 是最优解矛盾,说明最优解中必须包含物品 \(x\)

          对于该解中的其他物品,我们也可以构建出上述矛盾。总而言之,单位价值更大的物品总是更优选择,这说明贪心策略是有效的。

          diff --git a/chapter_greedy/greedy_algorithm/index.html b/chapter_greedy/greedy_algorithm/index.html index 1071eb79e..c45a9f4ce 100644 --- a/chapter_greedy/greedy_algorithm/index.html +++ b/chapter_greedy/greedy_algorithm/index.html @@ -2898,6 +2898,8 @@ + + @@ -3109,6 +3111,34 @@ + + + + + +
        3. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
        4. + + + + diff --git a/chapter_greedy/index.html b/chapter_greedy/index.html index 78fcc96ff..4541e67ef 100644 --- a/chapter_greedy/index.html +++ b/chapter_greedy/index.html @@ -2898,6 +2898,8 @@ + + @@ -3033,6 +3035,34 @@ + + + + + +
        5. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
        6. + + + + @@ -3261,6 +3291,7 @@
        7. 15.1   贪心算法
        8. 15.2   分数背包问题
        9. 15.3   最大容量问题
        10. +
        11. 15.4   最大切分乘积问题
        12. diff --git a/chapter_greedy/max_capacity_problem/index.html b/chapter_greedy/max_capacity_problem/index.html index 776b33949..da0ee5a9b 100644 --- a/chapter_greedy/max_capacity_problem/index.html +++ b/chapter_greedy/max_capacity_problem/index.html @@ -18,7 +18,7 @@ - + @@ -2898,6 +2898,8 @@ + + @@ -3020,6 +3022,25 @@ + + @@ -3038,6 +3059,81 @@ + + + + + + + + + + + + + + +
        13. + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + +
        14. @@ -3226,6 +3322,42 @@ + + +
      @@ -3256,14 +3388,14 @@

      最大容量问题的示例数据

      Fig. 最大容量问题的示例数据

      -

      第一步:问题分析

      +

      第一步:问题分析

      容器由任意两个隔板围成,因此本题的状态为两个隔板的索引,记为 \([i, j]\)

      根据定义,容量等于高度乘以宽度,其中高度由短板决定,宽度是两隔板的索引之差。设容量为 \(cap[i, j]\) ,可得计算公式:

      \[ cap[i, j] = \min(ht[i], ht[j]) \times (j - i) \]

      设数组长度为 \(n\) ,两个隔板的组合数量(即状态总数)为 \(C_n^2 = \frac{n(n - 1)}{2}\) 个。最直接地,我们可以穷举所有状态,从而求得最大容量,时间复杂度为 \(O(n^2)\)

      -

      第二步:贪心策略确定

      +

      第二步:贪心策略确定

      当然,这道题还有更高效率的解法。如下图所示,现选取一个状态 \([i, j]\) ,其满足索引 \(i < j\) 且高度 \(ht[i] < ht[j]\) ,即 \(i\) 为短板、 \(j\) 为长板。

      初始状态

      Fig. 初始状态

      @@ -3318,7 +3450,8 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i) -

      代码实现如下所示。最多循环 \(n\) 轮,因此时间复杂度为 \(O(n)\) 。变量 \(i\) , \(j\) , \(res\) 使用常数大小额外空间,因此空间复杂度为 \(O(1)\)

      +

      代码实现

      +

      如下代码所示,循环最多 \(n\) 轮,因此时间复杂度为 \(O(n)\) 。变量 \(i\) , \(j\) , \(res\) 使用常数大小额外空间,因此空间复杂度为 \(O(1)\)

      @@ -3421,7 +3554,7 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
      -

      第三步:正确性证明

      +

      第三步:正确性证明

      之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。

      比如在状态 \(cap[i, j]\) 下,\(i\) 为短板、\(j\) 为长板。若贪心地将短板 \(i\) 向内移动一格,会导致以下状态被“跳过”,意味着之后无法验证这些状态的容量大小

      \[ @@ -3528,13 +3661,13 @@ cap[i, i+1], cap[i, i+2], \cdots, cap[i, j-2], cap[i, j-1] -