mirror of
https://github.com/krahets/hello-algo.git
synced 2026-08-10 19:10:57 +00: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:
@@ -2,49 +2,51 @@
|
||||
|
||||
!!! question
|
||||
|
||||
Given $n$ items, the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with a capacity of $cap$. Each item can be chosen only once, **but a part of the item can be selected, with its value calculated based on the proportion of the weight chosen**, what is the maximum value of the items in the knapsack under the limited capacity? An example is shown in the figure below.
|
||||
Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can be selected only once, **but a portion of an item can be selected, with the value calculated based on the proportion of weight selected**, what is the maximum value of items in the knapsack under the limited capacity? An example is shown in the figure below.
|
||||
|
||||

|
||||

|
||||
|
||||
The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, involving the current item $i$ and capacity $c$, aiming to maximize the value within the limited capacity of the knapsack.
|
||||
The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, with states including the current item $i$ and capacity $c$, and the goal being to maximize value under the limited knapsack capacity.
|
||||
|
||||
The difference is that, in this problem, only a part of an item can be chosen. As shown in the figure below, **we can arbitrarily split the items and calculate the corresponding value based on the weight proportion**.
|
||||
The difference is that this problem allows selecting only a portion of an item. As shown in the figure below, **we can arbitrarily split items and calculate the corresponding value based on the weight proportion**.
|
||||
|
||||
1. For item $i$, its value per unit weight is $val[i-1] / wgt[i-1]$, referred to as the unit value.
|
||||
2. Suppose we put a part of item $i$ with weight $w$ into the knapsack, then the value added to the knapsack is $w \times val[i-1] / wgt[i-1]$.
|
||||
1. For item $i$, its value per unit weight is $val[i-1] / wgt[i-1]$, referred to as unit value.
|
||||
2. Suppose we put a portion of item $i$ with weight $w$ into the knapsack, then the value added to the knapsack is $w \times val[i-1] / wgt[i-1]$.
|
||||
|
||||

|
||||

|
||||
|
||||
### Greedy strategy determination
|
||||
|
||||
Maximizing the total value of the items in the knapsack **essentially means maximizing the value per unit weight**. From this, the greedy strategy shown in the figure below can be deduced.
|
||||
Maximizing the total value of items in the knapsack **is essentially maximizing the value per unit weight of items**. From this, we can derive the greedy strategy shown in the figure below.
|
||||
|
||||
1. Sort the items by their unit value from high to low.
|
||||
2. Iterate over all items, **greedily choosing the item with the highest unit value in each round**.
|
||||
3. If the remaining capacity of the knapsack is insufficient, use part of the current item to fill the knapsack.
|
||||
1. Sort items by unit value from high to low.
|
||||
2. Iterate through all items, **greedily selecting the item with the highest unit value in each round**.
|
||||
3. If the remaining knapsack capacity is insufficient, use a portion of the current item to fill the knapsack.
|
||||
|
||||

|
||||

|
||||
|
||||
### Code implementation
|
||||
|
||||
We have created an `Item` class in order to sort the items by their unit value. We loop and make greedy choices until the knapsack is full, then exit and return the solution:
|
||||
We created an `Item` class to facilitate sorting items by unit value. We loop to make greedy selections, breaking when the knapsack is full and returning the solution:
|
||||
|
||||
```src
|
||||
[file]{fractional_knapsack}-[class]{}-[func]{fractional_knapsack}
|
||||
```
|
||||
|
||||
Apart from sorting, in the worst case, the entire list of items needs to be traversed, **hence the time complexity is $O(n)$**, where $n$ is the number of items.
|
||||
The time complexity of built-in sorting algorithms is usually $O(\log n)$, and the space complexity is usually $O(\log n)$ or $O(n)$, depending on the specific implementation of the programming language.
|
||||
|
||||
Apart from sorting, in the worst case the entire item list needs to be traversed, **therefore the time complexity is $O(n)$**, where $n$ is the number of items.
|
||||
|
||||
Since an `Item` object list is initialized, **the space complexity is $O(n)$**.
|
||||
|
||||
### Correctness proof
|
||||
|
||||
Using proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm yields a maximum value `res`, but the solution does not include item $x$.
|
||||
Using proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm yields a maximum value of `res`, but this solution does not include item $x$.
|
||||
|
||||
Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item $x$. Since the unit value of item $x$ is the highest, the total value after replacement will definitely be greater than `res`. **This contradicts the assumption that `res` is the optimal solution, proving that the optimal solution must include item $x$**.
|
||||
Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item $x$. Since item $x$ has the highest unit value, the total value after replacement will definitely be greater than `res`. **This contradicts the assumption that `res` is the optimal solution, proving that the optimal solution must include item $x$**.
|
||||
|
||||
For other items in this solution, we can also construct the above contradiction. Overall, **items with greater unit value are always better choices**, proving that the greedy strategy is effective.
|
||||
For other items in this solution, we can also construct the above contradiction. In summary, **items with greater unit value are always better choices**, which proves that the greedy strategy is effective.
|
||||
|
||||
As shown in the figure below, if the item weight and unit value are viewed as the horizontal and vertical axes of a two-dimensional chart respectively, the fractional knapsack problem can be transformed into "seeking the largest area enclosed within a limited horizontal axis range". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective.
|
||||
As shown in the figure below, if we view item weight and item unit value as the horizontal and vertical axes of a two-dimensional chart respectively, then the fractional knapsack problem can be transformed into "finding the maximum area enclosed within a limited horizontal axis range". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective.
|
||||
|
||||

