mirror of
https://github.com/krahets/hello-algo.git
synced 2026-05-02 14:33:06 +08:00
Revisit the English version (#1835)
* Review the English version using Claude-4.5. * Update mkdocs.yml * Align the section titles. * Bug fixes
This commit is contained in:
@@ -1,42 +1,42 @@
|
||||
# Divide and conquer search strategy
|
||||
|
||||
We have learned that search algorithms fall into two main categories.
|
||||
We have already learned that search algorithms are divided into two major categories.
|
||||
|
||||
- **Brute-force search**: It is implemented by traversing the data structure, with a time complexity of $O(n)$.
|
||||
- **Adaptive search**: It utilizes a unique data organization form or prior information, and its time complexity can reach $O(\log n)$ or even $O(1)$.
|
||||
- **Brute-force search**: Implemented by traversing the data structure, with a time complexity of $O(n)$.
|
||||
- **Adaptive search**: Utilizes unique data organization forms or prior information, with time complexity reaching $O(\log n)$ or even $O(1)$.
|
||||
|
||||
In fact, **search algorithms with a time complexity of $O(\log n)$ are usually based on the divide-and-conquer strategy**, such as binary search and trees.
|
||||
In fact, **search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy**, such as binary search and trees.
|
||||
|
||||
- Each step of binary search divides the problem (searching for a target element in an array) into a smaller problem (searching for the target element in half of the array), continuing until the array is empty or the target element is found.
|
||||
- Trees represent the divide-and-conquer idea, where in data structures like binary search trees, AVL trees, and heaps, the time complexity of various operations is $O(\log n)$.
|
||||
- Trees are representative of the divide and conquer idea. In data structures such as binary search trees, AVL trees, and heaps, the time complexity of various operations is $O(\log n)$.
|
||||
|
||||
The divide-and-conquer strategy of binary search is as follows.
|
||||
The divide and conquer strategy of binary search is as follows.
|
||||
|
||||
- **The problem can be divided**: Binary search recursively divides the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element.
|
||||
- **Subproblems are independent**: In binary search, each round handles one subproblem, unaffected by other subproblems.
|
||||
- **The solutions of subproblems do not need to be merged**: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved.
|
||||
- **The problem can be decomposed**: Binary search recursively decomposes the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element.
|
||||
- **Subproblems are independent**: In binary search, each round only processes one subproblem, which is not affected by other subproblems.
|
||||
- **Solutions of subproblems do not need to be merged**: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved.
|
||||
|
||||
Divide-and-conquer can enhance search efficiency because brute-force search can only eliminate one option per round, **whereas divide-and-conquer can eliminate half of the options**.
|
||||
Divide and conquer can improve search efficiency because brute-force search can only eliminate one option per round, **while divide and conquer search can eliminate half of the options per round**.
|
||||
|
||||
### Implementing binary search based on divide-and-conquer
|
||||
### Implementing binary search based on divide and conquer
|
||||
|
||||
In previous chapters, binary search was implemented based on iteration. Now, we implement it based on divide-and-conquer (recursion).
|
||||
In previous sections, binary search was implemented based on iteration. Now we implement it based on divide and conquer (recursion).
|
||||
|
||||
!!! question
|
||||
|
||||
Given an ordered array `nums` of length $n$, where all elements are unique, please find the element `target`.
|
||||
Given a sorted array `nums` of length $n$, where all elements are unique, find the element `target`.
|
||||
|
||||
From a divide-and-conquer perspective, we denote the subproblem corresponding to the search interval $[i, j]$ as $f(i, j)$.
|
||||
From a divide and conquer perspective, we denote the subproblem corresponding to the search interval $[i, j]$ as $f(i, j)$.
|
||||
|
||||
Starting from the original problem $f(0, n-1)$, perform the binary search through the following steps.
|
||||
Starting from the original problem $f(0, n-1)$, perform binary search through the following steps.
|
||||
|
||||
1. Calculate the midpoint $m$ of the search interval $[i, j]$, and use it to eliminate half of the search interval.
|
||||
2. Recursively solve the subproblem reduced by half in size, which could be $f(i, m-1)$ or $f(m+1, j)$.
|
||||
3. Repeat steps `1.` and `2.`, until `target` is found or the interval is empty and returns.
|
||||
3. Repeat steps `1.` and `2.` until `target` is found or the interval is empty and return.
|
||||
|
||||
The figure below shows the divide-and-conquer process of binary search for element $6$ in an array.
|
||||
The figure below shows the divide and conquer process of binary search for element $6$ in an array.
|
||||
|
||||

|
||||

|
||||
|
||||
In the implementation code, we declare a recursive function `dfs()` to solve the problem $f(i, j)$:
|
||||
|
||||
|
||||
@@ -2,66 +2,66 @@
|
||||
|
||||
!!! question
|
||||
|
||||
Given the pre-order traversal `preorder` sequence and the in-order traversal `inorder` sequence of a binary tree, construct the binary tree and return its root node. Assume there are no duplicate node values in the binary tree (as shown in the figure below).
|
||||
Given the preorder traversal `preorder` and inorder traversal `inorder` of a binary tree, construct the binary tree and return the root node of the binary tree. Assume there are no duplicate node values in the binary tree (as shown in the figure below).
|
||||
|
||||

|
||||
|
||||
### Determining if it is a divide-and-conquer problem
|
||||
### Determining if it is a divide and conquer problem
|
||||
|
||||
The original problem of building a binary tree from the `preorder` and the `inorder` sequences is a typical divide-and-conquer problem.
|
||||
The original problem is defined as constructing a binary tree from `preorder` and `inorder`, which is a typical divide and conquer problem.
|
||||
|
||||
- **The problem can be decomposed**: From the perspective of divide-and-conquer, we can divide the original problem into two subproblems—building the left subtree and building the right subtree—plus one operation of initializing the root node. For each subtree (subproblem), we continue applying the same approach, partitioning it into smaller subtrees (subproblems), until reaching the smallest subproblem (an empty subtree).
|
||||
- **The subproblems are independent**: The left and right subtrees do not overlap. When building the left subtree, we only need the segments of the in-order and pre-order traversals that correspond to the left subtree. The same approach applies to the right subtree.
|
||||
- **Solutions to subproblems can be combined**: Once we have constructed the left and right subtrees (the subproblem solutions), we can attach them to the root node to obtain the solution to the original problem.
|
||||
- **The problem can be decomposed**: From a divide and conquer perspective, we can divide the original problem into two subproblems: constructing the left subtree and constructing the right subtree, plus one operation: initializing the root node. For each subtree (subproblem), we can still reuse the above division method, dividing it into smaller subtrees (subproblems) until the smallest subproblem (empty subtree) is reached.
|
||||
- **Subproblems are independent**: The left and right subtrees are independent of each other; there is no overlap between them. When constructing the left subtree, we only need to focus on the parts of the inorder and preorder traversals corresponding to the left subtree. The same applies to the right subtree.
|
||||
- **Solutions of subproblems can be merged**: Once we have the left and right subtrees (solutions of subproblems), we can link them to the root node to obtain the solution to the original problem.
|
||||
|
||||
### How to divide the subtrees
|
||||
### How to divide subtrees
|
||||
|
||||
Based on the above analysis, this problem can be solved using divide-and-conquer. **However, how do we use the pre-order traversal `preorder` sequence and the in-order traversal `inorder` sequence to divide the left and right subtrees?**
|
||||
Based on the above analysis, this problem can be solved using divide and conquer, **but how do we divide the left and right subtrees through the preorder traversal `preorder` and inorder traversal `inorder`**?
|
||||
|
||||
By definition, both the `preorder` and `inorder` sequences can be divided into three parts:
|
||||
According to the definition, both `preorder` and `inorder` can be divided into three parts.
|
||||
|
||||
- Pre-order traversal: `[ Root | Left Subtree | Right Subtree ]`. For example, in the figure, the tree corresponds to `[ 3 | 9 | 2 1 7 ]`.
|
||||
- In-order traversal: `[ Left Subtree | Root | Right Subtree ]`. For example, in the figure, the tree corresponds to `[ 9 | 3 | 1 2 7 ]`.
|
||||
- Preorder traversal: `[ Root Node | Left Subtree | Right Subtree ]`, for example, the tree in the figure above corresponds to `[ 3 | 9 | 2 1 7 ]`.
|
||||
- Inorder traversal: `[ Left Subtree | Root Node | Right Subtree ]`, for example, the tree in the figure above corresponds to `[ 9 | 3 | 1 2 7 ]`.
|
||||
|
||||
Using the data from the preceding figure, we can follow the steps shown in the next figure to obtain the division results:
|
||||
Using the data from the figure above as an example, we can obtain the division results through the steps shown in the figure below.
|
||||
|
||||
1. The first element 3 in the pre-order traversal is the value of the root node.
|
||||
2. Find the index of the root node 3 in the `inorder` sequence, and use this index to split `inorder` into `[ 9 | 3 | 1 2 7 ]`.
|
||||
3. According to the split of the `inorder` sequence, it is straightforward to determine that the left and right subtrees contain 1 and 3 nodes, respectively, so we can split the `preorder` sequence into `[ 3 | 9 | 2 1 7 ]` accordingly.
|
||||
1. The first element 3 in the preorder traversal is the value of the root node.
|
||||
2. Find the index of root node 3 in `inorder`, and use this index to divide `inorder` into `[ 9 | 3 | 1 2 7 ]`.
|
||||
3. Based on the division result of `inorder`, it is easy to determine that the left and right subtrees have 1 and 3 nodes respectively, allowing us to divide `preorder` into `[ 3 | 9 | 2 1 7 ]`.
|
||||
|
||||

|
||||

|
||||
|
||||
### Describing subtree ranges based on variables
|
||||
### Describing subtree intervals based on variables
|
||||
|
||||
Based on the above division method, **we have now obtained the index ranges of the root, left subtree, and right subtree in the `preorder` and `inorder` sequences**. To describe these index ranges, we use several pointer variables.
|
||||
Based on the above division method, **we have obtained the index intervals of the root node, left subtree, and right subtree in `preorder` and `inorder`**. To describe these index intervals, we need to use several pointer variables.
|
||||
|
||||
- Let the index of the current tree's root node in the `preorder` sequence be denoted as $i$.
|
||||
- Let the index of the current tree's root node in the `inorder` sequence be denoted as $m$.
|
||||
- Let the index range of the current tree in the `inorder` sequence be denoted as $[l, r]$.
|
||||
- Denote the index of the current tree's root node in `preorder` as $i$.
|
||||
- Denote the index of the current tree's root node in `inorder` as $m$.
|
||||
- Denote the index interval of the current tree in `inorder` as $[l, r]$.
|
||||
|
||||
As shown in the table below, these variables represent the root node’s index in the `preorder` sequence and the index ranges of the subtrees in the `inorder` sequence.
|
||||
As shown in the table below, through these variables we can represent the index of the root node in `preorder` and the index intervals of the subtrees in `inorder`.
|
||||
|
||||
<p align="center"> Table <id> Indexes of the root node and subtrees in pre-order and in-order traversals </p>
|
||||
<p align="center"> Table <id> Indices of root node and subtrees in preorder and inorder traversals </p>
|
||||
|
||||
| | Root node index in `preorder` | Subtree index range in `inorder` |
|
||||
| ------------- | ----------------------------- | ----------------------------------- |
|
||||
| Current tree | $i$ | $[l, r]$ |
|
||||
| Left subtree | $i + 1$ | $[l, m-1]$ |
|
||||
| Right subtree | $i + 1 + (m - l)$ | $[m+1, r]$ |
|
||||
| | Root node index in `preorder` | Subtree index interval in `inorder` |
|
||||
| ------------ | ----------------------------- | ----------------------------------- |
|
||||
| Current tree | $i$ | $[l, r]$ |
|
||||
| Left subtree | $i + 1$ | $[l, m-1]$ |
|
||||
| Right subtree| $i + 1 + (m - l)$ | $[m+1, r]$ |
|
||||
|
||||
Please note that $(m-l)$ in the right subtree root index represents "the number of nodes in the left subtree." It may help to consult the figure below for a clearer understanding.
|
||||
Please note that $(m-l)$ in the right subtree root node index means "the number of nodes in the left subtree". It is recommended to understand this in conjunction with the figure below.
|
||||
|
||||

|
||||

|
||||
|
||||
### Code implementation
|
||||
|
||||
To improve the efficiency of querying $m$, we use a hash table `hmap` to store the mapping from elements in the `inorder` sequence to their indexes:
|
||||
To improve the efficiency of querying $m$, we use a hash table `hmap` to store the mapping from elements in the `inorder` array to their indices:
|
||||
|
||||
```src
|
||||
[file]{build_tree}-[class]{}-[func]{build_tree}
|
||||
```
|
||||
|
||||
The figure below shows the recursive process of building the binary tree. Each node is created during the "descending" phase of the recursion, and each edge (reference) is formed during the "ascending" phase.
|
||||
The figure below shows the recursive process of building the binary tree. Each node is established during the downward "recursion" process, while each edge (reference) is established during the upward "return" process.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
@@ -90,10 +90,10 @@ The figure below shows the recursive process of building the binary tree. Each n
|
||||
=== "<9>"
|
||||

|
||||
|
||||
Each recursive function's division of the `preorder` and `inorder` sequences is illustrated in the figure below.
|
||||
The division results of the preorder traversal `preorder` and inorder traversal `inorder` within each recursive function are shown in the figure below.
|
||||
|
||||

|
||||

|
||||
|
||||
Assuming the binary tree has $n$ nodes, initializing each node (calling the recursive function `dfs()`) takes $O(1)$ time. **Therefore, the overall time complexity is $O(n)$**.
|
||||
Let the number of nodes in the tree be $n$. Initializing each node (executing one recursive function `dfs()`) takes $O(1)$ time. **Therefore, the overall time complexity is $O(n)$**.
|
||||
|
||||
Because the hash table stores the mapping from `inorder` elements to their indexes, it requires $O(n)$ space. In the worst case, if the binary tree degenerates into a linked list, the recursive depth can reach $n$, consuming $O(n)$ stack space. **Hence, the overall space complexity is $O(n)$**.
|
||||
The hash table stores the mapping from `inorder` elements to their indices, with a space complexity of $O(n)$. In the worst case, when the binary tree degenerates into a linked list, the recursion depth reaches $n$, using $O(n)$ stack frame space. **Therefore, the overall space complexity is $O(n)$**.
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
# Divide and conquer algorithms
|
||||
|
||||
<u>Divide and conquer</u> is an important and popular algorithm strategy. As the name suggests, the algorithm is typically implemented recursively and consists of two steps: "divide" and "conquer".
|
||||
<u>Divide and conquer</u> is a very important and common algorithm strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: "divide" and "conquer".
|
||||
|
||||
1. **Divide (partition phase)**: Recursively break down the original problem into two or more smaller sub-problems until the smallest sub-problem is reached.
|
||||
2. **Conquer (merge phase)**: Starting from the smallest sub-problem with known solution, we construct the solution to the original problem by merging the solutions of sub-problems in a bottom-up manner.
|
||||
1. **Divide (partition phase)**: Recursively divide the original problem into two or more subproblems until the smallest subproblem is reached.
|
||||
2. **Conquer (merge phase)**: Starting from the smallest subproblems with known solutions, merge the solutions of subproblems from bottom to top to construct the solution to the original problem.
|
||||
|
||||
As shown in the figure below, "merge sort" is one of the typical applications of the divide and conquer strategy.
|
||||
|
||||
1. **Divide**: Recursively divide the original array (original problem) into two sub-arrays (sub-problems), until the sub-array has only one element (smallest sub-problem).
|
||||
2. **Conquer**: Merge the ordered sub-arrays (solutions to the sub-problems) from bottom to top to obtain an ordered original array (solution to the original problem).
|
||||
1. **Divide**: Recursively divide the original array (original problem) into two subarrays (subproblems) until the subarray has only one element (smallest subproblem).
|
||||
2. **Conquer**: Merge the sorted subarrays (solutions to subproblems) from bottom to top to obtain a sorted original array (solution to the original problem).
|
||||
|
||||

|
||||

|
||||
|
||||
## How to identify divide and conquer problems
|
||||
## How to determine divide and conquer problems
|
||||
|
||||
Whether a problem is suitable for a divide-and-conquer solution can usually be decided based on the following criteria.
|
||||
Whether a problem is suitable for solving with divide and conquer can usually be determined based on the following criteria.
|
||||
|
||||
1. **The problem can be broken down into smaller ones**: The original problem can be divided into smaller, similar sub-problems and such process can be recursively done in the same manner.
|
||||
2. **Sub-problems are independent**: There is no overlap between sub-problems, and they are independent and can be solved separately.
|
||||
3. **Solutions to sub-problems can be merged**: The solution to the original problem is derived by combining the solutions of the sub-problems.
|
||||
1. **The problem can be decomposed**: The original problem can be divided into smaller, similar subproblems, and can be recursively divided in the same way.
|
||||
2. **Subproblems are independent**: There is no overlap between subproblems, they are independent of each other and can be solved independently.
|
||||
3. **Solutions of subproblems can be merged**: The solution to the original problem is obtained by merging the solutions of subproblems.
|
||||
|
||||
Clearly, merge sort meets these three criteria.
|
||||
Clearly, merge sort satisfies these three criteria.
|
||||
|
||||
1. **The problem can be broken down into smaller ones**: Recursively divide the array (original problem) into two sub-arrays (sub-problems).
|
||||
2. **Sub-problems are independent**: Each sub-array can be sorted independently (sub-problems can be solved independently).
|
||||
3. **Solutions to sub-problems can be merged**: Two ordered sub-arrays (solutions to the sub-problems) can be merged into one ordered array (solution to the original problem).
|
||||
1. **The problem can be decomposed**: Recursively divide the array (original problem) into two subarrays (subproblems).
|
||||
2. **Subproblems are independent**: Each subarray can be sorted independently (subproblems can be solved independently).
|
||||
3. **Solutions of subproblems can be merged**: Two sorted subarrays (solutions of subproblems) can be merged into one sorted array (solution of the original problem).
|
||||
|
||||
## Improve efficiency through divide and conquer
|
||||
## Improving efficiency through divide and conquer
|
||||
|
||||
The **divide-and-conquer strategy not only effectively solves algorithm problems but also often enhances efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection sort, bubble sort, and insertion sort because they apply the divide-and-conquer strategy.
|
||||
**Divide and conquer can not only effectively solve algorithmic problems but often also improve algorithm efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy.
|
||||
|
||||
We may have a question in mind: **Why can divide and conquer improve algorithm efficiency, and what is the underlying logic?** In other words, why is breaking a problem into sub-problems, solving them, and combining their solutions to address the original problem offer more efficiency than directly solving the original problem? This question can be analyzed from two aspects: operation count and parallel computation.
|
||||
This raises the question: **Why can divide and conquer improve algorithm efficiency, and what is the underlying logic**? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation.
|
||||
|
||||
### Optimization of operation count
|
||||
### Operation count optimization
|
||||
|
||||
Taking "bubble sort" as an example, it requires $O(n^2)$ time to process an array of length $n$. Suppose we divide the array from the midpoint into two sub-arrays as shown in the figure below, such division requires $O(n)$ time. Sorting each sub-array requires $O((n / 2)^2)$ time. And merging the two sub-arrays requires $O(n)$ time. Thus, the overall time complexity is:
|
||||
Taking "bubble sort" as an example, processing an array of length $n$ requires $O(n^2)$ time. Suppose we divide the array into two subarrays from the midpoint as shown in the figure below, the division requires $O(n)$ time, sorting each subarray requires $O((n / 2)^2)$ time, and merging the two subarrays requires $O(n)$ time, resulting in an overall time complexity of:
|
||||
|
||||
$$
|
||||
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
|
||||
$$
|
||||
|
||||

|
||||

|
||||
|
||||
Let's calculate the following inequality, where the left side represents the total number of operations before division and the right side represents the total number of operations after division, respectively:
|
||||
Next, we compute the following inequality, where the left and right sides represent the total number of operations before and after division, respectively:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -52,40 +52,40 @@ n(n - 4) & > 0
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
**This means that when $n > 4$, the number of operations after partitioning is fewer, leading to better performance**. Please note that the time complexity after partitioning is still quadratic $O(n^2)$, but the constant factor in the complexity has decreased.
|
||||
**This means that when $n > 4$, the number of operations after division is smaller, and sorting efficiency should be higher**. Note that the time complexity after division is still quadratic $O(n^2)$, but the constant term in the complexity has become smaller.
|
||||
|
||||
We can go even further. **How about keeping dividing the sub-arrays from their midpoints into two sub-arrays** until the sub-arrays have only one element left? This idea is actually "merge sort," with a time complexity of $O(n \log n)$.
|
||||
Going further, **what if we continuously divide the subarrays from their midpoints into two subarrays** until the subarrays have only one element? This approach is actually "merge sort", with a time complexity of $O(n \log n)$.
|
||||
|
||||
Let's try something a bit different again. **How about splitting into more partitions instead of just two?** For example, we evenly divide the original array into $k$ sub-arrays? This approach is very similar to "bucket sort," which is very suitable for sorting massive data. Theoretically, the time complexity can reach $O(n + k)$.
|
||||
Thinking further, **what if we set multiple division points** and evenly divide the original array into $k$ subarrays? This situation is very similar to "bucket sort", which is well-suited for sorting massive amounts of data, with a theoretical time complexity of $O(n + k)$.
|
||||
|
||||
### Optimization through parallel computation
|
||||
### Parallel computation optimization
|
||||
|
||||
We know that the sub-problems generated by divide and conquer are independent of each other, **which means that they can be solved in parallel.** As a result, divide and conquer not only reduces the algorithm's time complexity, **but also facilitates parallel optimization by modern operating systems.**
|
||||
We know that the subproblems generated by divide and conquer are independent of each other, **so they can typically be solved in parallel**. This means divide and conquer can not only reduce the time complexity of algorithms, **but also benefits from parallel optimization by operating systems**.
|
||||
|
||||
Parallel optimization is particularly effective in environments with multiple cores or processors. As the system can process multiple sub-problems simultaneously, fully utilizing computing resources, the overall runtime is significantly reduced.
|
||||
Parallel optimization is particularly effective in multi-core or multi-processor environments, as the system can simultaneously handle multiple subproblems, making fuller use of computing resources and significantly reducing overall runtime.
|
||||
|
||||
For example, in the "bucket sort" shown in the figure below, we break massive data evenly into various buckets. The jobs of sorting each bucket can be allocated to available computing units. Once all jobs are done, all sorted buckets are merged to produce the final result.
|
||||
For example, in the "bucket sort" shown in the figure below, we evenly distribute massive data into various buckets, and the sorting tasks for all buckets can be distributed to various computing units. After completion, the results are merged.
|
||||
|
||||

|
||||

|
||||
|
||||
## Common applications of divide and conquer
|
||||
|
||||
Divide and conquer can be used to solve many classic algorithm problems.
|
||||
On one hand, divide and conquer can be used to solve many classic algorithmic problems.
|
||||
|
||||
- **Finding the closest pair of points**: This algorithm works by dividing the set of points into two halves. Then it recursively finds the closest pair in each half. Finally it considers pairs that span the two halves to find the overall closest pair.
|
||||
- **Large integer multiplication**: One algorithm is called Karatsuba. It breaks down large integer multiplication into several smaller integer multiplications and additions.
|
||||
- **Matrix multiplication**: One example is the Strassen algorithm. It breaks down a large matrix multiplication into multiple small matrix multiplications and additions.
|
||||
- **Tower of Hanoi problem**: The Tower of Hanoi problem can be solved recursively, a typical application of the divide-and-conquer strategy.
|
||||
- **Solving inversion pairs**: In a sequence, if a preceding number is greater than a following number, then these two numbers constitute an inversion pair. Solving inversion pair problem can utilize the idea of divide and conquer, with the aid of merge sort.
|
||||
- **Finding the closest pair of points**: This algorithm first divides the point set into two parts, then finds the closest pair of points in each part separately, and finally finds the closest pair of points that spans both parts.
|
||||
- **Large integer multiplication**: For example, the Karatsuba algorithm, which decomposes large integer multiplication into several smaller integer multiplications and additions.
|
||||
- **Matrix multiplication**: For example, the Strassen algorithm, which decomposes large matrix multiplication into multiple small matrix multiplications and additions.
|
||||
- **Hanota problem**: The hanota problem can be solved through recursion, which is a typical application of the divide and conquer strategy.
|
||||
- **Solving inversion pairs**: In a sequence, if a preceding number is greater than a following number, these two numbers form an inversion pair. Solving the inversion pair problem can utilize the divide and conquer approach with the help of merge sort.
|
||||
|
||||
Divide and conquer is also widely applied in the design of algorithms and data structures.
|
||||
On the other hand, divide and conquer is widely applied in the design of algorithms and data structures.
|
||||
|
||||
- **Binary search**: Binary search divides a sorted array into two halves from the midpoint index. And then based on the comparison result between the target value and the middle element value, one half is discarded. The search continues on the remaining half with the same process until the target is found or there is no remaining element.
|
||||
- **Merge sort**: Already introduced at the beginning of this section, no further elaboration is needed.
|
||||
- **Quicksort**: Quicksort picks a pivot value to divide the array into two sub-arrays, one with elements smaller than the pivot and the other with elements larger than the pivot. Such process goes on against each of these two sub-arrays until they hold only one element.
|
||||
- **Bucket sort**: The basic idea of bucket sort is to distribute data to multiple buckets. After sorting the elements within each bucket, retrieve the elements from the buckets in order to obtain an ordered array.
|
||||
- **Trees**: For example, binary search trees, AVL trees, red-black trees, B-trees, and B+ trees, etc. Their operations, such as search, insertion, and deletion, can all be regarded as applications of the divide-and-conquer strategy.
|
||||
- **Heap**: A heap is a special type of complete binary tree. Its various operations, such as insertion, deletion, and heapify, actually imply the idea of divide and conquer.
|
||||
- **Hash table**: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the strategy. For example, long lists in chained addressing may be converted to red-black trees to improve query efficiency.
|
||||
- **Binary search**: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary operation on the remaining interval.
|
||||
- **Merge sort**: Already introduced at the beginning of this section, no further elaboration needed.
|
||||
- **Quick sort**: Quick sort selects a pivot value, then divides the array into two subarrays, one with elements smaller than the pivot and the other with elements larger than the pivot, then performs the same division operation on these two parts until the subarrays have only one element.
|
||||
- **Bucket sort**: The basic idea of bucket sort is to scatter data into multiple buckets, then sort the elements within each bucket, and finally extract the elements from each bucket in sequence to obtain a sorted array.
|
||||
- **Trees**: For example, binary search trees, AVL trees, red-black trees, B-trees, B+ trees, etc. Their search, insertion, and deletion operations can all be viewed as applications of the divide and conquer strategy.
|
||||
- **Heaps**: A heap is a special complete binary tree, and its various operations, such as insertion, deletion, and heapify, actually imply the divide and conquer idea.
|
||||
- **Hash tables**: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve query efficiency.
|
||||
|
||||
It can be seen that **divide and conquer is a subtly pervasive algorithmic idea**, embedded within various algorithms and data structures.
|
||||
It can be seen that **divide and conquer is a "subtly pervasive" algorithmic idea**, embedded in various algorithms and data structures.
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Tower of Hanoi Problem
|
||||
# Hanota problem
|
||||
|
||||
In both merge sort and binary tree construction, we break the original problem into two subproblems, each half the size of the original problem. However, for the Tower of Hanoi, we adopt a different decomposition strategy.
|
||||
In merge sort and building binary trees, we decompose the original problem into two subproblems, each half the size of the original problem. However, for the hanota problem, we adopt a different decomposition strategy.
|
||||
|
||||
!!! question
|
||||
|
||||
We are given three pillars, denoted as `A`, `B`, and `C`. Initially, pillar `A` has $n$ discs, arranged from top to bottom in ascending size. Our task is to move these $n$ discs to pillar `C`, maintaining their original order (as shown in the figure below). The following rules apply during the movement:
|
||||
|
||||
1. A disc can be removed only from the top of a pillar and must be placed on the top of another pillar.
|
||||
Given three pillars, denoted as `A`, `B`, and `C`. Initially, pillar `A` has $n$ discs stacked on it, arranged from top to bottom in ascending order of size. Our task is to move these $n$ discs to pillar `C` while maintaining their original order (as shown in the figure below). The following rules must be followed when moving the discs.
|
||||
|
||||
1. A disc can only be taken from the top of one pillar and placed on top of another pillar.
|
||||
2. Only one disc can be moved at a time.
|
||||
3. A smaller disc must always be on top of a larger disc.
|
||||
|
||||

|
||||

|
||||
|
||||
**We denote the Tower of Hanoi problem of size $i$ as $f(i)$**. For example, $f(3)$ represents moving $3$ discs from pillar `A` to pillar `C`.
|
||||
**We denote the hanota problem of size $i$ as $f(i)$**. For example, $f(3)$ represents moving $3$ discs from `A` to `C`.
|
||||
|
||||
### Consider the base cases
|
||||
### Considering the base cases
|
||||
|
||||
As shown in the figure below, for the problem $f(1)$—which has only one disc—we can directly move it from `A` to `C`.
|
||||
As shown in the figure below, for problem $f(1)$, when there is only one disc, we can move it directly from `A` to `C`.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
@@ -24,7 +24,7 @@ As shown in the figure below, for the problem $f(1)$—which has only one disc
|
||||
=== "<2>"
|
||||

|
||||
|
||||
For $f(2)$—which has two discs—**we rely on pillar `B` to help keep the smaller disc above the larger disc**, as illustrated in the following figure:
|
||||
As shown in the figure below, for problem $f(2)$, when there are two discs, **since we must always keep the smaller disc on top of the larger disc, we need to use `B` to assist in the move**.
|
||||
|
||||
1. First, move the smaller disc from `A` to `B`.
|
||||
2. Then move the larger disc from `A` to `C`.
|
||||
@@ -42,17 +42,17 @@ For $f(2)$—which has two discs—**we rely on pillar `B` to help keep the smal
|
||||
=== "<4>"
|
||||

|
||||
|
||||
The process of solving $f(2)$ can be summarized as: **moving two discs from `A` to `C` with the help of `B`**. Here, `C` is called the target pillar, and `B` is called the buffer pillar.
|
||||
The process of solving problem $f(2)$ can be summarized as: **moving two discs from `A` to `C` with the help of `B`**. Here, `C` is called the target pillar, and `B` is called the buffer pillar.
|
||||
|
||||
### Decomposition of subproblems
|
||||
### Subproblem decomposition
|
||||
|
||||
For the problem $f(3)$—that is, when there are three discs—the situation becomes slightly more complicated.
|
||||
For problem $f(3)$, when there are three discs, the situation becomes slightly more complex.
|
||||
|
||||
Since we already know the solutions to $f(1)$ and $f(2)$, we can adopt a divide-and-conquer perspective and **treat the top two discs on `A` as a single unit**, performing the steps shown in the figure below. This allows the three discs to be successfully moved from `A` to `C`.
|
||||
Since we already know the solutions to $f(1)$ and $f(2)$, we can think from a divide and conquer perspective, **treating the top two discs on `A` as a whole**, and execute the steps shown in the figure below. This successfully moves the three discs from `A` to `C`.
|
||||
|
||||
1. Let `B` be the target pillar and `C` the buffer pillar, then move the two discs from `A` to `B`.
|
||||
1. Let `B` be the target pillar and `C` be the buffer pillar, and move two discs from `A` to `B`.
|
||||
2. Move the remaining disc from `A` directly to `C`.
|
||||
3. Let `C` be the target pillar and `A` the buffer pillar, then move the two discs from `B` to `C`.
|
||||
3. Let `C` be the target pillar and `A` be the buffer pillar, and move two discs from `B` to `C`.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
@@ -66,32 +66,32 @@ Since we already know the solutions to $f(1)$ and $f(2)$, we can adopt a divide-
|
||||
=== "<4>"
|
||||

|
||||
|
||||
Essentially, **we decompose $f(3)$ into two $f(2)$ subproblems and one $f(1)$ subproblem**. By solving these three subproblems in sequence, the original problem is solved, indicating that the subproblems are independent and their solutions can be merged.
|
||||
Essentially, **we divide problem $f(3)$ into two subproblems $f(2)$ and one subproblem $f(1)$**. By solving these three subproblems in order, the original problem is solved. This shows that the subproblems are independent and their solutions can be merged.
|
||||
|
||||
From this, we can summarize the divide-and-conquer strategy for the Tower of Hanoi, illustrated in the figure below. We divide the original problem $f(n)$ into two subproblems $f(n-1)$ and one subproblem $f(1)$, and solve these three subproblems in the following order:
|
||||
From this, we can summarize the divide and conquer strategy for solving the hanota problem shown in the figure below: divide the original problem $f(n)$ into two subproblems $f(n-1)$ and one subproblem $f(1)$, and solve these three subproblems in the following order.
|
||||
|
||||
1. Move $n-1$ discs from `A` to `B`, using `C` as a buffer.
|
||||
2. Move the remaining disc directly from `A` to `C`.
|
||||
3. Move $n-1$ discs from `B` to `C`, using `A` as a buffer.
|
||||
1. Move $n-1$ discs from `A` to `B` with the help of `C`.
|
||||
2. Move the remaining $1$ disc directly from `A` to `C`.
|
||||
3. Move $n-1$ discs from `B` to `C` with the help of `A`.
|
||||
|
||||
For each $f(n-1)$ subproblem, **we can apply the same recursive partition** until we reach the smallest subproblem $f(1)$. Because $f(1)$ is already known to require just a single move, it is trivial to solve.
|
||||
For these two subproblems $f(n-1)$, **we can recursively divide them in the same way** until reaching the smallest subproblem $f(1)$. The solution to $f(1)$ is known and requires only one move operation.
|
||||
|
||||

|
||||

|
||||
|
||||
### Code implementation
|
||||
|
||||
In the code, we define a recursive function `dfs(i, src, buf, tar)` which moves the top $i$ discs from pillar `src` to pillar `tar`, using pillar `buf` as a buffer:
|
||||
In the code, we declare a recursive function `dfs(i, src, buf, tar)`, whose purpose is to move the top $i$ discs from pillar `src` to target pillar `tar` with the help of buffer pillar `buf`:
|
||||
|
||||
```src
|
||||
[file]{hanota}-[class]{}-[func]{solve_hanota}
|
||||
```
|
||||
|
||||
As shown in the figure below, the Tower of Hanoi problem can be visualized as a recursive tree of height $n$. Each node represents a subproblem, corresponding to a call to `dfs()`, **Hence, the time complexity is $O(2^n)$, and the space complexity is $O(n)$.**
|
||||
As shown in the figure below, the hanota problem forms a recursion tree of height $n$, where each node represents a subproblem corresponding to an invocation of the `dfs()` function, **therefore the time complexity is $O(2^n)$ and the space complexity is $O(n)$**.
|
||||
|
||||

|
||||

|
||||
|
||||
!!! quote
|
||||
|
||||
The Tower of Hanoi originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and $64$ differently sized golden discs. They believed that when the last disc was correctly placed, the world would end.
|
||||
The hanota problem originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and $64$ golden discs of different sizes. The monks continuously moved the discs, believing that when the last disc was correctly placed, the world would come to an end.
|
||||
|
||||
However, even if the monks moved one disc every second, it would take about $2^{64} \approx 1.84×10^{19}$ —approximately 585 billion years—far exceeding current estimates of the age of the universe. Thus, if the legend is true, we probably do not need to worry about the world ending.
|
||||
However, even if the monks moved one disc per second, it would take approximately $2^{64} \approx 1.84×10^{19}$ seconds, which is about $5850$ billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Divide and conquer
|
||||
|
||||

|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
Difficult problems are decomposed layer by layer, with each decomposition making them simpler.
|
||||
|
||||
Divide and conquer unveils a profound truth: begin with simplicity, and complexity dissolves.
|
||||
Divide and conquer reveals an important truth: start with simplicity, and nothing remains complex.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Summary
|
||||
|
||||
- Divide and conquer is a common algorithm design strategy that consists of two stages—divide (partition) and conquer (merge)—and is generally implemented using recursion.
|
||||
- To determine whether a problem is suited for a divide and conquer approach, we check if the problem can be decomposed, whether the subproblems are independent, and whether the subproblems can be merged.
|
||||
- Merge sort is a typical example of the divide and conquer strategy. It recursively splits an array into two equal-length subarrays until only one element remains, and then merges these subarrays layer by layer to complete the sorting.
|
||||
- Introducing the divide and conquer strategy often improves algorithm efficiency. On one hand, it reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division.
|
||||
- Divide and conquer can be applied to numerous algorithmic problems and is widely used in data structures and algorithm design, appearing in many scenarios.
|
||||
- Compared to brute force search, adaptive search is more efficient. Search algorithms with a time complexity of $O(\log n)$ are typically based on the divide and conquer strategy.
|
||||
- Binary search is another classic application of the divide-and-conquer strategy. It does not involve merging subproblem solutions and can be implemented via a recursive divide-and-conquer approach.
|
||||
- In the problem of constructing binary trees, building the tree (the original problem) can be divided into building the left subtree and right subtree (the subproblems). This can be achieved by partitioning the index ranges of the preorder and inorder traversals.
|
||||
- In the Tower of Hanoi problem, a problem of size $n$ can be broken down into two subproblems of size $n-1$ and one subproblem of size $1$. By solving these three subproblems in sequence, the original problem is resolved.
|
||||
- Divide and conquer is a common algorithm design strategy, consisting of two phases: divide (partition) and conquer (merge), typically implemented based on recursion.
|
||||
- The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged.
|
||||
- Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting.
|
||||
- Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, the divide and conquer strategy reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division.
|
||||
- Divide and conquer can both solve many algorithmic problems and is widely applied in data structure and algorithm design, appearing everywhere.
|
||||
- Compared to brute-force search, adaptive search is more efficient. Search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy.
|
||||
- Binary search is another typical application of divide and conquer. It does not include the step of merging solutions of subproblems. We can implement binary search through recursive divide and conquer.
|
||||
- In the problem of building a binary tree, building the tree (original problem) can be divided into building the left subtree and right subtree (subproblems), which can be achieved by dividing the index intervals of the preorder and inorder traversals.
|
||||
- In the hanota problem, a problem of size $n$ can be divided into two subproblems of size $n-1$ and one subproblem of size $1$. After solving these three subproblems in order, the original problem is solved.
|
||||
|
||||
Reference in New Issue
Block a user