From 5f4a7728b20593ceeee4162892e451044df3c420 Mon Sep 17 00:00:00 2001 From: krahets Date: Wed, 30 Aug 2023 15:28:46 +0800 Subject: [PATCH] build --- chapter_array_and_linkedlist/linked_list.md | 2 +- chapter_data_structure/number_encoding.md | 34 +- .../dp_problem_features.md | 111 ++++++- .../dp_solution_pipeline.md | 185 ++++++++++- .../edit_distance_problem.md | 124 +++++++- .../knapsack_problem.md | 185 ++++++++++- .../unbounded_knapsack_problem.md | 290 +++++++++++++++++- chapter_searching/binary_search.md | 2 +- chapter_sorting/radix_sort.md | 2 +- 9 files changed, 877 insertions(+), 58 deletions(-) diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index b1e102669..ae01048fc 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -1124,7 +1124,7 @@ comments: true 如图 4-8 所示,常见的链表类型包括三种。 -- **单向链表**:即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的引用两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向空 $\text{None}$ 。 +- **单向链表**:即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的引用两项数据。我们将首个节点称为头节点,将最后一个节点称为尾节点,尾节点指向空 $\text{None}$ 。 - **环形链表**:如果我们令单向链表的尾节点指向头节点(即首尾相接),则得到一个环形链表。在环形链表中,任意节点都可以视作头节点。 - **双向链表**:与单向链表相比,双向链表记录了两个方向的引用。双向链表的节点定义同时包含指向后继节点(下一个节点)和前驱节点(上一个节点)的引用(指针)。相较于单向链表,双向链表更具灵活性,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。 diff --git a/chapter_data_structure/number_encoding.md b/chapter_data_structure/number_encoding.md index 823380155..0da6a62ec 100644 --- a/chapter_data_structure/number_encoding.md +++ b/chapter_data_structure/number_encoding.md @@ -29,8 +29,8 @@ comments: true $$ \begin{aligned} & 1 + (-2) \newline -& \rightarrow 0000 \space 0001 + 1000 \space 0010 \newline -& = 1000 \space 0011 \newline +& \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline +& = 1000 \; 0011 \newline & \rightarrow -3 \end{aligned} $$ @@ -40,10 +40,10 @@ $$ $$ \begin{aligned} & 1 + (-2) \newline -& \rightarrow 0000 \space 0001 \space \text{(原码)} + 1000 \space 0010 \space \text{(原码)} \newline -& = 0000 \space 0001 \space \text{(反码)} + 1111 \space 1101 \space \text{(反码)} \newline -& = 1111 \space 1110 \space \text{(反码)} \newline -& = 1000 \space 0001 \space \text{(原码)} \newline +& \rightarrow 0000 \; 0001 \; \text{(原码)} + 1000 \; 0010 \; \text{(原码)} \newline +& = 0000 \; 0001 \; \text{(反码)} + 1111 \; 1101 \; \text{(反码)} \newline +& = 1111 \; 1110 \; \text{(反码)} \newline +& = 1000 \; 0001 \; \text{(原码)} \newline & \rightarrow -1 \end{aligned} $$ @@ -52,8 +52,8 @@ $$ $$ \begin{aligned} -+0 & \rightarrow 0000 \space 0000 \newline --0 & \rightarrow 1000 \space 0000 ++0 & \rightarrow 0000 \; 0000 \newline +-0 & \rightarrow 1000 \; 0000 \end{aligned} $$ @@ -61,25 +61,25 @@ $$ $$ \begin{aligned} --0 \rightarrow \space & 1000 \space 0000 \space \text{(原码)} \newline -= \space & 1111 \space 1111 \space \text{(反码)} \newline -= 1 \space & 0000 \space 0000 \space \text{(补码)} \newline +-0 \rightarrow \; & 1000 \; 0000 \; \text{(原码)} \newline += \; & 1111 \; 1111 \; \text{(反码)} \newline += 1 \; & 0000 \; 0000 \; \text{(补码)} \newline \end{aligned} $$ -在负零的反码基础上加 $1$ 会产生进位,但 `byte` 类型的长度只有 8 位,因此溢出到第 9 位的 $1$ 会被舍弃。也就是说,**负零的补码为 $0000 \space 0000$ ,与正零的补码相同**。这意味着在补码表示中只存在一个零,正负零歧义从而得到解决。 +在负零的反码基础上加 $1$ 会产生进位,但 `byte` 类型的长度只有 8 位,因此溢出到第 9 位的 $1$ 会被舍弃。也就是说,**负零的补码为 $0000 \; 0000$ ,与正零的补码相同**。这意味着在补码表示中只存在一个零,正负零歧义从而得到解决。 还剩余最后一个疑惑:`byte` 类型的取值范围是 $[-128, 127]$ ,多出来的一个负数 $-128$ 是如何得到的呢?我们注意到,区间 $[-127, +127]$ 内的所有整数都有对应的原码、反码和补码,并且原码和补码之间是可以互相转换的。 -然而,**补码 $1000 \space 0000$ 是一个例外,它并没有对应的原码**。根据转换方法,我们得到该补码的原码为 $0000 \space 0000$ 。这显然是矛盾的,因为该原码表示数字 $0$ ,它的补码应该是自身。计算机规定这个特殊的补码 $1000 \space 0000$ 代表 $-128$ 。实际上,$(-1) + (-127)$ 在补码下的计算结果就是 $-128$ 。 +然而,**补码 $1000 \; 0000$ 是一个例外,它并没有对应的原码**。根据转换方法,我们得到该补码的原码为 $0000 \; 0000$ 。这显然是矛盾的,因为该原码表示数字 $0$ ,它的补码应该是自身。计算机规定这个特殊的补码 $1000 \; 0000$ 代表 $-128$ 。实际上,$(-1) + (-127)$ 在补码下的计算结果就是 $-128$ 。 $$ \begin{aligned} & (-127) + (-1) \newline -& \rightarrow 1111 \space 1111 \space \text{(原码)} + 1000 \space 0001 \space \text{(原码)} \newline -& = 1000 \space 0000 \space \text{(反码)} + 1111 \space 1110 \space \text{(反码)} \newline -& = 1000 \space 0001 \space \text{(补码)} + 1111 \space 1111 \space \text{(补码)} \newline -& = 1000 \space 0000 \space \text{(补码)} \newline +& \rightarrow 1111 \; 1111 \; \text{(原码)} + 1000 \; 0001 \; \text{(原码)} \newline +& = 1000 \; 0000 \; \text{(反码)} + 1111 \; 1110 \; \text{(反码)} \newline +& = 1000 \; 0001 \; \text{(补码)} + 1111 \; 1111 \; \text{(补码)} \newline +& = 1000 \; 0000 \; \text{(补码)} \newline & \rightarrow -128 \end{aligned} $$ diff --git a/chapter_dynamic_programming/dp_problem_features.md b/chapter_dynamic_programming/dp_problem_features.md index 19720677b..e09c009bd 100644 --- a/chapter_dynamic_programming/dp_problem_features.md +++ b/chapter_dynamic_programming/dp_problem_features.md @@ -126,13 +126,45 @@ $$ === "JS" ```javascript title="min_cost_climbing_stairs_dp.js" - [class]{}-[func]{minCostClimbingStairsDP} + /* 爬楼梯最小代价:动态规划 */ + function minCostClimbingStairsDP(cost) { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + // 初始化 dp 表,用于存储子问题的解 + const dp = new Array(n + 1); + // 初始状态:预设最小子问题的解 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状态转移:从较小子问题逐步求解较大子问题 + for (let i = 3; i <= n; i++) { + dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i]; + } + return dp[n]; + } ``` === "TS" ```typescript title="min_cost_climbing_stairs_dp.ts" - [class]{}-[func]{minCostClimbingStairsDP} + /* 爬楼梯最小代价:动态规划 */ + function minCostClimbingStairsDP(cost: Array): number { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + // 初始化 dp 表,用于存储子问题的解 + const dp = new Array(n + 1); + // 初始状态:预设最小子问题的解 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状态转移:从较小子问题逐步求解较大子问题 + for (let i = 3; i <= n; i++) { + dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i]; + } + return dp[n]; + } ``` === "C" @@ -328,13 +360,41 @@ $$ === "JS" ```javascript title="min_cost_climbing_stairs_dp.js" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 爬楼梯最小代价:状态压缩后的动态规划 */ + function minCostClimbingStairsDPComp(cost) { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + let a = cost[1], + b = cost[2]; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = Math.min(a, tmp) + cost[i]; + a = tmp; + } + return b; + } ``` === "TS" ```typescript title="min_cost_climbing_stairs_dp.ts" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 爬楼梯最小代价:状态压缩后的动态规划 */ + function minCostClimbingStairsDPComp(cost: Array): number { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + let a = cost[1], + b = cost[2]; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = Math.min(a, tmp) + cost[i]; + a = tmp; + } + return b; + } ``` === "C" @@ -569,13 +629,52 @@ $$ === "JS" ```javascript title="climbing_stairs_constraint_dp.js" - [class]{}-[func]{climbingStairsConstraintDP} + /* 带约束爬楼梯:动态规划 */ + function climbingStairsConstraintDP(n) { + if (n === 1 || n === 2) { + return n; + } + // 初始化 dp 表,用于存储子问题的解 + const dp = Array.from(new Array(n + 1), () => new Array(3)); + // 初始状态:预设最小子问题的解 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状态转移:从较小子问题逐步求解较大子问题 + for (let i = 3; i <= n; i++) { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + return dp[n][1] + dp[n][2]; + } ``` === "TS" ```typescript title="climbing_stairs_constraint_dp.ts" - [class]{}-[func]{climbingStairsConstraintDP} + /* 带约束爬楼梯:动态规划 */ + function climbingStairsConstraintDP(n: number): number { + if (n === 1 || n === 2) { + return n; + } + // 初始化 dp 表,用于存储子问题的解 + const dp = Array.from( + { length: n + 1 }, + () => new Array(3) + ); + // 初始状态:预设最小子问题的解 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状态转移:从较小子问题逐步求解较大子问题 + for (let i = 3; i <= n; i++) { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + return dp[n][1] + dp[n][2]; + } ``` === "C" diff --git a/chapter_dynamic_programming/dp_solution_pipeline.md b/chapter_dynamic_programming/dp_solution_pipeline.md index 3a4821a76..79689b712 100644 --- a/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/chapter_dynamic_programming/dp_solution_pipeline.md @@ -195,13 +195,47 @@ $$ === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDFS} + /* 最小路径和:暴力搜索 */ + function minPathSumDFS(grid, i, j) { + // 若为左上角单元格,则终止搜索 + if (i === 0 && j === 0) { + return grid[0][0]; + } + // 若行列索引越界,则返回 +∞ 代价 + if (i < 0 || j < 0) { + return Infinity; + } + // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价 + const left = minPathSumDFS(grid, i - 1, j); + const up = minPathSumDFS(grid, i, j - 1); + // 返回从左上角到 (i, j) 的最小路径代价 + return Math.min(left, up) + grid[i][j]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDFS} + /* 最小路径和:暴力搜索 */ + function minPathSumDFS( + grid: Array>, + i: number, + j: number + ): number { + // 若为左上角单元格,则终止搜索 + if (i === 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,则返回 +∞ 代价 + if (i < 0 || j < 0) { + return Infinity; + } + // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价 + const left = minPathSumDFS(grid, i - 1, j); + const up = minPathSumDFS(grid, i, j - 1); + // 返回从左上角到 (i, j) 的最小路径代价 + return Math.min(left, up) + grid[i][j]; + } ``` === "C" @@ -435,13 +469,58 @@ $$ === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDFSMem} + /* 最小路径和:记忆化搜索 */ + function minPathSumDFSMem(grid, mem, i, j) { + // 若为左上角单元格,则终止搜索 + if (i === 0 && j === 0) { + return grid[0][0]; + } + // 若行列索引越界,则返回 +∞ 代价 + if (i < 0 || j < 0) { + return Infinity; + } + // 若已有记录,则直接返回 + if (mem[i][j] !== -1) { + return mem[i][j]; + } + // 左边和上边单元格的最小路径代价 + const left = minPathSumDFSMem(grid, mem, i - 1, j); + const up = minPathSumDFSMem(grid, mem, i, j - 1); + // 记录并返回左上角到 (i, j) 的最小路径代价 + mem[i][j] = Math.min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDFSMem} + /* 最小路径和:记忆化搜索 */ + function minPathSumDFSMem( + grid: Array>, + mem: Array>, + i: number, + j: number + ): number { + // 若为左上角单元格,则终止搜索 + if (i === 0 && j === 0) { + return grid[0][0]; + } + // 若行列索引越界,则返回 +∞ 代价 + if (i < 0 || j < 0) { + return Infinity; + } + // 若已有记录,则直接返回 + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左边和上边单元格的最小路径代价 + const left = minPathSumDFSMem(grid, mem, i - 1, j); + const up = minPathSumDFSMem(grid, mem, i, j - 1); + // 记录并返回左上角到 (i, j) 的最小路径代价 + mem[i][j] = Math.min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "C" @@ -701,13 +780,61 @@ $$ === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDP} + /* 最小路径和:动态规划 */ + function minPathSumDP(grid) { + const n = grid.length, + m = grid[0].length; + // 初始化 dp 表 + const dp = Array.from({ length: n }, () => + Array.from({ length: m }, () => 0) + ); + dp[0][0] = grid[0][0]; + // 状态转移:首行 + for (let j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 状态转移:首列 + for (let i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 状态转移:其余行列 + for (let i = 1; i < n; i++) { + for (let j = 1; j < m; j++) { + dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDP} + /* 最小路径和:动态规划 */ + function minPathSumDP(grid: Array>): number { + const n = grid.length, + m = grid[0].length; + // 初始化 dp 表 + const dp = Array.from({ length: n }, () => + Array.from({ length: m }, () => 0) + ); + dp[0][0] = grid[0][0]; + // 状态转移:首行 + for (let j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 状态转移:首列 + for (let i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 状态转移:其余行列 + for (let i = 1; i < n; i++) { + for (let j: number = 1; j < m; j++) { + dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } ``` === "C" @@ -1004,13 +1131,55 @@ $$ === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDPComp} + /* 最小路径和:状态压缩后的动态规划 */ + function minPathSumDPComp(grid) { + const n = grid.length, + m = grid[0].length; + // 初始化 dp 表 + const dp = new Array(m); + // 状态转移:首行 + dp[0] = grid[0][0]; + for (let j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状态转移:其余行 + for (let i = 1; i < n; i++) { + // 状态转移:首列 + dp[0] = dp[0] + grid[i][0]; + // 状态转移:其余列 + for (let j = 1; j < m; j++) { + dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDPComp} + /* 最小路径和:状态压缩后的动态规划 */ + function minPathSumDPComp(grid: Array>): number { + const n = grid.length, + m = grid[0].length; + // 初始化 dp 表 + const dp = new Array(m); + // 状态转移:首行 + dp[0] = grid[0][0]; + for (let j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状态转移:其余行 + for (let i = 1; i < n; i++) { + // 状态转移:首列 + dp[0] = dp[0] + grid[i][0]; + // 状态转移:其余列 + for (let j = 1; j < m; j++) { + dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } ``` === "C" diff --git a/chapter_dynamic_programming/edit_distance_problem.md b/chapter_dynamic_programming/edit_distance_problem.md index 8a29efe62..f1ca020b4 100644 --- a/chapter_dynamic_programming/edit_distance_problem.md +++ b/chapter_dynamic_programming/edit_distance_problem.md @@ -199,13 +199,73 @@ $$ === "JS" ```javascript title="edit_distance.js" - [class]{}-[func]{editDistanceDP} + /* 编辑距离:动态规划 */ + function editDistanceDP(s, t) { + const n = s.length, + m = t.length; + const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); + // 状态转移:首行首列 + for (let i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (let j = 1; j <= m; j++) { + dp[0][j] = j; + } + // 状态转移:其余行列 + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // 若两字符相等,则直接跳过此两字符 + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[i][j] = + Math.min( + Math.min(dp[i][j - 1], dp[i - 1][j]), + dp[i - 1][j - 1] + ) + 1; + } + } + } + return dp[n][m]; + } ``` === "TS" ```typescript title="edit_distance.ts" - [class]{}-[func]{editDistanceDP} + /* 编辑距离:动态规划 */ + function editDistanceDP(s: string, t: string): number { + const n = s.length, + m = t.length; + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: m + 1 }, () => 0) + ); + // 状态转移:首行首列 + for (let i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (let j = 1; j <= m; j++) { + dp[0][j] = j; + } + // 状态转移:其余行列 + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // 若两字符相等,则直接跳过此两字符 + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[i][j] = + Math.min( + Math.min(dp[i][j - 1], dp[i - 1][j]), + dp[i - 1][j - 1] + ) + 1; + } + } + } + return dp[n][m]; + } ``` === "C" @@ -552,13 +612,69 @@ $$ === "JS" ```javascript title="edit_distance.js" - [class]{}-[func]{editDistanceDPComp} + /* 编辑距离:状态压缩后的动态规划 */ + function editDistanceDPComp(s, t) { + const n = s.length, + m = t.length; + const dp = new Array(m + 1).fill(0); + // 状态转移:首行 + for (let j = 1; j <= m; j++) { + dp[j] = j; + } + // 状态转移:其余行 + for (let i = 1; i <= n; i++) { + // 状态转移:首列 + let leftup = dp[0]; // 暂存 dp[i-1, j-1] + dp[0] = i; + // 状态转移:其余列 + for (let j = 1; j <= m; j++) { + const temp = dp[j]; + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // 若两字符相等,则直接跳过此两字符 + dp[j] = leftup; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 更新为下一轮的 dp[i-1, j-1] + } + } + return dp[m]; + } ``` === "TS" ```typescript title="edit_distance.ts" - [class]{}-[func]{editDistanceDPComp} + /* 编辑距离:状态压缩后的动态规划 */ + function editDistanceDPComp(s: string, t: string): number { + const n = s.length, + m = t.length; + const dp = new Array(m + 1).fill(0); + // 状态转移:首行 + for (let j = 1; j <= m; j++) { + dp[j] = j; + } + // 状态转移:其余行 + for (let i = 1; i <= n; i++) { + // 状态转移:首列 + let leftup = dp[0]; // 暂存 dp[i-1, j-1] + dp[0] = i; + // 状态转移:其余列 + for (let j = 1; j <= m; j++) { + const temp = dp[j]; + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // 若两字符相等,则直接跳过此两字符 + dp[j] = leftup; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 更新为下一轮的 dp[i-1, j-1] + } + } + return dp[m]; + } ``` === "C" diff --git a/chapter_dynamic_programming/knapsack_problem.md b/chapter_dynamic_programming/knapsack_problem.md index 9b236013e..0d494882a 100644 --- a/chapter_dynamic_programming/knapsack_problem.md +++ b/chapter_dynamic_programming/knapsack_problem.md @@ -147,13 +147,49 @@ $$ === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDFS} + /* 0-1 背包:暴力搜索 */ + function knapsackDFS(wgt, val, i, c) { + // 若已选完所有物品或背包无容量,则返回价值 0 + if (i === 0 || c === 0) { + return 0; + } + // 若超过背包容量,则只能不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 计算不放入和放入物品 i 的最大价值 + const no = knapsackDFS(wgt, val, i - 1, c); + const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 返回两种方案中价值更大的那一个 + return Math.max(no, yes); + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDFS} + /* 0-1 背包:暴力搜索 */ + function knapsackDFS( + wgt: Array, + val: Array, + i: number, + c: number + ): number { + // 若已选完所有物品或背包无容量,则返回价值 0 + if (i === 0 || c === 0) { + return 0; + } + // 若超过背包容量,则只能不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 计算不放入和放入物品 i 的最大价值 + const no = knapsackDFS(wgt, val, i - 1, c); + const yes = + knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 返回两种方案中价值更大的那一个 + return Math.max(no, yes); + } ``` === "C" @@ -386,13 +422,60 @@ $$ === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 背包:记忆化搜索 */ + function knapsackDFSMem(wgt, val, mem, i, c) { + // 若已选完所有物品或背包无容量,则返回价值 0 + if (i === 0 || c === 0) { + return 0; + } + // 若已有记录,则直接返回 + if (mem[i][c] !== -1) { + return mem[i][c]; + } + // 若超过背包容量,则只能不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 计算不放入和放入物品 i 的最大价值 + const no = knapsackDFSMem(wgt, val, mem, i - 1, c); + const yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 记录并返回两种方案中价值更大的那一个 + mem[i][c] = Math.max(no, yes); + return mem[i][c]; + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 背包:记忆化搜索 */ + function knapsackDFSMem( + wgt: Array, + val: Array, + mem: Array>, + i: number, + c: number + ): number { + // 若已选完所有物品或背包无容量,则返回价值 0 + if (i === 0 || c === 0) { + return 0; + } + // 若已有记录,则直接返回 + if (mem[i][c] !== -1) { + return mem[i][c]; + } + // 若超过背包容量,则只能不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 计算不放入和放入物品 i 的最大价值 + const no = knapsackDFSMem(wgt, val, mem, i - 1, c); + const yes = + knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 记录并返回两种方案中价值更大的那一个 + mem[i][c] = Math.max(no, yes); + return mem[i][c]; + } ``` === "C" @@ -645,13 +728,63 @@ $$ === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDP} + /* 0-1 背包:动态规划 */ + function knapsackDP(wgt, val, cap) { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array(n + 1) + .fill(0) + .map(() => Array(cap + 1).fill(0)); + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超过背包容量,则不选物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i - 1][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDP} + /* 0-1 背包:动态规划 */ + function knapsackDP( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超过背包容量,则不选物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i - 1][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } ``` === "C" @@ -949,13 +1082,49 @@ $$ === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDPComp} + /* 0-1 背包:状态压缩后的动态规划 */ + function knapsackDPComp(wgt, val, cap) { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array(cap + 1).fill(0); + // 状态转移 + for (let i = 1; i <= n; i++) { + // 倒序遍历 + for (let c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 不选和选物品 i 这两种方案的较大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDPComp} + /* 0-1 背包:状态压缩后的动态规划 */ + function knapsackDPComp( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array(cap + 1).fill(0); + // 状态转移 + for (let i = 1; i <= n; i++) { + // 倒序遍历 + for (let c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 不选和选物品 i 这两种方案的较大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "C" diff --git a/chapter_dynamic_programming/unbounded_knapsack_problem.md b/chapter_dynamic_programming/unbounded_knapsack_problem.md index 72bfaf229..0adbdd806 100644 --- a/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -137,13 +137,63 @@ $$ === "JS" ```javascript title="unbounded_knapsack.js" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全背包:动态规划 */ + function unboundedKnapsackDP(wgt, val, cap) { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超过背包容量,则不选物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } ``` === "TS" ```typescript title="unbounded_knapsack.ts" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全背包:动态规划 */ + function unboundedKnapsackDP( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超过背包容量,则不选物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } ``` === "C" @@ -396,13 +446,53 @@ $$ === "JS" ```javascript title="unbounded_knapsack.js" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全背包:状态压缩后的动态规划 */ + function unboundedKnapsackDPComp(wgt, val, cap) { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: cap + 1 }, () => 0); + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超过背包容量,则不选物品 i + dp[c] = dp[c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "TS" ```typescript title="unbounded_knapsack.ts" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全背包:状态压缩后的动态规划 */ + function unboundedKnapsackDPComp( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: cap + 1 }, () => 0); + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超过背包容量,则不选物品 i + dp[c] = dp[c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "C" @@ -702,13 +792,63 @@ $$ === "JS" ```javascript title="coin_change.js" - [class]{}-[func]{coinChangeDP} + /* 零钱兑换:动态规划 */ + function coinChangeDP(coins, amt) { + const n = coins.length; + const MAX = amt + 1; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 状态转移:首行首列 + for (let a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 状态转移:其余行列 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] !== MAX ? dp[n][amt] : -1; + } ``` === "TS" ```typescript title="coin_change.ts" - [class]{}-[func]{coinChangeDP} + /* 零钱兑换:动态规划 */ + function coinChangeDP(coins: Array, amt: number): number { + const n = coins.length; + const MAX = amt + 1; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 状态转移:首行首列 + for (let a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 状态转移:其余行列 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] !== MAX ? dp[n][amt] : -1; + } ``` === "C" @@ -1030,13 +1170,53 @@ $$ === "JS" ```javascript title="coin_change.js" - [class]{}-[func]{coinChangeDPComp} + /* 零钱兑换:状态压缩后的动态规划 */ + function coinChangeDPComp(coins, amt) { + const n = coins.length; + const MAX = amt + 1; + // 初始化 dp 表 + const dp = Array.from({ length: amt + 1 }, () => MAX); + dp[0] = 0; + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[a] = dp[a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] !== MAX ? dp[amt] : -1; + } ``` === "TS" ```typescript title="coin_change.ts" - [class]{}-[func]{coinChangeDPComp} + /* 零钱兑换:状态压缩后的动态规划 */ + function coinChangeDPComp(coins: Array, amt: number): number { + const n = coins.length; + const MAX = amt + 1; + // 初始化 dp 表 + const dp = Array.from({ length: amt + 1 }, () => MAX); + dp[0] = 0; + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[a] = dp[a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] !== MAX ? dp[amt] : -1; + } ``` === "C" @@ -1319,13 +1499,61 @@ $$ === "JS" ```javascript title="coin_change_ii.js" - [class]{}-[func]{coinChangeIIDP} + /* 零钱兑换 II:动态规划 */ + function coinChangeIIDP(coins, amt) { + const n = coins.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 初始化首列 + for (let i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不选和选硬币 i 这两种方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } ``` === "TS" ```typescript title="coin_change_ii.ts" - [class]{}-[func]{coinChangeIIDP} + /* 零钱兑换 II:动态规划 */ + function coinChangeIIDP(coins: Array, amt: number): number { + const n = coins.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 初始化首列 + for (let i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不选和选硬币 i 这两种方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } ``` === "C" @@ -1579,13 +1807,51 @@ $$ === "JS" ```javascript title="coin_change_ii.js" - [class]{}-[func]{coinChangeIIDPComp} + /* 零钱兑换 II:状态压缩后的动态规划 */ + function coinChangeIIDPComp(coins, amt) { + const n = coins.length; + // 初始化 dp 表 + const dp = Array.from({ length: amt + 1 }, () => 0); + dp[0] = 1; + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[a] = dp[a]; + } else { + // 不选和选硬币 i 这两种方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "TS" ```typescript title="coin_change_ii.ts" - [class]{}-[func]{coinChangeIIDPComp} + /* 零钱兑换 II:状态压缩后的动态规划 */ + function coinChangeIIDPComp(coins: Array, amt: number): number { + const n = coins.length; + // 初始化 dp 表 + const dp = Array.from({ length: amt + 1 }, () => 0); + dp[0] = 1; + // 状态转移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[a] = dp[a]; + } else { + // 不选和选硬币 i 这两种方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "C" diff --git a/chapter_searching/binary_search.md b/chapter_searching/binary_search.md index cbd575d41..6f64d8ec9 100755 --- a/chapter_searching/binary_search.md +++ b/chapter_searching/binary_search.md @@ -18,7 +18,7 @@ comments: true 接下来,循环执行以下两步。 -1. 计算中点索引 $m = \lfloor {(i + j) / 2} \rfloor$ ,其中 $\lfloor \space \rfloor$ 表示向下取整操作。 +1. 计算中点索引 $m = \lfloor {(i + j) / 2} \rfloor$ ,其中 $\lfloor \: \rfloor$ 表示向下取整操作。 2. 判断 `nums[m]` 和 `target` 的大小关系,分为以下三种情况。 1. 当 `nums[m] < target` 时,说明 `target` 在区间 $[m + 1, j]$ 中,因此执行 $i = m + 1$ 。 2. 当 `nums[m] > target` 时,说明 `target` 在区间 $[i, m - 1]$ 中,因此执行 $j = m - 1$ 。 diff --git a/chapter_sorting/radix_sort.md b/chapter_sorting/radix_sort.md index 1a14ece8b..cee5fbbd7 100644 --- a/chapter_sorting/radix_sort.md +++ b/chapter_sorting/radix_sort.md @@ -26,7 +26,7 @@ $$ x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d $$ -其中 $\lfloor a \rfloor$ 表示对浮点数 $a$ 向下取整,而 $\bmod \space d$ 表示对 $d$ 取余。对于学号数据,$d = 10$ 且 $k \in [1, 8]$ 。 +其中 $\lfloor a \rfloor$ 表示对浮点数 $a$ 向下取整,而 $\bmod \: d$ 表示对 $d$ 取余。对于学号数据,$d = 10$ 且 $k \in [1, 8]$ 。 此外,我们需要小幅改动计数排序代码,使之可以根据数字的第 $k$ 位进行排序。