|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# Greedy algorithms
|
||||
# Greedy algorithm
|
||||
|
||||
<u>Greedy algorithm</u> is a common algorithm for solving optimization problems, which fundamentally involves making the seemingly best choice at each decision-making stage of the problem, i.e., greedily making locally optimal decisions in hopes of finding a globally optimal solution. Greedy algorithms are concise and efficient, and are widely used in many practical problems.
|
||||
<u>Greedy algorithm</u> is a common algorithm for solving optimization problems. Its basic idea is to make the seemingly best choice at each decision stage of the problem, that is, to greedily make locally optimal decisions in hopes of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely applied in many practical problems.
|
||||
|
||||
Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as relying on the property of optimal substructure, but they operate differently.
|
||||
Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as both relying on the optimal substructure property, but they work differently.
|
||||
|
||||
- Dynamic programming considers all previous decisions at the current decision stage and uses solutions to past subproblems to construct solutions for the current subproblem.
|
||||
- Greedy algorithms do not consider past decisions; instead, they proceed with greedy choices, continually narrowing the scope of the problem until it is solved.
|
||||
- Dynamic programming considers all previous decisions when making the current decision, and uses solutions to past subproblems to construct the solution to the current subproblem.
|
||||
- Greedy algorithms do not consider past decisions, but instead make greedy choices moving forward, continually reducing the problem size until the problem is solved.
|
||||
|
||||
Let's first understand the working principle of the greedy algorithm through the example of "coin change," which has been introduced in the "Complete Knapsack Problem" chapter. I believe you are already familiar with it.
|
||||
We will first understand how greedy algorithms work through the example problem "coin change". This problem has already been introduced in the "Complete Knapsack Problem" chapter, so I believe you are not unfamiliar with it.
|
||||
|
||||
!!! question
|
||||
|
||||
Given $n$ types of coins, where the denomination of the $i$th type of coin is $coins[i - 1]$, and the target amount is $amt$, with each type of coin available indefinitely, what is the minimum number of coins needed to make up the target amount? If it is not possible to make up the target amount, return $-1$.
|
||||
Given $n$ types of coins, where the denomination of the $i$-th type of coin is $coins[i - 1]$, and the target amount is $amt$, with each type of coin available for repeated selection, what is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return $-1$.
|
||||
|
||||
The greedy strategy adopted in this problem is shown in the figure below. Given the target amount, **we greedily choose the coin that is closest to and not greater than it**, repeatedly following this step until the target amount is met.
|
||||
The greedy strategy adopted for this problem is shown in the figure below. Given a target amount, **we greedily select the coin that is not greater than and closest to it**, and continuously repeat this step until the target amount is reached.
|
||||
|
||||

|
||||
|
||||
@@ -27,41 +27,41 @@ You might exclaim: So clean! The greedy algorithm solves the coin change problem
|
||||
|
||||
## Advantages and limitations of greedy algorithms
|
||||
|
||||
**Greedy algorithms are not only straightforward and simple to implement, but they are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy choice loops at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude smaller than the time complexity of the dynamic programming solution, which is $O(n \times amt)$.
|
||||
**Greedy algorithms are not only straightforward and simple to implement, but are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy choice loops at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude smaller than the time complexity of the dynamic programming solution $O(n \times amt)$.
|
||||
|
||||
However, **for some combinations of coin denominations, greedy algorithms cannot find the optimal solution**. The figure below provides two examples.
|
||||
However, **for certain coin denomination combinations, greedy algorithms cannot find the optimal solution**. The figure below provides two examples.
|
||||
|
||||
- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: In this coin combination, given any $amt$, the greedy algorithm can find the optimal solution.
|
||||
- **Negative example $coins = [1, 20, 50]$**: Suppose $amt = 60$, the greedy algorithm can only find the combination $50 + 1 \times 10$, totaling 11 coins, but dynamic programming can find the optimal solution of $20 + 20 + 20$, needing only 3 coins.
|
||||
- **Negative example $coins = [1, 49, 50]$**: Suppose $amt = 98$, the greedy algorithm can only find the combination $50 + 1 \times 48$, totaling 49 coins, but dynamic programming can find the optimal solution of $49 + 49$, needing only 2 coins.
|
||||
- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: With this coin combination, given any $amt$, the greedy algorithm can find the optimal solution.
|
||||
- **Negative example $coins = [1, 20, 50]$**: Suppose $amt = 60$, the greedy algorithm can only find the combination $50 + 1 \times 10$, totaling $11$ coins, but dynamic programming can find the optimal solution $20 + 20 + 20$, requiring only $3$ coins.
|
||||
- **Negative example $coins = [1, 49, 50]$**: Suppose $amt = 98$, the greedy algorithm can only find the combination $50 + 1 \times 48$, totaling $49$ coins, but dynamic programming can find the optimal solution $49 + 49$, requiring only $2$ coins.
|
||||
|
||||

