From 872edb67c1fcb91fdff1de313891a900b34fb574 Mon Sep 17 00:00:00 2001 From: krahets Date: Fri, 21 Jul 2023 21:53:15 +0800 Subject: [PATCH] deploy --- 404.html | 30 + chapter_appendix/contribution/index.html | 30 + chapter_appendix/index.html | 36 +- chapter_appendix/installation/index.html | 30 + chapter_array_and_linkedlist/array/index.html | 30 + chapter_array_and_linkedlist/index.html | 30 + .../linked_list/index.html | 30 + chapter_array_and_linkedlist/list/index.html | 30 + .../summary/index.html | 30 + .../backtracking_algorithm/index.html | 30 + chapter_backtracking/index.html | 30 + .../n_queens_problem/index.html | 85 +- .../permutations_problem/index.html | 147 +- .../subset_sum_problem/index.html | 144 +- chapter_backtracking/summary/index.html | 30 + chapter_computational_complexity/index.html | 30 + .../performance_evaluation/index.html | 30 + .../space_complexity/index.html | 30 + .../summary/index.html | 30 + .../time_complexity/index.html | 30 + .../basic_data_types/index.html | 30 + .../character_encoding/index.html | 30 + .../index.html | 30 + chapter_data_structure/index.html | 30 + .../number_encoding/index.html | 30 + chapter_data_structure/summary/index.html | 30 + .../binary_search_recur/index.html | 55 +- .../build_binary_tree_problem/index.html | 136 + .../divide_and_conquer/index.html | 30 + .../hanota_problem/index.html | 128 +- chapter_divide_and_conquer/index.html | 30 + chapter_divide_and_conquer/summary/index.html | 30 + .../dp_problem_features/index.html | 38 +- .../dp_solution_pipeline/index.html | 117 +- .../edit_distance_problem/index.html | 106 + chapter_dynamic_programming/index.html | 30 + .../intro_to_dynamic_programming/index.html | 51 +- .../knapsack_problem/index.html | 77 +- .../summary/index.html | 36 + .../unbounded_knapsack_problem/index.html | 158 +- chapter_graph/graph/index.html | 30 + chapter_graph/graph_operations/index.html | 30 + chapter_graph/graph_traversal/index.html | 30 + chapter_graph/index.html | 30 + chapter_graph/summary/index.html | 30 + .../fractional_knapsack_problem/index.html | 139 +- chapter_greedy/greedy_algorithm/index.html | 30 + chapter_greedy/index.html | 31 + .../max_capacity_problem/index.html | 147 +- .../max_product_cutting_definition.png | Bin 0 -> 42098 bytes ...max_product_cutting_greedy_calculation.png | Bin 0 -> 32246 bytes .../max_product_cutting_greedy_infer1.png | Bin 0 -> 39518 bytes .../max_product_cutting_greedy_infer2.png | Bin 0 -> 37269 bytes .../max_product_cutting_greedy_infer3.png | Bin 0 -> 39437 bytes .../max_product_cutting_problem/index.html | 3738 +++++++++++++++++ chapter_greedy/summary/index.html | 3362 +++++++++++++++ chapter_hashing/hash_algorithm/index.html | 30 + chapter_hashing/hash_collision/index.html | 30 + chapter_hashing/hash_map/index.html | 30 + chapter_hashing/index.html | 30 + chapter_hashing/summary/index.html | 30 + chapter_heap/build_heap/index.html | 30 + chapter_heap/heap/index.html | 30 + chapter_heap/index.html | 30 + chapter_heap/summary/index.html | 30 + chapter_heap/top_k/index.html | 30 + .../algorithms_are_everywhere/index.html | 30 + chapter_introduction/index.html | 30 + chapter_introduction/summary/index.html | 30 + chapter_introduction/what_is_dsa/index.html | 30 + chapter_preface/about_the_book/index.html | 30 + chapter_preface/index.html | 30 + chapter_preface/suggestions/index.html | 30 + chapter_preface/summary/index.html | 30 + chapter_reference/index.html | 30 + chapter_searching/binary_search/index.html | 30 + .../binary_search_edge/index.html | 30 + chapter_searching/index.html | 30 + .../replace_linear_by_hashing/index.html | 30 + .../searching_algorithm_revisited/index.html | 30 + chapter_searching/summary/index.html | 30 + chapter_sorting/bubble_sort/index.html | 30 + chapter_sorting/bucket_sort/index.html | 30 + chapter_sorting/counting_sort/index.html | 30 + chapter_sorting/heap_sort/index.html | 30 + chapter_sorting/index.html | 30 + chapter_sorting/insertion_sort/index.html | 30 + chapter_sorting/merge_sort/index.html | 30 + chapter_sorting/quick_sort/index.html | 30 + chapter_sorting/radix_sort/index.html | 30 + chapter_sorting/selection_sort/index.html | 30 + chapter_sorting/sorting_algorithm/index.html | 30 + chapter_sorting/summary/index.html | 30 + chapter_stack_and_queue/deque/index.html | 30 + chapter_stack_and_queue/index.html | 30 + chapter_stack_and_queue/queue/index.html | 30 + chapter_stack_and_queue/stack/index.html | 30 + chapter_stack_and_queue/summary/index.html | 30 + .../array_representation_of_tree/index.html | 30 + chapter_tree/avl_tree/index.html | 30 + chapter_tree/binary_search_tree/index.html | 30 + chapter_tree/binary_tree/index.html | 30 + chapter_tree/binary_tree_traversal/index.html | 30 + chapter_tree/index.html | 30 + chapter_tree/summary/index.html | 30 + index.html | 30 + search/search_index.json | 2 +- sitemap.xml | 10 + sitemap.xml.gz | Bin 945 -> 962 bytes 109 files changed, 11092 insertions(+), 111 deletions(-) create mode 100644 chapter_greedy/max_product_cutting_problem.assets/max_product_cutting_definition.png create mode 100644 chapter_greedy/max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png create mode 100644 chapter_greedy/max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png create mode 100644 chapter_greedy/max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png create mode 100644 chapter_greedy/max_product_cutting_problem.assets/max_product_cutting_greedy_infer3.png create mode 100644 chapter_greedy/max_product_cutting_problem/index.html create mode 100644 chapter_greedy/summary/index.html 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.   重复子集剪枝

    +

    重复子集剪枝

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

    • 当数组元素较多,尤其是当 target 较大时,搜索过程会产生大量的重复子集。
    • @@ -3590,6 +3701,7 @@

      Fig. 不同选择顺序导致的重复子集

      总结来看,给定输入数组 \([x_1, x_2, \cdots, x_n]\) ,设搜索过程中的选择序列为 \([x_{i_1}, x_{i_2}, \cdots , x_{i_m}]\) ,则该选择序列需要满足 \(i_1 \leq i_2 \leq \cdots \leq i_m\)不满足该条件的选择序列都是重复子集

      +

      代码实现

      为实现该剪枝,我们初始化变量 start ,用于指示遍历起点。当做出选择 \(x_{i}\) 后,设定下一轮从索引 \(i\) 开始遍历,从而完成子集去重。

      除此之外,我们还对代码进行了两项优化。首先,我们在开启搜索前将数组 nums 排序,在搜索过程中,当子集和超过 target 时直接结束循环,因为后边的元素更大,其子集和都一定会超过 target 。其次,我们通过在 target 上执行减法来统计元素和,当 target 等于 \(0\) 时记录解,省去了元素和变量 total

      @@ -3844,7 +3956,7 @@

      子集和 I 回溯过程

      Fig. 子集和 I 回溯过程

      -

      13.3.3.   相等元素剪枝

      +

      13.3.2.   考虑重复元素的情况

      Question

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

      @@ -3853,8 +3965,10 @@

      相等元素导致的重复子集

      Fig. 相等元素导致的重复子集

      +

      相等元素剪枝

      为解决此问题,我们需要限制相等元素在每一轮中只被选择一次。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。利用该特性,在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。

      与此同时,本题规定数组元素只能被选择一次。幸运的是,我们也可以利用变量 start 来满足该约束:当做出选择 \(x_{i}\) 后,设定下一轮从索引 \(i + 1\) 开始向后遍历。这样即能去除重复子集,也能避免重复选择相等元素。

      +

      代码实现

      diff --git a/chapter_backtracking/summary/index.html b/chapter_backtracking/summary/index.html index 1e70a553c..f21a343ac 100644 --- a/chapter_backtracking/summary/index.html +++ b/chapter_backtracking/summary/index.html @@ -2908,6 +2908,8 @@ + + @@ -3043,6 +3045,34 @@ + + + + + +
    • + + + + + 15.4.   最大切分乘积问题 + + + + + + + + + + + + +
    • + + + +
    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 @@
    • - - 12.2.1.   基于分治实现二分 + + 基于分治的搜索算法 + + +
    • + +
    • + + 基于分治实现二分
    • @@ -3309,13 +3353,14 @@

      12.2.   分治搜索策略

      我们已经学过,搜索算法分为两大类:暴力搜索、自适应搜索。暴力搜索的时间复杂度为 \(O(n)\) 。自适应搜索利用特有的数据组织形式或先验信息,可达到 \(O(\log n)\) 甚至 \(O(1)\) 的时间复杂度。

      +

      基于分治的搜索算法

      实际上,\(O(\log n)\) 的搜索算法通常都是基于分治策略实现的,例如:

      • 二分查找的每一步都将问题(在数组中搜索目标元素)分解为一个小问题(在数组的一半中搜索目标元素),这个过程一直持续到数组为空或找到目标元素为止。
      • 树是分治关系的代表,在二叉搜索树、AVL 树、堆等数据结构中,各种操作的时间复杂度皆为 \(O(\log n)\)

      分治之所以能够提升搜索效率,是因为暴力搜索每轮只能排除一个选项,而基于分治的搜索每轮可以排除一半选项

      -

      12.2.1.   基于分治实现二分

      +

      基于分治实现二分

      接下来,我们尝试从分治策略的角度分析二分查找的性质:

      @@ -3226,6 +3322,42 @@ + + +
    @@ -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.   方法一:暴力搜索

      +

      方法一:暴力搜索

      搜索代码包含以下要素:

      • 递归参数:状态 \([i, c]\)返回值:子问题的解 \(dp[i, c]\)
      • @@ -3517,7 +3561,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])

        0-1 背包的暴力搜索递归树

        Fig. 0-1 背包的暴力搜索递归树

        -

        14.4.2.   方法二:记忆化搜索

        +

        方法二:记忆化搜索

        为了防止重复求解重叠子问题,我们借助一个记忆列表 mem 来记录子问题的解,其中 mem[i][c] 对应解 \(dp[i, c]\)

        @@ -3689,7 +3733,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])

        0-1 背包的记忆化搜索递归树

        Fig. 0-1 背包的记忆化搜索递归树

        -

        14.4.3.   方法三:动态规划

        +

        方法三:动态规划

        动态规划解法本质上就是在状态转移中填充 \(dp\) 表的过程,代码如下所示。

        @@ -3890,7 +3934,8 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
        -

        最后考虑状态压缩。以上代码中的数组 dp 占用 \(O(n \times cap)\) 空间。由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 \(O(n^2)\) 将低至 \(O(n)\) 。代码省略,有兴趣的同学可以自行实现。

        +

        状态压缩

        +

        最后考虑状态压缩。以上代码中的数组 dp 占用 \(O(n \times cap)\) 空间。由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 \(O(n^2)\) 将低至 \(O(n)\) 。代码省略,有兴趣的同学可以自行实现。

        那么,我们是否可以仅用一个数组实现状态压缩呢?观察可知,每个状态都是由正上方或左上方的格子转移过来的。假设只有一个数组,当遍历到第 \(i\) 行时,该数组存储的仍然是第 \(i-1\) 行的状态,为了避免左方区域的格子在状态转移中被覆盖,应该采取倒序遍历

        以下动画展示了在单个数组下从第 \(i=1\) 行转换至第 \(i=2\) 行的过程。建议你思考一下正序遍历和倒序遍历的区别。

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

      背包问题

      +
      • 背包问题是最典型的动态规划题目,具有 0-1 背包、完全背包、多重背包等变种问题。
      • 0-1 背包的状态定义为前 \(i\) 个物品在剩余容量为 \(c\) 的背包中的最大价值。这是一种常见的定义方式。不放入物品 \(i\) ,状态转移至 \([i-1, c]\) ,放入则转移至 \([i-1, c-wgt[i-1]]\) ,由此便得到最优子结构,并构建出状态转移方程。对于状态压缩,由于每个状态依赖正上方和左上方的状态,因此需要倒序遍历列表,避免左上方状态被覆盖。
      • 完全背包的每种物品有无数个,因此在放置物品 \(i\) 后,状态转移至 \([i, c-wgt[i-1]]\) 。由于状态依赖于正上方和正左方的状态,因此状态压缩后应该正序遍历。
      • 零钱兑换问题是完全背包的一个变种。为从求“最大“价值变为求“最小”硬币数量,我们将状态转移方程中的 \(\max()\) 改为 \(\min()\) 。为从求“不超过”背包容量到求“恰好”凑出目标金额,我们使用 \(amt + 1\) 来表示“无法凑出目标金额”的无效解。
      • 零钱兑换 II 问题从求“最少硬币数量”改为求“硬币组合数量”,状态转移方程相应地从 \(\min()\) 改为求和运算符。
      • +
      +

      编辑距离问题

      +
      • 编辑距离(Levenshtein 距离)用于衡量两个字符串之间的相似度,定义为从一个字符串到另一个字符串的最小编辑步数,编辑操作包括添加、删除、替换。
      • 编辑距离问题的状态定义为将 \(s\) 的前 \(i\) 个字符更改为 \(t\) 的前 \(j\) 个字符所需的最少编辑步数。考虑字符 \(s[i]\)\(t[j]\) ,具有三种决策:在 \(s[i-1]\) 之后添加 \(t[j-1]\) 、删除 \(s[i-1]\) 、将 \(s[i-1]\) 替换为 \(t[j-1]\) ,它们都有相应的剩余子问题,据此就可以找出最优子结构与构建状态转移方程。值得注意的是,当 \(s[i] = t[j]\) 时,无需编辑当前字符,直接跳过即可。
      • 在编辑距离中,状态依赖于其正上方、正左方、左上方的状态,因此状态压缩后正序或倒序遍历都无法正确地进行状态转移。利用一个变量暂存左上方状态,即转化至完全背包地情况,可以在状态压缩后使用正序遍历。
      • diff --git a/chapter_dynamic_programming/unbounded_knapsack_problem/index.html b/chapter_dynamic_programming/unbounded_knapsack_problem/index.html index 6e8fd4847..9f6b27130 100644 --- a/chapter_dynamic_programming/unbounded_knapsack_problem/index.html +++ b/chapter_dynamic_programming/unbounded_knapsack_problem/index.html @@ -2864,6 +2864,26 @@ 14.5.1.   完全背包问题 + +
      • @@ -2871,6 +2891,26 @@ 14.5.2.   零钱兑换问题 + +
      • @@ -2878,6 +2918,26 @@ 14.5.3.   零钱兑换问题 II + +
      @@ -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] -