diff --git a/chapter_array_and_linkedlist/array.md b/chapter_array_and_linkedlist/array.md deleted file mode 100755 index c7e95397f..000000000 --- a/chapter_array_and_linkedlist/array.md +++ /dev/null @@ -1,1225 +0,0 @@ ---- -comments: true ---- - -# 4.1 数组 - -「数组 array」是一种线性数据结构,其将相同类型元素存储在连续的内存空间中。我们将元素在数组中的位置称为该元素的「索引 index」。图 4-1 展示了数组的主要术语和概念。 - - - -
图 4-1 数组定义与存储方式
- -## 4.1.1 数组常用操作 - -### 1. 初始化数组 - -我们可以根据需求选用数组的两种初始化方式:无初始值、给定初始值。在未指定初始值的情况下,大多数编程语言会将数组元素初始化为 $0$ 。 - -=== "Python" - - ```python title="array.py" - # 初始化数组 - arr: list[int] = [0] * 5 # [ 0, 0, 0, 0, 0 ] - nums: list[int] = [1, 3, 2, 5, 4] - ``` - -=== "C++" - - ```cpp title="array.cpp" - /* 初始化数组 */ - // 存储在栈上 - int arr[5]; - int nums[5] { 1, 3, 2, 5, 4 }; - // 存储在堆上(需要手动释放空间) - int* arr1 = new int[5]; - int* nums1 = new int[5] { 1, 3, 2, 5, 4 }; - ``` - -=== "Java" - - ```java title="array.java" - /* 初始化数组 */ - int[] arr = new int[5]; // { 0, 0, 0, 0, 0 } - int[] nums = { 1, 3, 2, 5, 4 }; - ``` - -=== "C#" - - ```csharp title="array.cs" - /* 初始化数组 */ - int[] arr = new int[5]; // { 0, 0, 0, 0, 0 } - int[] nums = { 1, 3, 2, 5, 4 }; - ``` - -=== "Go" - - ```go title="array.go" - /* 初始化数组 */ - var arr [5]int - // 在 Go 中,指定长度时([5]int)为数组,不指定长度时([]int)为切片 - // 由于 Go 的数组被设计为在编译期确定长度,因此只能使用常量来指定长度 - // 为了方便实现扩容 extend() 方法,以下将切片(Slice)看作数组(Array) - nums := []int{1, 3, 2, 5, 4} - ``` - -=== "Swift" - - ```swift title="array.swift" - /* 初始化数组 */ - let arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0] - let nums = [1, 3, 2, 5, 4] - ``` - -=== "JS" - - ```javascript title="array.js" - /* 初始化数组 */ - var arr = new Array(5).fill(0); - var nums = [1, 3, 2, 5, 4]; - ``` - -=== "TS" - - ```typescript title="array.ts" - /* 初始化数组 */ - let arr: number[] = new Array(5).fill(0); - let nums: number[] = [1, 3, 2, 5, 4]; - ``` - -=== "Dart" - - ```dart title="array.dart" - /* 初始化数组 */ - List图 4-2 数组元素的内存地址计算
- -观察图 4-2 ,我们发现数组首个元素的索引为 $0$ ,这似乎有些反直觉,因为从 $1$ 开始计数会更自然。但从地址计算公式的角度看,**索引的含义本质上是内存地址的偏移量**。首个元素的地址偏移量是 $0$ ,因此它的索引为 $0$ 也是合理的。 - -在数组中访问元素是非常高效的,我们可以在 $O(1)$ 时间内随机访问数组中的任意一个元素。 - -=== "Python" - - ```python title="array.py" - def random_access(nums: list[int]) -> int: - """随机访问元素""" - # 在区间 [0, len(nums)-1] 中随机抽取一个数字 - random_index = random.randint(0, len(nums) - 1) - # 获取并返回随机元素 - random_num = nums[random_index] - return random_num - ``` - -=== "C++" - - ```cpp title="array.cpp" - /* 随机访问元素 */ - int randomAccess(int *nums, int size) { - // 在区间 [0, size) 中随机抽取一个数字 - int randomIndex = rand() % size; - // 获取并返回随机元素 - int randomNum = nums[randomIndex]; - return randomNum; - } - ``` - -=== "Java" - - ```java title="array.java" - /* 随机访问元素 */ - int randomAccess(int[] nums) { - // 在区间 [0, nums.length) 中随机抽取一个数字 - int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length); - // 获取并返回随机元素 - int randomNum = nums[randomIndex]; - return randomNum; - } - ``` - -=== "C#" - - ```csharp title="array.cs" - /* 随机访问元素 */ - int randomAccess(int[] nums) { - Random random = new(); - // 在区间 [0, nums.Length) 中随机抽取一个数字 - int randomIndex = random.Next(nums.Length); - // 获取并返回随机元素 - int randomNum = nums[randomIndex]; - return randomNum; - } - ``` - -=== "Go" - - ```go title="array.go" - /* 随机访问元素 */ - func randomAccess(nums []int) (randomNum int) { - // 在区间 [0, nums.length) 中随机抽取一个数字 - randomIndex := rand.Intn(len(nums)) - // 获取并返回随机元素 - randomNum = nums[randomIndex] - return - } - ``` - -=== "Swift" - - ```swift title="array.swift" - /* 随机访问元素 */ - func randomAccess(nums: [Int]) -> Int { - // 在区间 [0, nums.count) 中随机抽取一个数字 - let randomIndex = nums.indices.randomElement()! - // 获取并返回随机元素 - let randomNum = nums[randomIndex] - return randomNum - } - ``` - -=== "JS" - - ```javascript title="array.js" - /* 随机访问元素 */ - function randomAccess(nums) { - // 在区间 [0, nums.length) 中随机抽取一个数字 - const random_index = Math.floor(Math.random() * nums.length); - // 获取并返回随机元素 - const random_num = nums[random_index]; - return random_num; - } - ``` - -=== "TS" - - ```typescript title="array.ts" - /* 随机访问元素 */ - function randomAccess(nums: number[]): number { - // 在区间 [0, nums.length) 中随机抽取一个数字 - const random_index = Math.floor(Math.random() * nums.length); - // 获取并返回随机元素 - const random_num = nums[random_index]; - return random_num; - } - ``` - -=== "Dart" - - ```dart title="array.dart" - /* 随机访问元素 */ - int randomAccess(List nums) { - // 在区间 [0, nums.length) 中随机抽取一个数字 - int randomIndex = Random().nextInt(nums.length); - // 获取并返回随机元素 - int randomNum = nums[randomIndex]; - return randomNum; - } - ``` - -=== "Rust" - - ```rust title="array.rs" - /* 随机访问元素 */ - fn random_access(nums: &[i32]) -> i32 { - // 在区间 [0, nums.len()) 中随机抽取一个数字 - let random_index = rand::thread_rng().gen_range(0..nums.len()); - // 获取并返回随机元素 - let random_num = nums[random_index]; - random_num - } - ``` - -=== "C" - - ```c title="array.c" - /* 随机访问元素 */ - int randomAccess(int *nums, int size) { - // 在区间 [0, size) 中随机抽取一个数字 - int randomIndex = rand() % size; - // 获取并返回随机元素 - int randomNum = nums[randomIndex]; - return randomNum; - } - ``` - -=== "Zig" - - ```zig title="array.zig" - // 随机访问元素 - fn randomAccess(nums: []i32) i32 { - // 在区间 [0, nums.len) 中随机抽取一个整数 - var randomIndex = std.crypto.random.intRangeLessThan(usize, 0, nums.len); - // 获取并返回随机元素 - var randomNum = nums[randomIndex]; - return randomNum; - } - ``` - -### 3. 插入元素 - -数组元素在内存中是“紧挨着的”,它们之间没有空间再存放任何数据。如图 4-3 所示,如果想要在数组中间插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。 - - - -图 4-3 数组插入元素示例
- -值得注意的是,由于数组的长度是固定的,因此插入一个元素必定会导致数组尾部元素的“丢失”。我们将这个问题的解决方案留在列表章节中讨论。 - -=== "Python" - - ```python title="array.py" - def insert(nums: list[int], num: int, index: int): - """在数组的索引 index 处插入元素 num""" - # 把索引 index 以及之后的所有元素向后移动一位 - for i in range(len(nums) - 1, index, -1): - nums[i] = nums[i - 1] - # 将 num 赋给 index 处元素 - nums[index] = num - ``` - -=== "C++" - - ```cpp title="array.cpp" - /* 在数组的索引 index 处插入元素 num */ - void insert(int *nums, int size, int num, int index) { - // 把索引 index 以及之后的所有元素向后移动一位 - for (int i = size - 1; i > index; i--) { - nums[i] = nums[i - 1]; - } - // 将 num 赋给 index 处元素 - nums[index] = num; - } - ``` - -=== "Java" - - ```java title="array.java" - /* 在数组的索引 index 处插入元素 num */ - void insert(int[] nums, int num, int index) { - // 把索引 index 以及之后的所有元素向后移动一位 - for (int i = nums.length - 1; i > index; i--) { - nums[i] = nums[i - 1]; - } - // 将 num 赋给 index 处元素 - nums[index] = num; - } - ``` - -=== "C#" - - ```csharp title="array.cs" - /* 在数组的索引 index 处插入元素 num */ - void insert(int[] nums, int num, int index) { - // 把索引 index 以及之后的所有元素向后移动一位 - for (int i = nums.Length - 1; i > index; i--) { - nums[i] = nums[i - 1]; - } - // 将 num 赋给 index 处元素 - nums[index] = num; - } - ``` - -=== "Go" - - ```go title="array.go" - /* 在数组的索引 index 处插入元素 num */ - func insert(nums []int, num int, index int) { - // 把索引 index 以及之后的所有元素向后移动一位 - for i := len(nums) - 1; i > index; i-- { - nums[i] = nums[i-1] - } - // 将 num 赋给 index 处元素 - nums[index] = num - } - ``` - -=== "Swift" - - ```swift title="array.swift" - /* 在数组的索引 index 处插入元素 num */ - func insert(nums: inout [Int], num: Int, index: Int) { - // 把索引 index 以及之后的所有元素向后移动一位 - for i in sequence(first: nums.count - 1, next: { $0 > index + 1 ? $0 - 1 : nil }) { - nums[i] = nums[i - 1] - } - // 将 num 赋给 index 处元素 - nums[index] = num - } - ``` - -=== "JS" - - ```javascript title="array.js" - /* 在数组的索引 index 处插入元素 num */ - function insert(nums, num, index) { - // 把索引 index 以及之后的所有元素向后移动一位 - for (let i = nums.length - 1; i > index; i--) { - nums[i] = nums[i - 1]; - } - // 将 num 赋给 index 处元素 - nums[index] = num; - } - ``` - -=== "TS" - - ```typescript title="array.ts" - /* 在数组的索引 index 处插入元素 num */ - function insert(nums: number[], num: number, index: number): void { - // 把索引 index 以及之后的所有元素向后移动一位 - for (let i = nums.length - 1; i > index; i--) { - nums[i] = nums[i - 1]; - } - // 将 num 赋给 index 处元素 - nums[index] = num; - } - ``` - -=== "Dart" - - ```dart title="array.dart" - /* 在数组的索引 index 处插入元素 num */ - void insert(List nums, int num, int index) { - // 把索引 index 以及之后的所有元素向后移动一位 - for (var i = nums.length - 1; i > index; i--) { - nums[i] = nums[i - 1]; - } - // 将 num 赋给 index 处元素 - nums[index] = num; - } - ``` - -=== "Rust" - - ```rust title="array.rs" - /* 在数组的索引 index 处插入元素 num */ - fn insert(nums: &mut Vec图 4-4 数组删除元素示例
- -请注意,删除元素完成后,原先末尾的元素变得“无意义”了,所以我们无须特意去修改它。 - -=== "Python" - - ```python title="array.py" - def remove(nums: list[int], index: int): - """删除索引 index 处元素""" - # 把索引 index 之后的所有元素向前移动一位 - for i in range(index, len(nums) - 1): - nums[i] = nums[i + 1] - ``` - -=== "C++" - - ```cpp title="array.cpp" - /* 删除索引 index 处元素 */ - void remove(int *nums, int size, int index) { - // 把索引 index 之后的所有元素向前移动一位 - for (int i = index; i < size - 1; i++) { - nums[i] = nums[i + 1]; - } - } - ``` - -=== "Java" - - ```java title="array.java" - /* 删除索引 index 处元素 */ - void remove(int[] nums, int index) { - // 把索引 index 之后的所有元素向前移动一位 - for (int i = index; i < nums.length - 1; i++) { - nums[i] = nums[i + 1]; - } - } - ``` - -=== "C#" - - ```csharp title="array.cs" - /* 删除索引 index 处元素 */ - void remove(int[] nums, int index) { - // 把索引 index 之后的所有元素向前移动一位 - for (int i = index; i < nums.Length - 1; i++) { - nums[i] = nums[i + 1]; - } - } - ``` - -=== "Go" - - ```go title="array.go" - /* 删除索引 index 处元素 */ - func remove(nums []int, index int) { - // 把索引 index 之后的所有元素向前移动一位 - for i := index; i < len(nums)-1; i++ { - nums[i] = nums[i+1] - } - } - ``` - -=== "Swift" - - ```swift title="array.swift" - /* 删除索引 index 处元素 */ - func remove(nums: inout [Int], index: Int) { - let count = nums.count - // 把索引 index 之后的所有元素向前移动一位 - for i in sequence(first: index, next: { $0 < count - 1 - 1 ? $0 + 1 : nil }) { - nums[i] = nums[i + 1] - } - } - ``` - -=== "JS" - - ```javascript title="array.js" - /* 删除索引 index 处元素 */ - function remove(nums, index) { - // 把索引 index 之后的所有元素向前移动一位 - for (let i = index; i < nums.length - 1; i++) { - nums[i] = nums[i + 1]; - } - } - ``` - -=== "TS" - - ```typescript title="array.ts" - /* 删除索引 index 处元素 */ - function remove(nums: number[], index: number): void { - // 把索引 index 之后的所有元素向前移动一位 - for (let i = index; i < nums.length - 1; i++) { - nums[i] = nums[i + 1]; - } - } - ``` - -=== "Dart" - - ```dart title="array.dart" - /* 删除索引 index 处元素 */ - void remove(List nums, int index) { - // 把索引 index 之后的所有元素向前移动一位 - for (var i = index; i < nums.length - 1; i++) { - nums[i] = nums[i + 1]; - } - } - ``` - -=== "Rust" - - ```rust title="array.rs" - /* 删除索引 index 处元素 */ - fn remove(nums: &mut Vec图 13-1 在前序遍历中搜索节点
- -## 13.1.1 尝试与回退 - -**之所以称之为回溯算法,是因为该算法在搜索解空间时会采用“尝试”与“回退”的策略**。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。 - -对于例题一,访问每个节点都代表一次“尝试”,而越过叶结点或返回父节点的 `return` 则表示“回退”。 - -值得说明的是,**回退并不仅仅包括函数返回**。为解释这一点,我们对例题一稍作拓展。 - -!!! question "例题二" - - 在二叉树中搜索所有值为 $7$ 的节点,**请返回根节点到这些节点的路径**。 - -在例题一代码的基础上,我们需要借助一个列表 `path` 记录访问过的节点路径。当访问到值为 $7$ 的节点时,则复制 `path` 并添加进结果列表 `res` 。遍历完成后,`res` 中保存的就是所有的解。 - -=== "Python" - - ```python title="preorder_traversal_ii_compact.py" - def pre_order(root: TreeNode): - """前序遍历:例题二""" - if root is None: - return - # 尝试 - path.append(root) - if root.val == 7: - # 记录解 - res.append(list(path)) - pre_order(root.left) - pre_order(root.right) - # 回退 - path.pop() - ``` - -=== "C++" - - ```cpp title="preorder_traversal_ii_compact.cpp" - /* 前序遍历:例题二 */ - void preOrder(TreeNode *root) { - if (root == nullptr) { - return; - } - // 尝试 - path.push_back(root); - if (root->val == 7) { - // 记录解 - res.push_back(path); - } - preOrder(root->left); - preOrder(root->right); - // 回退 - path.pop_back(); - } - ``` - -=== "Java" - - ```java title="preorder_traversal_ii_compact.java" - /* 前序遍历:例题二 */ - void preOrder(TreeNode root) { - if (root == null) { - return; - } - // 尝试 - path.add(root); - if (root.val == 7) { - // 记录解 - res.add(new ArrayList<>(path)); - } - preOrder(root.left); - preOrder(root.right); - // 回退 - path.remove(path.size() - 1); - } - ``` - -=== "C#" - - ```csharp title="preorder_traversal_ii_compact.cs" - /* 前序遍历:例题二 */ - void preOrder(TreeNode root) { - if (root == null) { - return; - } - // 尝试 - path.Add(root); - if (root.val == 7) { - // 记录解 - res.Add(new List图 13-2 尝试与回退
- -## 13.1.2 剪枝 - -复杂的回溯问题通常包含一个或多个约束条件,**约束条件通常可用于“剪枝”**。 - -!!! question "例题三" - - 在二叉树中搜索所有值为 $7$ 的节点,请返回根节点到这些节点的路径,**并要求路径中不包含值为 $3$ 的节点**。 - -为了满足以上约束条件,**我们需要添加剪枝操作**:在搜索过程中,若遇到值为 $3$ 的节点,则提前返回,停止继续搜索。 - -=== "Python" - - ```python title="preorder_traversal_iii_compact.py" - def pre_order(root: TreeNode): - """前序遍历:例题三""" - # 剪枝 - if root is None or root.val == 3: - return - # 尝试 - path.append(root) - if root.val == 7: - # 记录解 - res.append(list(path)) - pre_order(root.left) - pre_order(root.right) - # 回退 - path.pop() - ``` - -=== "C++" - - ```cpp title="preorder_traversal_iii_compact.cpp" - /* 前序遍历:例题三 */ - void preOrder(TreeNode *root) { - // 剪枝 - if (root == nullptr || root->val == 3) { - return; - } - // 尝试 - path.push_back(root); - if (root->val == 7) { - // 记录解 - res.push_back(path); - } - preOrder(root->left); - preOrder(root->right); - // 回退 - path.pop_back(); - } - ``` - -=== "Java" - - ```java title="preorder_traversal_iii_compact.java" - /* 前序遍历:例题三 */ - void preOrder(TreeNode root) { - // 剪枝 - if (root == null || root.val == 3) { - return; - } - // 尝试 - path.add(root); - if (root.val == 7) { - // 记录解 - res.add(new ArrayList<>(path)); - } - preOrder(root.left); - preOrder(root.right); - // 回退 - path.remove(path.size() - 1); - } - ``` - -=== "C#" - - ```csharp title="preorder_traversal_iii_compact.cs" - /* 前序遍历:例题三 */ - void preOrder(TreeNode root) { - // 剪枝 - if (root == null || root.val == 3) { - return; - } - // 尝试 - path.Add(root); - if (root.val == 7) { - // 记录解 - res.Add(new List图 13-3 根据约束条件剪枝
- -## 13.1.3 框架代码 - -接下来,我们尝试将回溯的“尝试、回退、剪枝”的主体框架提炼出来,提升代码的通用性。 - -在以下框架代码中,`state` 表示问题的当前状态,`choices` 表示当前状态下可以做出的选择。 - -=== "Python" - - ```python title="" - def backtrack(state: State, choices: list[choice], res: list[state]): - """回溯算法框架""" - # 判断是否为解 - if is_solution(state): - # 记录解 - record_solution(state, res) - # 停止继续搜索 - return - # 遍历所有选择 - for choice in choices: - # 剪枝:判断选择是否合法 - if is_valid(state, choice): - # 尝试:做出选择,更新状态 - make_choice(state, choice) - backtrack(state, choices, res) - # 回退:撤销选择,恢复到之前的状态 - undo_choice(state, choice) - ``` - -=== "C++" - - ```cpp title="" - /* 回溯算法框架 */ - void backtrack(State *state, vector图 13-4 保留与删除 return 的搜索过程对比
- -相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰嗦,但通用性更好。实际上,**许多回溯问题都可以在该框架下解决**。我们只需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法即可。 - -## 13.1.4 常用术语 - -为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。 - -表 13-1 常见的回溯算法术语
- -图 13-15 4 皇后问题的解
- -图 13-16 展示了本题的三个约束条件:**多个皇后不能在同一行、同一列、同一对角线**。值得注意的是,对角线分为主对角线 `\` 和次对角线 `/` 两种。 - - - -图 13-16 n 皇后问题的约束条件
- -### 1. 逐行放置策略 - -皇后的数量和棋盘的行数都为 $n$ ,因此我们容易得到一个推论:**棋盘每行都允许且只允许放置一个皇后**。 - -也就是说,我们可以采取逐行放置策略:从第一行开始,在每行放置一个皇后,直至最后一行结束。 - -如图 13-17 所示,为 $4$ 皇后问题的逐行放置过程。受画幅限制,图 13-17 仅展开了第一行的其中一个搜索分支,并且将不满足列约束和对角线约束的方案都进行了剪枝。 - - - -图 13-17 逐行放置策略
- -本质上看,**逐行放置策略起到了剪枝的作用**,它避免了同一行出现多个皇后的所有搜索分支。 - -### 2. 列与对角线剪枝 - -为了满足列约束,我们可以利用一个长度为 $n$ 的布尔型数组 `cols` 记录每一列是否有皇后。在每次决定放置前,我们通过 `cols` 将已有皇后的列进行剪枝,并在回溯中动态更新 `cols` 的状态。 - -那么,如何处理对角线约束呢?设棋盘中某个格子的行列索引为 $(row, col)$ ,选定矩阵中的某条主对角线,我们发现该对角线上所有格子的行索引减列索引都相等,**即对角线上所有格子的 $row - col$ 为恒定值**。 - -也就是说,如果两个格子满足 $row_1 - col_1 = row_2 - col_2$ ,则它们一定处在同一条主对角线上。利用该规律,我们可以借助图 13-18 所示的数组 `diag1` ,记录每条主对角线上是否有皇后。 - -同理,**次对角线上的所有格子的 $row + col$ 是恒定值**。我们同样也可以借助数组 `diag2` 来处理次对角线约束。 - - - -图 13-18 处理列约束和对角线约束
- -### 3. 代码实现 - -请注意,$n$ 维方阵中 $row - col$ 的范围是 $[-n + 1, n - 1]$ ,$row + col$ 的范围是 $[0, 2n - 2]$ ,所以主对角线和次对角线的数量都为 $2n - 1$ ,即数组 `diag1` 和 `diag2` 的长度都为 $2n - 1$ 。 - -=== "Python" - - ```python title="n_queens.py" - def backtrack( - row: int, - n: int, - state: list[list[str]], - res: list[list[list[str]]], - cols: list[bool], - diags1: list[bool], - diags2: list[bool], - ): - """回溯算法:N 皇后""" - # 当放置完所有行时,记录解 - if row == n: - res.append([list(row) for row in state]) - return - # 遍历所有列 - for col in range(n): - # 计算该格子对应的主对角线和副对角线 - diag1 = row - col + n - 1 - diag2 = row + col - # 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后 - if not cols[col] and not diags1[diag1] and not diags2[diag2]: - # 尝试:将皇后放置在该格子 - state[row][col] = "Q" - cols[col] = diags1[diag1] = diags2[diag2] = True - # 放置下一行 - backtrack(row + 1, n, state, res, cols, diags1, diags2) - # 回退:将该格子恢复为空位 - state[row][col] = "#" - cols[col] = diags1[diag1] = diags2[diag2] = False - - def n_queens(n: int) -> list[list[list[str]]]: - """求解 N 皇后""" - # 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位 - state = [["#" for _ in range(n)] for _ in range(n)] - cols = [False] * n # 记录列是否有皇后 - diags1 = [False] * (2 * n - 1) # 记录主对角线是否有皇后 - diags2 = [False] * (2 * n - 1) # 记录副对角线是否有皇后 - res = [] - backtrack(0, n, state, res, cols, diags1, diags2) - - return res - ``` - -=== "C++" - - ```cpp title="n_queens.cpp" - /* 回溯算法:N 皇后 */ - void backtrack(int row, int n, vector表 13-2 数组与链表的效率对比
- -图 13-5 全排列的递归树
- -### 1. 重复选择剪枝 - -为了实现每个元素只被选择一次,我们考虑引入一个布尔型数组 `selected` ,其中 `selected[i]` 表示 `choices[i]` 是否已被选择,并基于它实现以下剪枝操作。 - -- 在做出选择 `choice[i]` 后,我们就将 `selected[i]` 赋值为 $\text{True}$ ,代表它已被选择。 -- 遍历选择列表 `choices` 时,跳过所有已被选择过的节点,即剪枝。 - -如图 13-6 所示,假设我们第一轮选择 1 ,第二轮选择 3 ,第三轮选择 2 ,则需要在第二轮剪掉元素 1 的分支,在第三轮剪掉元素 1 和元素 3 的分支。 - - - -图 13-6 全排列剪枝示例
- -观察图 13-6 发现,该剪枝操作将搜索空间大小从 $O(n^n)$ 降低至 $O(n!)$ 。 - -### 2. 代码实现 - -想清楚以上信息之后,我们就可以在框架代码中做“完形填空”了。为了缩短代码行数,我们不单独实现框架代码中的各个函数,而是将他们展开在 `backtrack()` 函数中。 - -=== "Python" - - ```python title="permutations_i.py" - def backtrack( - state: list[int], choices: list[int], selected: list[bool], res: list[list[int]] - ): - """回溯算法:全排列 I""" - # 当状态长度等于元素数量时,记录解 - if len(state) == len(choices): - res.append(list(state)) - return - # 遍历所有选择 - for i, choice in enumerate(choices): - # 剪枝:不允许重复选择元素 - if not selected[i]: - # 尝试:做出选择,更新状态 - selected[i] = True - state.append(choice) - # 进行下一轮选择 - backtrack(state, choices, selected, res) - # 回退:撤销选择,恢复到之前的状态 - selected[i] = False - state.pop() - - def permutations_i(nums: list[int]) -> list[list[int]]: - """全排列 I""" - res = [] - backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res) - return res - ``` - -=== "C++" - - ```cpp title="permutations_i.cpp" - /* 回溯算法:全排列 I */ - void backtrack(vector图 13-7 重复排列
- -那么如何去除重复的排列呢?最直接地,考虑借助一个哈希表,直接对排列结果进行去重。然而这样做不够优雅,**因为生成重复排列的搜索分支是没有必要的,应当被提前识别并剪枝**,这样可以进一步提升算法效率。 - -### 1. 相等元素剪枝 - -观察图 13-8 ,在第一轮中,选择 $1$ 或选择 $\hat{1}$ 是等价的,在这两个选择之下生成的所有排列都是重复的。因此应该把 $\hat{1}$ 剪枝掉。 - -同理,在第一轮选择 $2$ 之后,第二轮选择中的 $1$ 和 $\hat{1}$ 也会产生重复分支,因此也应将第二轮的 $\hat{1}$ 剪枝。 - -本质上看,**我们的目标是在某一轮选择中,保证多个相等的元素仅被选择一次**。 - - - -图 13-8 重复排列剪枝
- -### 2. 代码实现 - -在上一题的代码的基础上,我们考虑在每一轮选择中开启一个哈希表 `duplicated` ,用于记录该轮中已经尝试过的元素,并将重复元素剪枝。 - -=== "Python" - - ```python title="permutations_ii.py" - def backtrack( - state: list[int], choices: list[int], selected: list[bool], res: list[list[int]] - ): - """回溯算法:全排列 II""" - # 当状态长度等于元素数量时,记录解 - if len(state) == len(choices): - res.append(list(state)) - return - # 遍历所有选择 - duplicated = set[int]() - for i, choice in enumerate(choices): - # 剪枝:不允许重复选择元素 且 不允许重复选择相等元素 - if not selected[i] and choice not in duplicated: - # 尝试:做出选择,更新状态 - duplicated.add(choice) # 记录选择过的元素值 - selected[i] = True - state.append(choice) - # 进行下一轮选择 - backtrack(state, choices, selected, res) - # 回退:撤销选择,恢复到之前的状态 - selected[i] = False - state.pop() - - def permutations_ii(nums: list[int]) -> list[list[int]]: - """全排列 II""" - res = [] - backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res) - return res - ``` - -=== "C++" - - ```cpp title="permutations_ii.cpp" - /* 回溯算法:全排列 II */ - void backtrack(vector图 13-9 两种剪枝条件的作用范围
diff --git a/chapter_backtracking/subset_sum_problem.md b/chapter_backtracking/subset_sum_problem.md deleted file mode 100644 index b00d00da9..000000000 --- a/chapter_backtracking/subset_sum_problem.md +++ /dev/null @@ -1,1441 +0,0 @@ ---- -comments: true ---- - -# 13.3 子集和问题 - -## 13.3.1 无重复元素的情况 - -!!! question - - 给定一个正整数数组 `nums` 和一个目标正整数 `target` ,请找出所有可能的组合,使得组合中的元素和等于 `target` 。给定数组无重复元素,每个元素可以被选取多次。请以列表形式返回这些组合,列表中不应包含重复组合。 - -例如,输入集合 $\{3, 4, 5\}$ 和目标整数 $9$ ,解为 $\{3, 3, 3\}, \{4, 5\}$ 。需要注意以下两点。 - -- 输入集合中的元素可以被无限次重复选取。 -- 子集是不区分元素顺序的,比如 $\{4, 5\}$ 和 $\{5, 4\}$ 是同一个子集。 - -### 1. 参考全排列解法 - -类似于全排列问题,我们可以把子集的生成过程想象成一系列选择的结果,并在选择过程中实时更新“元素和”,当元素和等于 `target` 时,就将子集记录至结果列表。 - -而与全排列问题不同的是,**本题集合中的元素可以被无限次选取**,因此无须借助 `selected` 布尔列表来记录元素是否已被选择。我们可以对全排列代码进行小幅修改,初步得到解题代码。 - -=== "Python" - - ```python title="subset_sum_i_naive.py" - def backtrack( - state: list[int], - target: int, - total: int, - choices: list[int], - res: list[list[int]], - ): - """回溯算法:子集和 I""" - # 子集和等于 target 时,记录解 - if total == target: - res.append(list(state)) - return - # 遍历所有选择 - for i in range(len(choices)): - # 剪枝:若子集和超过 target ,则跳过该选择 - if total + choices[i] > target: - continue - # 尝试:做出选择,更新元素和 total - state.append(choices[i]) - # 进行下一轮选择 - backtrack(state, target, total + choices[i], choices, res) - # 回退:撤销选择,恢复到之前的状态 - state.pop() - - def subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]: - """求解子集和 I(包含重复子集)""" - state = [] # 状态(子集) - total = 0 # 子集和 - res = [] # 结果列表(子集列表) - backtrack(state, target, total, nums, res) - return res - ``` - -=== "C++" - - ```cpp title="subset_sum_i_naive.cpp" - /* 回溯算法:子集和 I */ - void backtrack(vector