|
||||

|
||||
|
||||
This means that for the coin change problem, greedy algorithms cannot guarantee finding the globally optimal solution, and they might find a very poor solution. They are better suited for dynamic programming.
|
||||
In other words, for the coin change problem, greedy algorithms cannot guarantee finding the global optimal solution, and may even find very poor solutions. It is better suited for solving with dynamic programming.
|
||||
|
||||
Generally, the suitability of greedy algorithms falls into two categories.
|
||||
Generally, the applicability of greedy algorithms falls into the following two situations.
|
||||
|
||||
1. **Guaranteed to find the optimal solution**: In these cases, greedy algorithms are often the best choice, as they tend to be more efficient than backtracking or dynamic programming.
|
||||
2. **Can find a near-optimal solution**: Greedy algorithms are also applicable here. For many complex problems, finding the global optimal solution is very challenging, and being able to find a high-efficiency suboptimal solution is also very commendable.
|
||||
1. **Can guarantee finding the optimal solution**: In this situation, greedy algorithms are often the best choice, because they tend to be more efficient than backtracking and dynamic programming.
|
||||
2. **Can find an approximate optimal solution**: Greedy algorithms are also applicable in this situation. For many complex problems, finding the global optimal solution is very difficult, and being able to find a suboptimal solution with high efficiency is also very good.
|
||||
|
||||
## Characteristics of greedy algorithms
|
||||
|
||||
So, what kind of problems are suitable for solving with greedy algorithms? Or rather, under what conditions can greedy algorithms guarantee to find the optimal solution?
|
||||
So the question arises: what kind of problems are suitable for solving with greedy algorithms? Or in other words, under what conditions can greedy algorithms guarantee finding the optimal solution?
|
||||
|
||||
Compared to dynamic programming, greedy algorithms have stricter usage conditions, focusing mainly on two properties of the problem.
|
||||
Compared to dynamic programming, the conditions for using greedy algorithms are stricter, mainly focusing on two properties of the problem.
|
||||
|
||||
- **Greedy choice property**: Only when the locally optimal choice can always lead to a globally optimal solution can greedy algorithms guarantee to obtain the optimal solution.
|
||||
- **Optimal substructure**: The optimal solution to the original problem contains the optimal solutions to its subproblems.
|
||||
- **Greedy choice property**: Only when locally optimal choices can always lead to a globally optimal solution can greedy algorithms guarantee obtaining the optimal solution.
|
||||
- **Optimal substructure**: The optimal solution to the original problem contains the optimal solutions to subproblems.
|
||||
|
||||
Optimal substructure has already been introduced in the "Dynamic Programming" chapter, so it is not discussed further here. It's important to note that some problems do not have an obvious optimal substructure, but can still be solved using greedy algorithms.
|
||||
Optimal substructure has already been introduced in the "Dynamic Programming" chapter, so we won't elaborate on it here. It's worth noting that the optimal substructure of some problems is not obvious, but they can still be solved using greedy algorithms.
|
||||
|
||||
We mainly explore the method for determining the greedy choice property. Although its description seems simple, **in practice, proving the greedy choice property for many problems is not easy**.
|
||||
We mainly explore methods for determining the greedy choice property. Although its description seems relatively simple, **in practice, for many problems, proving the greedy choice property is not easy**.
|
||||
|
||||
For example, in the coin change problem, although we can easily cite counterexamples to disprove the greedy choice property, proving it is much more challenging. If asked, **what conditions must a coin combination meet to be solvable using a greedy algorithm**? We often have to rely on intuition or examples to provide an ambiguous answer, as it is difficult to provide a rigorous mathematical proof.
|
||||
For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving it is quite difficult. If asked: **what conditions must a coin combination satisfy to be solvable using a greedy algorithm**? We often can only rely on intuition or examples to give an ambiguous answer, and find it difficult to provide a rigorous mathematical proof.
|
||||
|
||||
!!! quote
|
||||
|
||||
A paper presents an algorithm with a time complexity of $O(n^3)$ for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount.
|
||||
There is a paper that presents an algorithm with $O(n^3)$ time complexity for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount.
|
||||
|
||||
Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.
|
||||
|
||||
@@ -69,26 +69,26 @@ For example, in the coin change problem, although we can easily cite counterexam
|
||||
|
||||
The problem-solving process for greedy problems can generally be divided into the following three steps.
|
||||
|
||||
1. **Problem analysis**: Sort out and understand the characteristics of the problem, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming.
|
||||
2. **Determine the greedy strategy**: Determine how to make a greedy choice at each step. This strategy can reduce the scale of the problem at each step and eventually solve the entire problem.
|
||||
3. **Proof of correctness**: It is usually necessary to prove that the problem has both a greedy choice property and optimal substructure. This step may require mathematical proofs, such as induction or reductio ad absurdum.
|
||||
1. **Problem analysis**: Sort out and understand the problem characteristics, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming.
|
||||
2. **Determine the greedy strategy**: Determine how to make greedy choices at each step. This strategy should be able to reduce the problem size at each step, ultimately solving the entire problem.
|
||||
3. **Correctness proof**: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical proofs, such as mathematical induction or proof by contradiction.
|
||||
|
||||
Determining the greedy strategy is the core step in solving the problem, but it may not be easy to implement, mainly for the following reasons.
|
||||
|
||||
- **Greedy strategies vary greatly between different problems**. For many problems, the greedy strategy is fairly straightforward, and we can come up with it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which is a real test of individual problem-solving experience and algorithmic capability.
|
||||
- **Some greedy strategies are quite misleading**. When we confidently design a greedy strategy, write the code, and submit it for testing, it is quite possible that some test cases will not pass. This is because the designed greedy strategy is only "partially correct," as described above with the coin change example.
|
||||
- **Greedy strategies differ greatly between different problems**. For many problems, the greedy strategy is relatively straightforward, and we can derive it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which really tests one's problem-solving experience and algorithmic ability.
|
||||
- **Some greedy strategies are highly misleading**. When we confidently design a greedy strategy, write the solution code and submit it for testing, we may find that some test cases cannot pass. This is because the designed greedy strategy is only "partially correct", as exemplified by the coin change problem discussed above.
|
||||
|
||||
To ensure accuracy, we should provide rigorous mathematical proofs for the greedy strategy, **usually involving reductio ad absurdum or mathematical induction**.
|
||||
To ensure correctness, we should rigorously mathematically prove the greedy strategy, **usually using proof by contradiction or mathematical induction**.
|
||||
|
||||
However, proving correctness may not be an easy task. If we are at a loss, we usually choose to debug the code based on test cases, modifying and verifying the greedy strategy step by step.
|
||||
However, correctness proofs may also not be easy. If we have no clue, we usually choose to debug the code based on test cases, step by step modifying and verifying the greedy strategy.
|
||||
|
||||
## Typical problems solved by greedy algorithms
|
||||
|
||||
Greedy algorithms are often applied to optimization problems that satisfy the properties of greedy choice and optimal substructure. Below are some typical greedy algorithm problems.
|
||||
Greedy algorithms are often applied to optimization problems that satisfy greedy choice property and optimal substructure. Below are some typical greedy algorithm problems.
|
||||
|
||||
- **Coin change problem**: In some coin combinations, the greedy algorithm always provides the optimal solution.
|
||||
- **Interval scheduling problem**: Suppose you have several tasks, each of which takes place over a period of time. Your goal is to complete as many tasks as possible. If you always choose the task that ends the earliest, then the greedy algorithm can achieve the optimal solution.
|
||||
- **Fractional knapsack problem**: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), the greedy algorithm can achieve the optimal solution in some cases.
|
||||
- **Stock trading problem**: Given a set of historical stock prices, you can make multiple trades, but you cannot buy again until after you have sold if you already own stocks. The goal is to achieve the maximum profit.
|
||||
- **Huffman coding**: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree, it always merges the two nodes with the lowest frequency, resulting in a Huffman tree with the minimum weighted path length (coding length).
|
||||
- **Coin change problem**: With certain coin combinations, greedy algorithms can always obtain the optimal solution.
|
||||
- **Interval scheduling problem**: Suppose you have some tasks, each taking place during a period of time, and your goal is to complete as many tasks as possible. If you always choose the task that ends earliest, then the greedy algorithm can obtain the optimal solution.
|
||||
- **Fractional knapsack problem**: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), then the greedy algorithm can obtain the optimal solution in some cases.
|
||||
- **Stock trading problem**: Given a set of historical stock prices, you can make multiple trades, but if you already hold stocks, you cannot buy again before selling, and the goal is to obtain the maximum profit.
|
||||
- **Huffman coding**: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree and always merging the two nodes with the lowest frequency, the resulting Huffman tree has the minimum weighted path length (encoding length).
|
||||
- **Dijkstra's algorithm**: It is a greedy algorithm for solving the shortest path problem from a given source vertex to all other vertices.
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
!!! abstract
|
||||
|
||||
Sunflowers turn towards the sun, always seeking the greatest possible growth for themselves.
|
||||
Sunflowers turn toward the sun, constantly pursuing the maximum potential for their own growth.
|
||||
|
||||
Greedy strategy guides to the best answer step by step through rounds of simple choices.
|
||||
Through rounds of simple choices, greedy strategies gradually lead to the best answer.
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
# Maximum capacity problem
|
||||
# Max capacity problem
|
||||
|
||||
!!! question
|
||||
|
||||
Input an array $ht$, where each element represents the height of a vertical partition. Any two partitions in the array, along with the space between them, can form a container.
|
||||
|
||||
The capacity of the container is the product of the height and the width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions.
|
||||
|
||||
Please select two partitions in the array that maximize the container's capacity and return this maximum capacity. An example is shown in the figure below.
|
||||
|
||||

|
||||
The capacity of the container equals the product of height and width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions.
|
||||
|
||||
The container is formed by any two partitions, **therefore the state of this problem is represented by the indices of the two partitions, denoted as $[i, j]$**.
|
||||
Please select two partitions in the array such that the capacity of the formed container is maximized, and return the maximum capacity. An example is shown in the figure below.
|
||||
|
||||
According to the problem statement, the capacity equals the product of height and width, where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions. The formula for capacity $cap[i, j]$ is:
|
||||

|
||||
|
||||
The container is formed by any two partitions, **therefore the state of this problem is the indices of two partitions, denoted as $[i, j]$**.
|
||||
|
||||
According to the problem description, capacity equals height multiplied by width, where height is determined by the shorter partition, and width is the difference in array indices between the two partitions. Let the capacity be $cap[i, j]$, then the calculation formula is:
|
||||
|
||||
$$
|
||||
cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
|
||||
$$
|
||||
|
||||
Assuming the length of the array is $n$, the number of combinations of two partitions (total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. The most straightforward approach is to **enumerate all possible states**, resulting in a time complexity of $O(n^2)$.
|
||||
Let the array length be $n$, then the number of combinations of two partitions (total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. Most directly, **we can exhaustively enumerate all states** to find the maximum capacity, with time complexity $O(n^2)$.
|
||||
|
||||
### Determination of a greedy strategy
|
||||
### Greedy strategy determination
|
||||
|
||||
There is a more efficient solution to this problem. As shown in the figure below, we select a state $[i, j]$ where the indices $i < j$ and the height $ht[i] < ht[j]$, meaning $i$ is the shorter partition, and $j$ is the taller one.
|
||||
This problem has a more efficient solution. As shown in the figure below, select a state $[i, j]$ where index $i < j$ and height $ht[i] < ht[j]$, meaning $i$ is the short partition and $j$ is the long partition.
|
||||
|
||||

|
||||
|
||||
As shown in the figure below, **if we move the taller partition $j$ closer to the shorter partition $i$, the capacity will definitely decrease**.
|
||||
As shown in the figure below, **if we now move the long partition $j$ closer to the short partition $i$, the capacity will definitely decrease**.
|
||||
|
||||
This is because when moving the taller partition $j$, the width $j-i$ definitely decreases; and since the height is determined by the shorter partition, the height can only remain the same (if $i$ remains the shorter partition) or decrease (if the moved $j$ becomes the shorter partition).
|
||||
This is because after moving the long partition $j$, the width $j-i$ definitely decreases; and since height is determined by the short partition, the height can only remain unchanged ($i$ is still the short partition) or decrease (the moved $j$ becomes the short partition).
|
||||
|
||||

|
||||

|
||||
|
||||
Conversely, **we can only possibly increase the capacity by moving the shorter partition $i$ inward**. Although the width will definitely decrease, **the height may increase** (if the moved shorter partition $i$ becomes taller). For example, in the figure below, the area increases after moving the shorter partition.
|
||||
Conversely, **we can only possibly increase capacity by contracting the short partition $i$ inward**. Because although width will definitely decrease, **height may increase** (the moved short partition $i$ may become taller). For example, in the figure below, the area increases after moving the short partition.
|
||||
|
||||

|
||||

|
||||
|
||||
This leads us to the greedy strategy for this problem: initialize two pointers at the ends of the container, and in each round, move the pointer corresponding to the shorter partition inward until the two pointers meet.
|
||||
From this we can derive the greedy strategy for this problem: initialize two pointers at both ends of the container, and in each round contract the pointer corresponding to the short partition inward, until the two pointers meet.
|
||||
|
||||
The figure below illustrate the execution of the greedy strategy.
|
||||
The figure below shows the execution process of the greedy strategy.
|
||||
|
||||
1. Initially, the pointers $i$ and $j$ are positioned at the ends of the array.
|
||||
2. Calculate the current state's capacity $cap[i, j]$ and update the maximum capacity.
|
||||
3. Compare the heights of partitions $i$ and $j$, and move the shorter partition inward by one step.
|
||||
4. Repeat steps `2.` and `3.` until $i$ and $j$ meet.
|
||||
1. In the initial state, pointers $i$ and $j$ are at both ends of the array.
|
||||
2. Calculate the capacity of the current state $cap[i, j]$, and update the maximum capacity.
|
||||
3. Compare the heights of partition $i$ and partition $j$, and move the short partition inward by one position.
|
||||
4. Loop through steps `2.` and `3.` until $i$ and $j$ meet.
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -72,28 +72,28 @@ The figure below illustrate the execution of the greedy strategy.
|
||||
=== "<9>"
|
||||

|
||||
|
||||
### Implementation
|
||||
### Code implementation
|
||||
|
||||
The code loops at most $n$ times, **thus the time complexity is $O(n)$**.
|
||||
The code loops at most $n$ rounds, **therefore the time complexity is $O(n)$**.
|
||||
|
||||
The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus the space complexity is $O(1)$**.
|
||||
Variables $i$, $j$, and $res$ use a constant amount of extra space, **therefore the space complexity is $O(1)$**.
|
||||
|
||||
```src
|
||||
[file]{max_capacity}-[class]{}-[func]{max_capacity}
|
||||
```
|
||||
|
||||
### Proof of correctness
|
||||
### Correctness proof
|
||||
|
||||
The reason why the greedy method is faster than enumeration is that each round of greedy selection "skips" some states.
|
||||
The reason greedy is faster than exhaustive enumeration is that each round of greedy selection "skips" some states.
|
||||
|
||||
For example, under the state $cap[i, j]$ where $i$ is the shorter partition and $j$ is the taller partition, greedily moving the shorter partition $i$ inward by one step leads to the "skipped" states shown in the figure below. **This means that these states' capacities cannot be verified later**.
|
||||
For example, in state $cap[i, j]$ where $i$ is the short partition and $j$ is the long partition, if we greedily move the short partition $i$ inward by one position, the states shown in the figure below will be "skipped". **This means that the capacities of these states cannot be verified later**.
|
||||
|
||||
$$
|
||||
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
|
||||
$$
|
||||
|
||||

|
||||

|
||||
|
||||
It is observed that **these skipped states are actually all states where the taller partition $j$ is moved inward**. We have already proven that moving the taller partition inward will definitely decrease the capacity. Therefore, the skipped states cannot possibly be the optimal solution, **and skipping them does not lead to missing the optimal solution**.
|
||||
Observing carefully, **these skipped states are actually all the states obtained by moving the long partition $j$ inward**. We have already proven that moving the long partition inward will definitely decrease capacity. That is, the skipped states cannot possibly be the optimal solution, **skipping them will not cause us to miss the optimal solution**.
|
||||
|
||||
The analysis shows that the operation of moving the shorter partition is "safe", and the greedy strategy is effective.
|
||||
The above analysis shows that the operation of moving the short partition is "safe", and the greedy strategy is effective.
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# Maximum product cutting problem
|
||||
# Max product cutting problem
|
||||
|
||||
!!! question
|
||||
|
||||
Given a positive integer $n$, split it into at least two positive integers that sum up to $n$, and find the maximum product of these integers, as illustrated in the figure below.
|
||||
Given a positive integer $n$, split it into the sum of at least two positive integers, and find the maximum product of all integers after splitting, as shown in the figure below.
|
||||
|
||||

|
||||

|
||||
|
||||
Assume we split $n$ into $m$ integer factors, where the $i$-th factor is denoted as $n_i$, that is,
|
||||
Suppose we split $n$ into $m$ integer factors, where the $i$-th factor is denoted as $n_i$, that is
|
||||
|
||||
$$
|
||||
n = \sum_{i=1}^{m}n_i
|
||||
$$
|
||||
|
||||
The goal of this problem is to find the maximum product of all integer factors, namely,
|
||||
The goal of this problem is to find the maximum product of all integer factors, namely
|
||||
|
||||
$$
|
||||
\max(\prod_{i=1}^{m}n_i)
|
||||
$$
|
||||
|
||||
We need to consider: How large should the number of splits $m$ be, and what should each $n_i$ be?
|
||||
We need to think about: how large should the splitting count $m$ be, and what should each $n_i$ be?
|
||||
|
||||
### Greedy strategy determination
|
||||
|
||||
Experience suggests that the product of two integers is often greater than their sum. Suppose we split a factor of $2$ from $n$, then their product is $2(n-2)$. Compare this product with $n$:
|
||||
Based on experience, the product of two integers is often greater than their sum. Suppose we split out a factor of $2$ from $n$, then their product is $2(n-2)$. We compare this product with $n$:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -32,54 +32,54 @@ n & \geq 4
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
As shown in the figure below, when $n \geq 4$, splitting out a $2$ increases the product, **which indicates that integers greater than or equal to $4$ should be split**.
|
||||
As shown in the figure below, when $n \geq 4$, splitting out a $2$ will increase the product, **which indicates that integers greater than or equal to $4$ should all be split**.
|
||||
|
||||
**Greedy strategy one**: If the splitting scheme includes factors $\geq 4$, they should be further split. The final split should only include factors $1$, $2$, and $3$.
|
||||
**Greedy strategy one**: If the splitting scheme includes factors $\geq 4$, then they should continue to be split. The final splitting scheme should only contain factors $1$, $2$, and $3$.
|
||||
|
||||

|
||||

|
||||
|
||||
Next, consider which factor is optimal. Among the factors $1$, $2$, and $3$, clearly $1$ is the worst, as $1 \times (n-1) < n$ always holds, meaning splitting out $1$ actually decreases the product.
|
||||
Next, consider which factor is optimal. Among the three factors $1$, $2$, and $3$, clearly $1$ is the worst, because $1 \times (n-1) < n$ always holds, meaning splitting out $1$ will actually decrease the product.
|
||||
|
||||
As shown in the figure below, when $n = 6$, $3 \times 3 > 2 \times 2 \times 2$. **This means splitting out $3$ is better than splitting out $2$**.
|
||||
As shown in the figure below, when $n = 6$, we have $3 \times 3 > 2 \times 2 \times 2$. **This means that splitting out $3$ is better than splitting out $2$**.
|
||||
|
||||
**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s. Because three $2$s can always be replaced by two $3$s to obtain a higher product.
|
||||
**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s. Because three $2$s can always be replaced by two $3$s to obtain a larger product.
|
||||
|
||||

|
||||

|
||||
|
||||
From the above, the following greedy strategies can be derived.
|
||||
In summary, the following greedy strategies can be derived.
|
||||
|
||||
1. Input integer $n$, continually split out factor $3$ until the remainder is $0$, $1$, or $2$.
|
||||
2. When the remainder is $0$, it means $n$ is a multiple of $3$, so no further action is taken.
|
||||
3. When the remainder is $2$, do not continue to split, keep it.
|
||||
1. Input integer $n$, continuously split out factor $3$ until the remainder is $0$, $1$, or $2$.
|
||||
2. When the remainder is $0$, it means $n$ is a multiple of $3$, so no further action is needed.
|
||||
3. When the remainder is $2$, do not continue splitting, keep it.
|
||||
4. When the remainder is $1$, since $2 \times 2 > 1 \times 3$, the last $3$ should be replaced with $2$.
|
||||
|
||||
### Code implementation
|
||||
|
||||
As shown in the figure below, we do not need to use loops to split the integer but can use the floor division operation to get the number of $3$s, $a$, and the modulo operation to get the remainder, $b$, thus:
|
||||
As shown in the figure below, we don't need to use loops to split the integer, but can use integer division to get the count of $3$s as $a$, and modulo operation to get the remainder as $b$, at which point we have:
|
||||
|
||||
$$
|
||||
n = 3a + b
|
||||
n = 3 a + b
|
||||
$$
|
||||
|
||||
Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, with a product of $1 \times (n - 1)$.
|
||||
Please note that for the edge case of $n \leq 3$, a $1$ must be split out, with product $1 \times (n - 1)$.
|
||||
|
||||
```src
|
||||
[file]{max_product_cutting}-[class]{}-[func]{max_product_cutting}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
**Time complexity depends on the implementation of the power operation in the programming language**. For Python, the commonly used power calculation functions are three types:
|
||||
**The time complexity depends on the implementation of the exponentiation operation in the programming language**. Taking Python as an example, there are three commonly used power calculation functions.
|
||||
|
||||
- Both the operator `**` and the function `pow()` have a time complexity of $O(\log a)$.
|
||||
- The `math.pow()` function internally calls the C language library's `pow()` function, performing floating-point exponentiation, with a time complexity of $O(1)$.
|
||||
- Both the operator `**` and the function `pow()` have time complexity $O(\log a)$.
|
||||
- The function `math.pow()` internally calls the C library's `pow()` function, which performs floating-point exponentiation, with time complexity $O(1)$.
|
||||
|
||||
Variables $a$ and $b$ use constant size of extra space, **hence the space complexity is $O(1)$**.
|
||||
Variables $a$ and $b$ use a constant amount of extra space, **therefore the space complexity is $O(1)$**.
|
||||
|
||||
### Correctness proof
|
||||
|
||||
Using the proof by contradiction, only analyze cases where $n \geq 3$.
|
||||
Using proof by contradiction, only analyzing the case where $n \geq 4$.
|
||||
|
||||
1. **All factors $\leq 3$**: Assume the optimal splitting scheme includes a factor $x \geq 4$, then it can definitely be further split into $2(x-2)$, obtaining a larger product. This contradicts the assumption.
|
||||
2. **The splitting scheme does not contain $1$**: Assume the optimal splitting scheme includes a factor of $1$, then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption.
|
||||
3. **The splitting scheme contains at most two $2$s**: Assume the optimal splitting scheme includes three $2$s, then they can definitely be replaced by two $3$s, achieving a higher product. This contradicts the assumption.
|
||||
1. **All factors $\leq 3$**: Suppose the optimal splitting scheme includes a factor $x \geq 4$, then it can definitely continue to be split into $2(x-2)$ to obtain a larger (or equal) product. This contradicts the assumption.
|
||||
2. **The splitting scheme does not contain $1$**: Suppose the optimal splitting scheme includes a factor of $1$, then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption.
|
||||
3. **The splitting scheme contains at most two $2$s**: Suppose the optimal splitting scheme includes three $2$s, then they can definitely be replaced by two $3$s for a larger product. This contradicts the assumption.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Summary
|
||||
|
||||
- Greedy algorithms are often used to solve optimization problems, where the principle is to make locally optimal decisions at each decision stage in order to achieve a globally optimal solution.
|
||||
- Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller sub-problem with each round, until the problem is resolved.
|
||||
- Greedy algorithms are not only simple to implement but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms generally have a lower time complexity.
|
||||
- In the problem of coin change, greedy algorithms can guarantee the optimal solution for certain combinations of coins; for others, however, the greedy algorithm might find a very poor solution.
|
||||
- Problems suitable for greedy algorithm solutions possess two main properties: greedy-choice property and optimal substructure. The greedy-choice property represents the effectiveness of the greedy strategy.
|
||||
- For some complex problems, proving the greedy-choice property is not straightforward. Contrarily, proving the invalidity is often easier, such as with the coin change problem.
|
||||
- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and proving correctness. Among these, determining the greedy strategy is the key step, while proving correctness often poses the challenge.
|
||||
- The fractional knapsack problem builds on the 0-1 knapsack problem by allowing the selection of a part of the items, hence it can be solved using a greedy algorithm. The correctness of the greedy strategy can be proved by contradiction.
|
||||
- The maximum capacity problem can be solved using the exhaustive method, with a time complexity of $O(n^2)$. By designing a greedy strategy, each round moves inwardly shortening the board, optimizing the time complexity to $O(n)$.
|
||||
- In the problem of maximum product after cutting, we deduce two greedy strategies: integers $\geq 4$ should continue to be cut, with the optimal cutting factor being $3$. The code includes power operations, and the time complexity depends on the method of implementing power operations, generally being $O(1)$ or $O(\log n)$.
|
||||
- Greedy algorithms are typically used to solve optimization problems. The principle is to make locally optimal decisions at each decision stage in hopes of obtaining a globally optimal solution.
|
||||
- Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller subproblem in each round, until the problem is solved.
|
||||
- Greedy algorithms are not only simple to implement, but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms typically have lower time complexity.
|
||||
- In the coin change problem, for certain coin combinations, greedy algorithms can guarantee finding the optimal solution; for other coin combinations, however, greedy algorithms may find very poor solutions.
|
||||
- Problems suitable for solving with greedy algorithms have two major properties: greedy choice property and optimal substructure. The greedy choice property represents the effectiveness of the greedy strategy.
|
||||
- For some complex problems, proving the greedy choice property is not simple. Relatively speaking, disproving it is easier, such as in the coin change problem.
|
||||
- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the difficult point.
|
||||
- The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting a portion of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction.
|
||||
- The max capacity problem can be solved using exhaustive enumeration with time complexity $O(n^2)$. By designing a greedy strategy to move the short partition inward in each round, the time complexity can be optimized to $O(n)$.
|
||||
- In the max product cutting problem, we successively derive two greedy strategies: integers $\geq 4$ should all continue to be split, and the optimal splitting factor is $3$. The code includes exponentiation operations, and the time complexity depends on the implementation method of exponentiation, typically being $O(1)$ or $O(\log n)$.
|
||||
|
||||
Reference in New Issue
Block a user