Merge branch 'english' into english

This commit is contained in:
floatLig
2020-03-08 15:37:31 +08:00
committed by GitHub
11 changed files with 614 additions and 568 deletions

View File

@ -4,7 +4,7 @@ These articles are somehow kinds of **Algorithmic Thinking**. All based on LeetC
I don't like one liner and confusing, I like clear and easy-understanding.
Gitbook has deployed, will sync with this branch of the repo: https://labuladong.gitbook.io/algo-en/
**Gitbook** has deployed, will sync with this branch of the repo: https://labuladong.gitbook.io/algo-en/
If you want to clone this repo, please use following command:
@ -26,14 +26,14 @@ This command specifies the `english` branch and limit the depth of clone, get ri
* [How to Check the Validation of Parenthesis](interview/valid-parentheses.md)
* [How to Find Missing Element](interview/missing_elements.md)
* [How to Pick Elements From a Arbitrary Sequence](interview/ReservoirSampling.md)
* [How to use Binary Search](interview/UsingBinarySearchAlgorithm.md)
* [How to Scheduling Seats](interview/Seatscheduling.md)
* [Union-Find Algorithm in Detail](think_like_computer/Union-find-Explanation.md)
* [Union-Find Application](think_like_computer/Union-Find-Application.md)
* [Find Sebesquence With Binary Search](interview/findSebesquenceWithBinarySearch.md)
* [如何运用二分查找算法](interview/koko偷香蕉.md)
* [Problems can be sloved by one line](interview/one-line-code-puzzles.md)
* [如何寻找缺失和重复的元素](interview/缺失和重复的元素.md)
* [如何判断回文链表](interview/判断回文链表.md)
* [一行代码就能解决的算法题](interview/一行代码解决的智力题.md)
* II. Data Structure
* [Binary Head and Priority Queue](data_structure/binary_heap_implements_priority_queues.md)
@ -59,8 +59,8 @@ This command specifies the `english` branch and limit the depth of clone, get ri
* [Interval Scheduling: Interval Merging](think_like_computer/IntervalMerging.md)
* [Interval Scheduling: Intersections of Intervals](think_like_computer/IntervalIntersection.md)
* [String Multiplication](think_like_computer/string_multiplication.md)
* [Pancake Soring Algorithm](think_like_computer/PancakesSorting.md)
* [Sliding Window Technique](think_like_computer/滑动窗口技巧.md)
* [烧饼排序](think_like_computer/烧饼排序.md)
* [常用的位操作](think_like_computer/常用的位操作.md)
* [信封嵌套问题](think_like_computer/信封嵌套问题.md)
* [回溯算法团灭排列、组合、子集问题](interview/子集排列组合.md)
@ -77,13 +77,13 @@ This command specifies the `english` branch and limit the depth of clone, get ri
* [Regular Expression](dynamic_programming/RegularExpression.md)
* [The Strategies of Subsequence Problem](dynamic_programming/StrategiesForSubsequenceProblem.md)
* [Greedy: Interval Scheduling](dynamic_programming/IntervalScheduling.md)
* [4 Keys Keyboard](dynamic_programming/FourKeysKeyboard.md)
* [动态规划详解](dynamic_programming/动态规划详解进阶.md)
* [动态规划答疑篇](dynamic_programming/最优子结构.md)
* [动态规划设计:最长递增子序列](dynamic_programming/动态规划设计:最长递增子序列.md)
* [动态规划之KMP字符匹配算法](dynamic_programming/动态规划之KMP字符匹配算法.md)
* [团灭 LeetCode 股票买卖问题](dynamic_programming/团灭股票问题.md)
* [团灭 LeetCode 打家劫舍问题](dynamic_programming/抢房子.md)
* [动态规划之四键键盘](dynamic_programming/动态规划之四键键盘.md)
* V. Common Knowledge
* [Difference Between Process and Thread in Linux](common_knowledge/linuxProcess.md)

View File

@ -13,14 +13,14 @@
* [How to Check the Validation of Parenthesis](interview/valid-parentheses.md)
* [How to Find Missing Element](interview/missing_elements.md)
* [How to Pick Elements From a Arbitrary Sequence](interview/ReservoirSampling.md)
* [How to use Binary Search](interview/UsingBinarySearchAlgorithm.md)
* [How to Scheduling Seats](interview/Seatscheduling.md)
* [Union-Find Algorithm in Detail](think_like_computer/Union-find-Explanation.md)
* [Union-Find Application](think_like_computer/Union-Find-Application.md)
* [Find Sebesquence With Binary Search](interview/findSebesquenceWithBinarySearch.md)
* [如何运用二分查找算法](interview/koko偷香蕉.md)
* [Problems can be sloved by one line](interview/one-line-code-puzzles.md)
* [如何寻找缺失和重复的元素](interview/缺失和重复的元素.md)
* [如何判断回文链表](interview/判断回文链表.md)
* [一行代码就能解决的算法题](interview/一行代码解决的智力题.md)
* II. Data Structure
* [Binary Head and Priority Queue](data_structure/binary_heap_implements_priority_queues.md)
@ -46,7 +46,7 @@
* [Interval Scheduling: Interval Merging](think_like_computer/IntervalMerging.md)
* [Interval Scheduling: Intersections of Intervals](think_like_computer/IntervalIntersection.md)
* [String Multiplication](think_like_computer/string_multiplication.md)
* [烧饼排序](think_like_computer/烧饼排序.md)
* [Pancake Soring Algorithm](think_like_computer/PancakesSorting.md)
* [滑动窗口技巧](think_like_computer/滑动窗口技巧.md)
* [常用的位操作](think_like_computer/常用的位操作.md)
* [信封嵌套问题](think_like_computer/信封嵌套问题.md)
@ -64,13 +64,13 @@
* [Regular Expression](dynamic_programming/RegularExpression.md)
* [The Strategies of Subsequence Problem](dynamic_programming/StrategiesForSubsequenceProblem.md)
* [Greedy: Interval Scheduling](dynamic_programming/IntervalScheduling.md)
* [4 Keys Keyboard](dynamic_programming/FourKeysKeyboard.md)
* [动态规划详解](dynamic_programming/动态规划详解进阶.md)
* [动态规划答疑篇](dynamic_programming/最优子结构.md)
* [动态规划设计:最长递增子序列](dynamic_programming/动态规划设计:最长递增子序列.md)
* [动态规划之KMP字符匹配算法](dynamic_programming/动态规划之KMP字符匹配算法.md)
* [团灭 LeetCode 股票买卖问题](dynamic_programming/团灭股票问题.md)
* [团灭 LeetCode 打家劫舍问题](dynamic_programming/抢房子.md)
* [动态规划之四键键盘](dynamic_programming/动态规划之四键键盘.md)
* V. Common Knowledge
* [Difference Between Process and Thread in Linux](common_knowledge/linuxProcess.md)

View File

@ -0,0 +1,184 @@
# 4 Keys Keyboard
**Translator: [upbin](https://github.com/upbin)**
**Author: [labuladong](https://github.com/labuladong)**
The problem of *4 keys keyboard* is very interesting and can broaden one's horizon. This problem can make you obviously feel that different definitions of dp arrays need completely different logic to think about, and this logic can produce completely different solutions.
We can't wait to solve this problem:
![](../pictures/4keyboard/title.png)
After reading the question, think about how to get the maximum number of characters 'A' after typing `N` times on the keyboard? We are more familiar with trying questions using enumeration. Whenever we want to press the keyboard (and can press it), there are `4 buttons` for us to choose from, we can enumerate every possible operation It is obvious that this is a dynamic programming problem.
### Discuss the first method
This kind of problem-solving idea is easy to understand, but the efficiency is not high. We follow the routine directly: **for dynamic programming problems, we must first understand which are [ states ] and which are [ choices ].**
Specific to this problem, what **choices** are obvious for each keystroke: four types are the **4** keys mentioned in the title, which are `A`, `Ctrl-A`, `Ctrl-C`, and `Ctrl-V`.
Next, let's think about what are the **states** of this problem? **In other words, what information do we need to know to break down the original problem into smaller subproblems?**
Now you think about it, Is it correct for me to define the status of this problem as follows?
- The first state is the remaining number of times the key can be pressed, we use `n` to represent it.
- The second state is the number of characters 'A' on the current screen, we use `a_num`.
- The third state is the number of characters 'A' still in the clipboard, represented by `copy`.
By defining **state** in this way, we can know the *base case*: when the number of remaining `n` is `0`, `a_num` is the answer we want.
Combining the four **choices** just mentioned, we can express these kinds of choices through state transitions:
```python
dp(n - 1, a_num + 1, copy) # [ A ]
# comment: Press the 'A' key to add a character to the screen.
# Subtract 1 at the same time the number of times you are allowed to press the keyboard.
dp(n - 1, a_num + copy, copy) # [Ctrl-V]
# comment: Press C-V to paste, the characters in the clipboard are added to the screen.
# Subtract 1 at the same time the number of times you are allowed to press the keyboard.
dp(n - 2, a_num, a_num) # [Ctrl-A] & [Ctrl-C]
# comment: Ctrl + A and Ctrl + C can obviously be used together.
# The number of 'A' in the clipboard becomes the number of 'A' on the screen.
# Subtract 2 at the same time the number of times you are allowed to press the keyboard.
```
By describing this, we can see that the scale of the problem `n` is constantly decreasing, and finally we can reach the *base case* of `n == 0`. So this idea is correct: (Do you think so?)
```python
def maxA(N: int) -> int:
# It can be verified that for the initial (n, a_num, copy) state,
# there can be at most dp (n, a_num, copy) 'A' on the screen.
def dp(n, a_num, copy):
# base case
if n <= 0: return a_num;
# Let s try all three options and choose the largest one.
return max(
dp(n - 1, a_num + 1, copy), # [ A ]
dp(n - 1, a_num + copy, copy), # [Ctrl-V]
dp(n - 2, a_num, a_num) # [Ctrl-A] & [Ctrl-C]
)
# You can press the key n times, then there is no 'A' in the screen
# and the clipboard.
return dp(N, 0, 0)
```
This solution should be well understood because it is semantically explicit.
Below we continue to follow the routine and use <u>memorized search</u> to eliminate those overlapping sub-problems:
```python
def maxA(N: int) -> int:
# memorandum
memo = dict()
def dp(n, a_num, copy):
if n <= 0: return a_num;
# Avoid overlapping subproblems being recalculated
if (n, a_num, copy) in memo:
return memo[(n, a_num, copy)]
memo[(n, a_num, copy)] = max(
# These options are still the same
)
return memo[(n, a_num, copy)]
return dp(N, 0, 0)
```
After we optimized our code in this way, although the sub-problem was repeatedly solved, the number of searches was still very large (*if we submit to LeetCode it will definitely time out*).
Now let's try to analyze the time complexity of the algorithm just now. The challenge is that this analysis is not easy. No matter what it is, now we write this *dp function* as a *dp array*:
```c++
dp[n][a_num][copy]
// The total number of states (spatial complexity) of this problem
// is the volume of this three-dimensional array.
```
We know that the maximum value of the variable `n` is `N`, but it is difficult to calculate the maximum number of `a_num` and `copy`. The lowest complexity is *O(N^3)*. So the algorithm just now is not good, the complexity is too high, and it can no longer be optimized.
The more embarrassing thing is that this also shows that I used to define **state** as it is not very good. Let's change the idea of defining this dp problem.
### Exploration of the second approach
Next, our thinking is a little more complicated, but it is very efficient.
Continue to follow our routine, **choice** has been defined before, or the `4`. But this time we only need to define **a state**, which is the remaining number of available keyboard presses `n`.
This algorithm is based on the following fact. There must be only two cases of the key sequence corresponding to the optimal answer:
- Either keep pressing A: `A`, ` A`, ... , `A` (more when `N` is smaller).
- Either this is the form: `A`, `A`, ..., `Ctrl-A`, `Ctrl-C`, `Ctrl-V`, `Ctrl-V`, ..., `Ctrl-V` (mostly when `N` is larger). *(Here you can find some mathematical rules, you can study if you are interested)*
Because when the number of characters to be printed is relatively small (`N` is small), "`Ctrl-A`, `Ctrl-C`, `Ctrl-V`" consumes a relatively high number of operations, so we might as well keep pressing `A`. When `N` is relatively large, the gain of `Ctrl-V` in the later period is definitely relatively large. In this case, the entire operation sequence is roughly like this: at the beginning, press several 'A's, then `Ctrl-A`, `Ctrl-C`, and finally several `Ctrl-V`, and then continue `Ctrl-A -> Ctrl-C -> Ctrl-V` Such a loop operation.
In other words, the last keystroke was either `A` or `Ctrl-V`. As long as we are clear on this, we can design the algorithm through these **two situations**:
```java
int[] dp = new int[N + 1];
// Definition: dp[i] indicates the maximum number of 'A' that can be displayed after the // first operation.
for (int i = 0; i <= N; i++)
dp[i] = max(
// Press [ A ] this time,
         // This time press [Ctrl-V].
)
```
Think about it. For the case of [pressing the `A` key], it is actually a new 'A' printed on the screen of **state i-1**, so it is easy to get the result:
```java
// If we press the [ A ] key, it's just one more 'A' than the last time.
dp[i] = dp[i - 1] + 1;
```
However, if we want to press `Ctrl-V`, we also need to consider where we did `Ctrl-A` and `Ctrl-C`.
Earlier we said that the optimal sequence of operations must be `Ctrl-A`, `Ctrl-C` followed by several `Ctrl-V`, so we use a variable `j` as the starting point for these `Ctrl-V` operations. Then the two operations before `j` should be `Ctrl-A` and `Ctrl-C`:
```java
public int maxA(int N) {
int[] dp = new int[N + 1];
dp[0] = 0;
for (int i = 1; i <= N; i++) {
// press [ A ]
dp[i] = dp[i - 1] + 1;
for (int j = 2; j < i; j++) {
// [Ctrl-A] & [Ctrl-C] -> dp[j-2], Paste i-j times
// There are { dp[j-2] * (i-j+1) }number of 'A' on the screen
dp[i] = Math.max(dp[i], dp[j - 2] * (i - j + 1));
}
}
// What is the maximum number of 'A' after N keystrokes?
return dp[N];
}
```
The `j` variable `minus 2` is used to save the number of operations available to `Ctrl-A`, `Ctrl-C`. See the description picture to understand:
![](../pictures/4keyboard/1.jpg)
We have just completed this algorithm. The time complexity of the algorithm is *O(N^2)* and the space complexity is *O(N)*, so this solution seems to be very efficient.
### Review our algorithmic ideas
Dynamic programming is difficult to find the state transition. The different definitions we set will produce different state transition logic. Although we can all get the correct results in the end, the efficiency of the program may have amazing differences.
Let's review the method we tried for the first time. Although the overlapping sub-problem has been eliminated, the efficiency of the program is still low, but where is the low? Let's abstract the recursive framework to find out:
```python
def dp(n, a_num, copy):
dp(n - 1, a_num + 1, copy), # [ A ]
dp(n - 1, a_num + copy, copy), # [Ctrl-V]
dp(n - 2, a_num, a_num) # [Ctrl-A] & [Ctrl-C]
```
Let's analyze the logic of this exhaustive scheme. Obviously, it is possible to have such a sequence of operations `Ctrl-A`, `Ctrl+C`, `Ctrl-A`, `Ctrl-C`, ... , or `Ctrl-V`, `Ctrl-V`, ... . However, the result of the operation sequence produced by this method is not optimal, even if we have not figured out a way to circumvent these situations, thereby adding a lot of calculations of unnecessary sub-problems.
After we review the second solution, we only need to think a little bit before we can think that the operation sequence of the optimal answer should be this form: `A`, `A`, ..., `Ctrl-A`, `Ctrl-C`, `Ctrl-V`, `Ctrl-V`, ..., `Ctrl-V`.
Based on the findings we found, we redefined <u>state</u> and re-searched for <u>state transition</u>, which logically reduced the number of invalid sub-problems, and ultimately optimized the program's operating efficiency.

View File

@ -1,173 +0,0 @@
# 动态规划之四键键盘
四键键盘问题很有意思,而且可以明显感受到:对 dp 数组的不同定义需要完全不同的逻辑,从而产生完全不同的解法。
首先看一下题目:
![](../pictures/4keyboard/title.png)
如何在 N 次敲击按钮后得到最多的 A我们穷举呗每次有对于每次按键我们可以穷举四种可能很明显就是一个动态规划问题。
### 第一种思路
这种思路会很容易理解,但是效率并不高,我们直接走流程:**对于动态规划问题,首先要明白有哪些「状态」,有哪些「选择」**。
具体到这个问题对于每次敲击按键有哪些「选择」是很明显的4 种,就是题目中提到的四个按键,分别是 `A``C-A``C-C``C-V``Ctrl` 简写为 `C`)。
接下来,思考一下对于这个问题有哪些「状态」?**或者换句话说,我们需要知道什么信息,才能将原问题分解为规模更小的子问题**
你看我这样定义三个状态行不行:第一个状态是剩余的按键次数,用 `n` 表示;第二个状态是当前屏幕上字符 A 的数量,用 `a_num` 表示;第三个状态是剪切板中字符 A 的数量,用 `copy` 表示。
如此定义「状态」,就可以知道 base case当剩余次数 `n` 为 0 时,`a_num` 就是我们想要的答案。
结合刚才说的 4 种「选择」,我们可以把这几种选择通过状态转移表示出来:
```python
dp(n - 1, a_num + 1, copy), # A
解释按下 A 屏幕上加一个字符
同时消耗 1 个操作数
dp(n - 1, a_num + copy, copy), # C-V
解释按下 C-V 粘贴剪切板中的字符加入屏幕
同时消耗 1 个操作数
dp(n - 2, a_num, a_num) # C-A C-C
解释全选和复制必然是联合使用的
剪切板中 A 的数量变为屏幕上 A 的数量
同时消耗 2 个操作数
```
这样可以看到问题的规模 `n` 在不断减小,肯定可以到达 `n = 0` 的 base case所以这个思路是正确的
```python
def maxA(N: int) -> int:
# 对于 (n, a_num, copy) 这个状态,
# 屏幕上能最终最多能有 dp(n, a_num, copy) 个 A
def dp(n, a_num, copy):
# base case
if n <= 0: return a_num;
# 几种选择全试一遍,选择最大的结果
return max(
dp(n - 1, a_num + 1, copy), # A
dp(n - 1, a_num + copy, copy), # C-V
dp(n - 2, a_num, a_num) # C-A C-C
)
# 可以按 N 次按键,屏幕和剪切板里都还没有 A
return dp(N, 0, 0)
```
这个解法应该很好理解,因为语义明确。下面就继续走流程,用备忘录消除一下重叠子问题:
```python
def maxA(N: int) -> int:
# 备忘录
memo = dict()
def dp(n, a_num, copy):
if n <= 0: return a_num;
# 避免计算重叠子问题
if (n, a_num, copy) in memo:
return memo[(n, a_num, copy)]
memo[(n, a_num, copy)] = max(
# 几种选择还是一样的
)
return memo[(n, a_num, copy)]
return dp(N, 0, 0)
```
这样优化代码之后,子问题虽然没有重复了,但数目仍然很多,在 LeetCode 提交会超时的。
我们尝试分析一下这个算法的时间复杂度,就会发现不容易分析。我们可以把这个 dp 函数写成 dp 数组:
```python
dp[n][a_num][copy]
# 状态的总数(时空复杂度)就是这个三维数组的体积
```
我们知道变量 `n` 最多为 `N`,但是 `a_num``copy` 最多为多少我们很难计算,复杂度起码也有 O(N^3) 把。所以这个算法并不好,复杂度太高,且已经无法优化了。
这也就说明,我们这样定义「状态」是不太优秀的,下面我们换一种定义 dp 的思路。
### 第二种思路
这种思路稍微有点复杂,但是效率高。继续走流程,「选择」还是那 4 个,但是这次我们只定义一个「状态」,也就是剩余的敲击次数 `n`
这个算法基于这样一个事实,**最优按键序列一定只有两种情况**
要么一直按 `A`A,A,...A当 N 比较小时)。
要么是这么一个形式A,A,...C-A,C-C,C-V,C-V,...C-V当 N 比较大时)。
因为字符数量少N 比较小)时,`C-A C-C C-V` 这一套操作的代价相对比较高,可能不如一个个按 `A`;而当 N 比较大时,后期 `C-V` 的收获肯定很大。这种情况下整个操作序列大致是:**开头连按几个 `A`,然后 `C-A C-C` 组合再接若干 `C-V`,然后再 `C-A C-C` 接着若干 `C-V`,循环下去**。
换句话说,最后一次按键要么是 `A` 要么是 `C-V`。明确了这一点,可以通过这两种情况来设计算法:
```java
int[] dp = new int[N + 1];
// 定义dp[i] 表示 i 次操作后最多能显示多少个 A
for (int i = 0; i <= N; i++)
dp[i] = max(
这次按 A
这次按 C-V
)
```
对于「按 `A` 键」这种情况,就是状态 `i - 1` 的屏幕上新增了一个 A 而已,很容易得到结果:
```java
// 按 A 键,就比上次多一个 A 而已
dp[i] = dp[i - 1] + 1;
```
但是,如果要按 `C-V`,还要考虑之前是在哪里 `C-A C-C` 的。
**刚才说了,最优的操作序列一定是 `C-A C-C` 接着若干 `C-V`,所以我们用一个变量 `j` 作为若干 `C-V` 的起点**。那么 `j` 之前的 2 个操作就应该是 `C-A C-C` 了:
```java
public int maxA(int N) {
int[] dp = new int[N + 1];
dp[0] = 0;
for (int i = 1; i <= N; i++) {
// 按 A 键
dp[i] = dp[i - 1] + 1;
for (int j = 2; j < i; j++) {
// 全选 & 复制 dp[j-2],连续粘贴 i - j 次
// 屏幕上共 dp[j - 2] * (i - j + 1) 个 A
dp[i] = Math.max(dp[i], dp[j - 2] * (i - j + 1));
}
}
// N 次按键之后最多有几个 A
return dp[N];
}
```
其中 `j` 变量减 2 是给 `C-A C-C` 留下操作数,看个图就明白了:
![](../pictures/4keyboard/1.jpg)
这样,此算法就完成了,时间复杂度 O(N^2),空间复杂度 O(N),这种解法应该是比较高效的了。
### 最后总结
动态规划难就难在寻找状态转移,不同的定义可以产生不同的状态转移逻辑,虽然最后都能得到正确的结果,但是效率可能有巨大的差异。
回顾第一种解法,重叠子问题已经消除了,但是效率还是低,到底低在哪里呢?抽象出递归框架:
```python
def dp(n, a_num, copy):
dp(n - 1, a_num + 1, copy), # A
dp(n - 1, a_num + copy, copy), # C-V
dp(n - 2, a_num, a_num) # C-A C-C
```
看这个穷举逻辑,是有可能出现这样的操作序列 `C-A C-CC-A C-C...` 或者 `C-V,C-V,...`。然这种操作序列的结果不是最优的,但是我们并没有想办法规避这些情况的发生,从而增加了很多没必要的子问题计算。
回顾第二种解法,我们稍加思考就能想到,最优的序列应该是这种形式:`A,A..C-A,C-C,C-V,C-V..C-A,C-C,C-V..`
根据这个事实,我们重新定义了状态,重新寻找了状态转移,从逻辑上减少了无效的子问题个数,从而提高了算法的效率。
坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章:
![labuladong](../pictures/labuladong.jpg)

View File

@ -0,0 +1,182 @@
# How to use a binary search algorithm
**Translator: [Dong Wang](https://github.com/Coder2Programmer)**
**Author: [labuladong](https://github.com/labuladong)**
In what scenarios can binary search be used
The most common example is in textbook, that is, searching for the index of a given target value in **an ordered array**. Moreover, if the target values is duplicated, the modified binary search can return the left boundary or right boundary index of the target value.
PS: The three binary search algorithms mentioned above are explained in detail in the previous [Binary Search Detailed Explanation](../think_like_computer/BinarySearch.md). It is strongly recommended if you haven't read it.
Putting aside the boring ordered array, how can binary search be applied to practical algorithm problems? When the search space is in order, you can perform *pruning* through binary search, greatly improving efficiency.
Talk is cheap, show you the specific *Koko eating banana* problem.
### 1. Problem analysis
Koko loves to eat bananas. There are `N` piles of bananas, the `i`-th pile has `piles[i]` bananas. The guards have gone and will come back in `H` hours.
Koko can decide her bananas-per-hour eating speed of `K`. Each hour, she chooses some pile of bananas, and eats `K` bananas from that pile. If the pile has less than `K` bananas, she eats all of them instead, and won't eat any more bananas during this hour.
Koko likes to eat slowly, but still wants to finish eating all the bananas before the guards come back.
Return the minimum integer `K` such that she can eat all the bananas within `H` hours.
<p><strong>Example 1:</strong></p>
<pre>
<strong>Input:</strong> piles = [3,6,7,11], H = 8
<strong>Output:</strong> 4
</pre>
<p><strong>Example 2:</strong></p>
<pre>
<strong>Input:</strong> piles = [30,11,23,4,20], H = 5
<strong>Output:</strong> 30
</pre>
In other words, Koko eats up to a bunch of bananas every hour.
1. If she can't, she can eat them until the next hour.
2. If she has an appetite after eating this bunch, she will only eat the next bunch until the next hour.
Under this condition, let us determine **the minimum speed** Koko eats bananas.
Given this scenario directly, can you think of where you can use the binary search algorithm? If you haven't seen a similar problem, it's hard to associate this problem with binary search.
So let's put aside the binary search algorithm and think about how to solve the problem violently?
First of all, the algorithm requires *minimum speed of eating bananas in `H` hours*. We might as well call it `speed`. What is the maximum possible `speed`? What is the minimum possible `speed`?
Obviously the minimum is 1 and the maximum is `max(piles)`, because you can only eat a bunch of bananas in an hour. Then the brute force solution is very simple. As long as it starts from 1 and exhausts to `max(piles)`, once it is found that a certain value can eat all bananas in `H` hours, this value is the minimum speed.
```java
int minEatingSpeed(int[] piles, int H) {
// the maximum value of piles
int max = getMax(piles);
for (int speed = 1; speed < max; speed++) {
// wherher can finish eating banana in H hours at speed
if (canFinish(piles, speed, H))
return speed;
}
return max;
}
```
Note that this for loop is a linear search in **continuous space, which is the flag that binary search can work**. Because we require the minimum speed, we can use a **binary search algorithm to find out the left boundary** to replace the linear search to improve efficiency.
```java
int minEatingSpeed(int[] piles, int H) {
// apply the algorithms framework for searching the left boundary
int left = 1, right = getMax(piles) + 1;
while (left < right) {
// prevent overflow
int mid = left + (right - left) / 2;
if (canFinish(piles, mid, H)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
```
PS: If you have questions about the details of this binary search algorithm, it is recommended to look at the algorithm template on the left boundary of the search for [Binary Search Detailed Explanation](../think_like_computer/BinarySearch.md) in the previous article.
The remaining helper functions are also very simple and can be disassembled step by step.
```java
// Time complexity O(N)
boolean canFinish(int[] piles, int speed, int H) {
int time = 0;
for (int n : piles) {
time += timeOf(n, speed);
}
return time <= H;
}
int timeOf(int n, int speed) {
return (n / speed) + ((n % speed > 0) ? 1 : 0);
}
int getMax(int[] piles) {
int max = 0;
for (int n : piles)
max = Math.max(n, max);
return max;
}
```
So far, with the help of the binary search, the time complexity of the algorithm is O(NlogN).
### 2. Extension
Similarly, look at a transportation problem again.
The `i`-th package on the conveyor belt has a weight of `weights[i]`. Each day, we load the ship with packages on the conveyor belt (in the order given by weights). We may not load more weight than the maximum weight capacity of the ship.
Return the least weight capacity of the ship that will result in all the packages on the conveyor belt being shipped within `D` days.
<p><strong>Example 1:</strong></p>
<pre>
<strong>Input:</strong> weights = [1,2,3,4,5,6,7,8,9,10], D = 5
<strong>Output:</strong> 15
<strong>Explanation:</strong>
A ship capacity of 15 is the minimum to ship all the packages in 5 days like this:
1st day: 1, 2, 3, 4, 5
2nd day: 6, 7
3rd day: 8
4th day: 9
5th day: 10
Note that the cargo must be shipped in the order given, so using a ship of capacity 14 and splitting the packages into parts like (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) is not allowed.
</pre>
To transport all the goods within `D` days, the goods are inseparable. How to determine the minimum load for transportation(hereinafter referred to as `cap`)?
In fact, it is essentially the same problem as Koko eating bananas. First, determine the minimum and maximum values of `cap` as `max(weights)` and `sum(weights)`.
We require **minimum load**, so a binary search algorithm that searches the left boundary can be used to optimize the linear search.
```java
// find the left boundary using binary search
int shipWithinDays(int[] weights, int D) {
// minimum possible load
int left = getMax(weights);
// maximum possible load + 1
int right = getSum(weights) + 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (canFinish(weights, D, mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
// If the load is cap, can I ship the goods within D days
boolean canFinish(int[] w, int D, int cap) {
int i = 0;
for (int day = 0; day < D; day++) {
int maxCap = cap;
while ((maxCap -= w[i]) >= 0) {
i++;
if (i == w.length)
return true;
}
}
return false;
}
```
Through these two examples, do you understand the application of binary search in practical problems
```java
for (int i = 0; i < n; i++)
if (isOK(i))
return ans;
```

View File

@ -1,142 +0,0 @@
# 如何运用二分查找算法
二分查找到底有能运用在哪里?
最常见的就是教科书上的例子,在**有序数组**中搜索给定的某个目标值的索引。再推广一点,如果目标值存在重复,修改版的二分查找可以返回目标值的左侧边界索引或者右侧边界索引。
PS以上提到的三种二分查找算法形式在前文「二分查找详解」有代码详解如果没看过强烈建议看看。
抛开有序数组这个枯燥的数据结构,二分查找如何运用到实际的算法问题中呢?当搜索空间有序的时候,就可以通过二分搜索「剪枝」,大幅提升效率。
说起来玄乎得很本文先用一个具体的「Koko 吃香蕉」的问题来举个例子。
### 一、问题分析
![](../pictures/二分应用/title1.png)
也就是说Koko 每小时最多吃一堆香蕉,如果吃不下的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。在这个条件下,让我们确定 Koko 吃香蕉的**最小速度**(根/小时)。
如果直接给你这个情景,你能想到哪里能用到二分查找算法吗?如果没有见过类似的问题,恐怕是很难把这个问题和二分查找联系起来的。
那么我们先抛开二分查找技巧,想想如何暴力解决这个问题呢?
首先,算法要求的是「`H` 小时内吃完香蕉的最小速度」,我们不妨称为 `speed`,请问 `speed` 最大可能为多少,最少可能为多少呢?
显然最少为 1最大为 `max(piles)`,因为一小时最多只能吃一堆香蕉。那么暴力解法就很简单了,只要从 1 开始穷举到 `max(piles)`,一旦发现发现某个值可以在 `H` 小时内吃完所有香蕉,这个值就是最小速度:
```java
int minEatingSpeed(int[] piles, int H) {
// piles 数组的最大值
int max = getMax(piles);
for (int speed = 1; speed < max; speed++) {
// 以 speed 是否能在 H 小时内吃完香蕉
if (canFinish(piles, speed, H))
return speed;
}
return max;
}
```
注意这个 for 循环,就是在**连续的空间线性搜索,这就是二分查找可以发挥作用的标志**。由于我们要求的是最小速度,所以可以用一个**搜索左侧边界的二分查找**来代替线性搜索,提升效率:
```java
int minEatingSpeed(int[] piles, int H) {
// 套用搜索左侧边界的算法框架
int left = 1, right = getMax(piles) + 1;
while (left < right) {
// 防止溢出
int mid = left + (right - left) / 2;
if (canFinish(piles, mid, H)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
```
PS如果对于这个二分查找算法的细节问题有疑问建议看下前文「二分查找详解」搜索左侧边界的算法模板这里不展开了。
剩下的辅助函数也很简单,可以一步步拆解实现:
```java
// 时间复杂度 O(N)
boolean canFinish(int[] piles, int speed, int H) {
int time = 0;
for (int n : piles) {
time += timeOf(n, speed);
}
return time <= H;
}
int timeOf(int n, int speed) {
return (n / speed) + ((n % speed > 0) ? 1 : 0);
}
int getMax(int[] piles) {
int max = 0;
for (int n : piles)
max = Math.max(n, max);
return max;
}
```
至此,借助二分查找技巧,算法的时间复杂度为 O(NlogN)。
### 二、扩展延伸
类似的,再看一道运输问题:
![](../pictures/二分应用/title2.png)
要在 `D` 天内运输完所有货物,货物不可分割,如何确定运输的最小载重呢(下文称为 `cap`
其实本质上和 Koko 吃香蕉的问题一样的,首先确定 `cap` 的最小值和最大值分别为 `max(weights)``sum(weights)`
我们要求**最小载重**,所以可以用搜索左侧边界的二分查找算法优化线性搜索:
```java
// 寻找左侧边界的二分查找
int shipWithinDays(int[] weights, int D) {
// 载重可能的最小值
int left = getMax(weights);
// 载重可能的最大值 + 1
int right = getSum(weights) + 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (canFinish(weights, D, mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
// 如果载重为 cap是否能在 D 天内运完货物?
boolean canFinish(int[] w, int D, int cap) {
int i = 0;
for (int day = 0; day < D; day++) {
int maxCap = cap;
while ((maxCap -= w[i]) >= 0) {
i++;
if (i == w.length)
return true;
}
}
return false;
}
```
通过这两个例子,你是否明白了二分查找在实际问题中的应用?
```java
for (int i = 0; i < n; i++)
if (isOK(i))
return ans;
```
坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章:
![labuladong](../pictures/labuladong.jpg)

View File

@ -0,0 +1,118 @@
# One-line Code Puzzles
**Translator: [tommytim0515](https://github.com/tommytim0515)**
**Author: [labuladong](https://github.com/labuladong)**
This is my summary of three interesting "brain teaser" puzzles from the problems I solved on LeetCode. They can all be solved by algorithmic programming. However, if you think a bit more, you may find the laws of them and figure them out directly.
### 1. Nim Game
The game rule is that there is a heap of stones on the table for you and friends to remove. Each of you takes turns to remove the stones and can take at least one and at most three each time. The one who takes the last stone will win the game. See more on [LeetCode page](https://leetcode.com/problems/nim-game/) and [Wikipedia](https://en.wikipedia.org/wiki/Nim).
Suppose both of you are very clever and have optimal strategies for the game, and you are the first one to take the stone. Write an algorithm to determine whether you can win the game given the number of stones in the heap. (Input a positive integer n, output true or false depending on whether you can win the game).
For instance, there are 4 stones in total, and the output should be false. Because no matter how many stones you take (1, 2, or 3), the opponent can always take the remaining at once including the last one. You are guaranteed to lose the game.
First of all, dynamic programming (DP) can be implemented into this problem, because you can find the repeated sub-problems. However, this method would be very complicated as it involves games played between you and your opponent, who are both clever.
**We usually use contrarian thinking to find a solution of this kind of problems**
If I win the game, I need to take the remaining stones (1\\~3 stones) at once.
How to make this situation come into being? If there are 4 stones remaining when your opponent takes the chance to pick the stones, no matter how he takes the stones, you can always win the game because there will always be 1\~3 stones remaining.
And how to force your opponent to face the situation when there are 4 stones left? If there are 5\~7 stones remaining by the time you take your turn, you can let your opponent face 4-stone situation.
Then how to get into a 5\~7 stones situation when you are picking? Let your opponent face 8 stones. No matter how he plans to take the stones, we can win the game because of the remaining 5\~7 stones.
And so on, we can find out that if n is a multiple of 4, you will fall into the trap and can never win the game. The solution to this problem is very simple:
```cpp
bool canWinNim(int n) {
// If n is a multiple of 4, then return false
// Otherwise, return true
return n % 4 != 0;
}
```
### 2. Stone Game
The game rule is that you and your friend play a game with piles of stones. The piles of stone are represented by an array, ```piles```. ```pile[i]``` refers to the number of stones in the ith pile. Each turn, a player takes the entire pile of stones from either the beginning or the end of the row. And the winner is the one who gets more stones in the end. See more on [LeetCode page](https://leetcode.com/problems/stone-game/).
**Suppose both of you are very clever and have optimal strategies for the game**, and you start first. Write an algorithm to determine whether you can win the game given the number of stones in the heap. (Input an array, pile, output true or false depending on whether you can win the game).
Please pay attention that the number of piles of stones should be even. So both of you get the same number of piles of stones. However, the total amount of stones is odd. So you are not able to get the same number of stones and there must be a winner.
For instance, `piles=[2, 1, 9, 5]`, you take first, you can choose 2 or 5 and you choose 2.
`piles=[1, 9, 5]`, your opponent's turn, he or she can choose 1 or 5, and he or she chooses 5.
`piles=[1, 9]`, your turn, you pick 9.
Finally, your opponent has no choice but choosing 1.
In summary, you get $2 + 9 = 11$ stones in total, your opponents gets $5 + 1 = 6$ stones. You win, the return value is true.
As you can see that it is not always correct to choose the one with larger number of stones. Why you should choose 2 rather than 5 at the first time? Because 9 is behind 5. You will lose the game for giving the pile of 9 stones to the opponent for chasing a moment's gain.
And that is why we need to emphasize that both the players are clever. The algorithm is also to determine whether you can win with best decisions.
The problem also involves playing a game by the two players. It is very complicated to use "brute force" method like dynamic programming (DP). And if we think a bit deeper we will find out that
```java
boolean stoneGame(int[] piles) {
return true;
}
```
Why we can write like this? There are two important conditions about the problem: the number of the pile of stones is even, while the total number of the stone is odd. These two conditions seem to increase the fairness of the game, while they indeed let it become a "leet-cutting" game. For instance, suppose the indexes of piles of stones are 1, 2, 3, 4 from start to end sequentially when `pile=[2, 1, 9, 5]`.
If we divide these four piles of stones into two groups according to whether the index is even or not, which equals 1, 3 in a group and 2, 4 in another group. The numbers of stones of these two groups are different as the total number of stones is odd.
As the first one to take the stones, you can decide to take all the even group or all the odd group at once.
In the beginning, you can choose the 1st pile or the 4th pile. If you want an even group, you can take the 4th pile. So that your opponent can only choose the 1st one or the 3rd one. No matter how he takes, you can choose the 2nd pile after that. Similarly, if you choose the 1st pile which is in the odd group, your opponent can only choose 2nd or 4th pile, no matter how he chooses, you can get the 3rd pile.
In other words, you can observe all the strategies at the first try. You can win the game by observing which group of stones is more, even or odd. Knowing this loophole, you can play a trick on your friend who doesn't know this.
### 3. Bulb Switcher
The description of the problem: there are n bulbs in a room and they are initially turned off. Now we need to do n operations:
1. Flip all the lights.
2. Flip lights with even numbers.
3. Flip the bulb whose number is a multiple of 3 (e.g. 3, 6, 9, ... and 3 is off while 6 is on).
For the i-th round, you toggle every i bulb. For the n-th round, you only toggle the last bulb.
You need to find how many bulbs are on after n rounds. See more on [LeetCode page](https://leetcode.com/problems/bulb-switcher/).
We can simulate the condition with a boolean array, then count the result. However, this method is not smart enough. The best solution is as follows:
```java
int bulbSwitch(int n) {
return (int)Math.sqrt(n);
}
```
What? What does this have to do with square roots? It's actually a pretty neat solution, and it's hard to figure out if nobody tells you how to do it.
First, because the lights are always off at the beginning, a certain light must be flipped an odd number of times if it is turned on at the end.
Let's say we only have six lights, and we're only looking at the sixth light and it's going to take six turns, right? How many times is the switch going to be pressed for the sixth light? It's not difficult to see that its switch will be pressed at the 1st, 2nd, 3rd and 6th round.
Why the light will be flipped at these rounds? Because $6=1\times6=2\times3$. In general, the factors come in pairs, which means that the number of times the switch is pressed is usually even, but in a special case, if there are 16 lights, how many times will the 16th light be flipped?
$16=1\times16=2\times8=4\times4$
The factor 4 repeats, so the 16th light will be flipped 5 times which is odd, and now you understand the relationships to the square root, right?
But, we're going to figure out how many lights are on at the end, and what does that mean by square root? Just think about it a little bit.
Suppose we have 16 lights, and we take the square root of 16, which is equal to 4, and that means we're going to end up with 4 lights on. The lights are $1\times1=1$, $2\times2=4$, $3\times3=9$, and $4\times4=16$.
Some square root of n turns out to be a decimal. However, converting them to integers is the same thing as getting all the integers smaller than a certain integer upper bound, and the square roots of these numbers are the index of the lights on at last. so just turn the square root into an integer, that's the answer to the question.

View File

@ -1,120 +0,0 @@
# 一行代码就能解决的算法题
下文是我在 LeetCode 刷题过程中总结的三道有趣的「脑筋急转弯」题目,可以使用算法编程解决,但只要稍加思考,就能找到规律,直接想出答案。
### 一、Nim 游戏
游戏规则是这样的:你和你的朋友面前有一堆石子,你们轮流拿,一次至少拿一颗,最多拿三颗,谁拿走最后一颗石子谁获胜。
假设你们都很聪明,由你第一个开始拿,请你写一个算法,输入一个正整数 n返回你是否能赢true 或 false
比如现在有 4 颗石子,算法应该返回 false。因为无论你拿 1 颗 2 颗还是 3 颗,对方都能一次性拿完,拿走最后一颗石子,所以你一定会输。
首先,这道题肯定可以使用动态规划,因为显然原问题存在子问题,且子问题存在重复。但是因为你们都很聪明,涉及到你和对手的博弈,动态规划会比较复杂。
**我们解决这种问题的思路一般都是反着思考**
如果我能赢,那么最后轮到我取石子的时候必须要剩下 1~3 颗石子,这样我才能一把拿完。
如何营造这样的一个局面呢?显然,如果对手拿的时候只剩 4 颗石子,那么无论他怎么拿,总会剩下 1~3 颗石子,我就能赢。
如何逼迫对手面对 4 颗石子呢?要想办法,让我选择的时候还有 5~7 颗石子,这样的话我就有把握让对方不得不面对 4 颗石子。
如何营造 5~7 颗石子的局面呢?让对手面对 8 颗石子,无论他怎么拿,都会给我剩下 5~7 颗,我就能赢。
这样一直循环下去,我们发现只要踩到 4 的倍数,就落入了圈套,永远逃不出 4 的倍数,而且一定会输。所以这道题的解法非常简单:
```cpp
bool canWinNim(int n) {
// 如果上来就踩到 4 的倍数,那就认输吧
// 否则,可以把对方控制在 4 的倍数,必胜
return n % 4 != 0;
}
```
### 二、石头游戏
游戏规则是这样的:你和你的朋友面前有一排石头堆,用一个数组 piles 表示piles[i] 表示第 i 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。
**假设你们都很聪明**,由你第一个开始拿,请你写一个算法,输入一个数组 piles返回你是否能赢true 或 false
注意,石头的堆的数量为偶数,所以你们两人拿走的堆数一定是相同的。石头的总数为奇数,也就是你们最后不可能拥有相同多的石头,一定有胜负之分。
举个例子,`piles=[2, 1, 9, 5]`,你先拿,可以拿 2 或者 5你选择 2。
`piles=[1, 9, 5]`,轮到对手,可以拿 1 或 5他选择 5。
`piles=[1, 9]` 轮到你拿,你拿 9。
最后,你的对手只能拿 1 了。
这样下来,你总共拥有 $2 + 9 = 11$ 颗石头,对手有 $5 + 1 = 6$ 颗石头,你是可以赢的,所以算法应该返回 true。
你看到了,并不是简单的挑数字大的选,为什么第一次选择 2 而不是 5 呢?因为 5 后面是 9你要是贪图一时的利益就把 9 这堆石头暴露给对手了,那你就要输了。
这也是强调双方都很聪明的原因,算法也是求最优决策过程下你是否能赢。
这道题又涉及到两人的博弈,也可以用动态规划算法暴力试,比较麻烦。但我们只要对规则深入思考,就会大惊失色:只要你足够聪明,你是必胜无疑的,因为你是先手。
```java
boolean stoneGame(int[] piles) {
return true;
}
```
这是为什么呢,因为题目有两个条件很重要:一是石头总共有偶数堆,石头的总数是奇数。这两个看似增加游戏公平性的条件,反而使该游戏成为了一个割韭菜游戏。我们以 `piles=[2, 1, 9, 5]` 讲解,假设这四堆石头从左到右的索引分别是 1234。
如果我们把这四堆石头按索引的奇偶分为两组,即第 1、3 堆和第 2、4 堆,那么这两组石头的数量一定不同,也就是说一堆多一堆少。因为石头的总数是奇数,不能被平分。
而作为第一个拿石头的人,你可以控制自己拿到所有偶数堆,或者所有的奇数堆。
你最开始可以选择第 1 堆或第 4 堆。如果你想要偶数堆,你就拿第 4 堆,这样留给对手的选择只有第 1、3 堆,他不管怎么拿,第 2 堆又会暴露出来,你就可以拿。同理,如果你想拿奇数堆,你就拿第 1 堆,留给对手的只有第 2、4 堆,他不管怎么拿,第 3 堆又给你暴露出来了。
也就是说,你可以在第一步就观察好,奇数堆的石头总数多,还是偶数堆的石头总数多,然后步步为营,就一切尽在掌控之中了。知道了这个漏洞,可以整一整不知情的同学了。
### 三、电灯开关问题
这个问题是这样描述的:有 n 盏电灯,最开始时都是关着的。现在要进行 n 轮操作:
第 1 轮操作是把每一盏电灯的开关按一下(全部打开)。
第 2 轮操作是把每两盏灯的开关按一下(就是按第 246... 盏灯的开关,它们被关闭)。
第 3 轮操作是把每三盏灯的开关按一下(就是按第 369... 盏灯的开关,有的被关闭,比如 3有的被打开比如 6...
如此往复,直到第 n 轮,即只按一下第 n 盏灯的开关。
现在给你输入一个正整数 n 代表电灯的个数,问你经过 n 轮操作后,这些电灯有多少盏是亮的?
我们当然可以用一个布尔数组表示这些灯的开关情况,然后模拟这些操作过程,最后去数一下就能出结果。但是这样显得没有灵性,最好的解法是这样的:
```java
int bulbSwitch(int n) {
return (int)Math.sqrt(n);
}
```
什么?这个问题跟平方根有什么关系?其实这个解法挺精妙,如果没人告诉你解法,还真不好想明白。
首先,因为电灯一开始都是关闭的,所以某一盏灯最后如果是点亮的,必然要被按奇数次开关。
我们假设只有 6 盏灯,而且我们只看第 6 盏灯。需要进行 6 轮操作对吧,请问对于第 6 盏灯,会被按下几次开关呢?这不难得出,第 1 轮会被按,第 2 轮,第 3 轮,第 6 轮都会被按。
为什么第 1、2、3、6 轮会被按呢?因为 $6=1\times6=2\times3$。一般情况下,因子都是成对出现的,也就是说开关被按的次数一般是偶数次。但是有特殊情况,比如说总共有 16 盏灯,那么第 16 盏灯会被按几次?
$16=1\times16=2\times8=4\times4$
其中因子 4 重复出现,所以第 16 盏灯会被按 5 次,奇数次。现在你应该理解这个问题为什么和平方根有关了吧?
不过,我们不是要算最后有几盏灯亮着吗,这样直接平方根一下是啥意思呢?稍微思考一下就能理解了。
就假设现在总共有 16 盏灯,我们求 16 的平方根,等于 4这就说明最后会有 4 盏灯亮着,它们分别是第 $1\times1=1$ 盏、第 $2\times2=4$ 盏、第 $3\times3=9$ 盏和第 $4\times4=16$ 盏。
就算有的 n 平方根结果是小数,强转成 int 型,也相当于一个最大整数上界,比这个上界小的所有整数,平方后的索引都是最后亮着的灯的索引。所以说我们直接把平方根转成整数,就是这个问题的答案。
坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章:
![labuladong](../pictures/labuladong.jpg)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -0,0 +1,121 @@
# Pancakes Sorting
**Translator: [Dong Wang](https://github.com/Coder2Programmer)**
**Author: [labuladong](https://github.com/labuladong)**
The pancake sorting is a very interesting practical problem: assuming there are `n` pieces of pancakes of **different sizes** on the plate, how do you turn it several times with a spatula to make these pancakes in order(small up, big down)?
![](../pictures/pancakeSort/1.jpg)
Imagine using a spatula to flip a pile of pancakes. There are actually a few restrictions that we can only flip the top cakes at a time:
![](../pictures/pancakeSort/2.png)
Our question is, **how do you use an algorithm to get a sequence of flips to make the cake pile order**?
First, we need to abstract this problem and use an array to represent the pancakes heap:
![](../pictures/pancakeSort/title.png)
How to solve this problem? In fact, it is similar to the previous article [Part of a Recursive Reverse Linked List](../data_structure/reverse_part_of_a_linked_list_via_recursion.md), which also requires **recursive thinking**.
### 1. Analysis of idea
Why is this problem recursive? For example, we need to implement a function like this:
```java
// cakes is a bunch of pancakes, the function will sort the first n pancakes
void sort(int[] cakes, int n);
```
If we find the largest of the first `n` pancakes, then we try to flip this pancake to the bottom:
![](../pictures/pancakeSort/3.jpg)
Then, the scale of the original problem can be reduced, recursively calling `pancakeSort (A, n-1)`:
![](../pictures/pancakeSort/4.jpg)
Next, how to sort the `n-1` pancakes above? Still find the largest piece of pancakes from it, then place this piece of pancake to the bottom, and then recursively call `pancakeSort (A, n-1-1)` ...
You see, this is the nature of recursion. To summarize, the idea is:
1. Find the largest of the `n` pancakes.
2. Move this largest pancake to the bottom.
3. Recursively call `pancakeSort(A, n-1)`.
Base case: When `n == 1`, there is no need to flip when sorting 1 pancake.
So, the last question left, **how do you manage to turn a piece of pancake to the end**?
In fact, it is very simple. For example, the third pancake is the largest, and we want to change it to the end, that is, to the `n` block. You can do this:
1. Use a spatula to turn the first 3 pieces of pancakes, so that the largest pancake turns to the top.
2. Use a spatula to flip all the first `n` cakes, so that the largest pancake turns to the `n`-th pancake, which is the last pancake.
After the above two processes are understood, the solution can be basically written, but the title requires us to write a specific sequence of inversion operations, which is also very simple, as long as it is recorded each time the pancake is turned.
### 2. Code implementation
As long as the above ideas are implemented in code, the only thing to note is that the array index starts from 0, and the results we want to return are calculated from 1.
```java
// record the reverse operation sequence
LinkedList<Integer> res = new LinkedList<>();
List<Integer> pancakeSort(int[] cakes) {
sort(cakes, cakes.length);
return res;
}
void sort(int[] cakes, int n) {
// base case
if (n == 1) return;
// find the index of the largest pancake
int maxCake = 0;
int maxCakeIndex = 0;
for (int i = 0; i < n; i++)
if (cakes[i] > maxCake) {
maxCakeIndex = i;
maxCake = cakes[i];
}
// first flip, turn the largest pancake to the top
reverse(cakes, 0, maxCakeIndex);
res.add(maxCakeIndex + 1);
// second flip, turn the largest pancake to the bottom
reverse(cakes, 0, n - 1);
res.add(n);
// recursive
sort(cakes, n - 1);
}
void reverse(int[] arr, int i, int j) {
while (i < j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++; j--;
}
}
```
hrough the detailed explanation just now, this code should be very clear.
The time complexity of the algorithm is easy to calculate, because the number of recursive calls is `n`, each recursive call requires a for loop, the time complexity is O(n), so the total complexity is O(n^2).
**Finally, we can think about a problem.**: According to our thinking, the length of the operation sequence should be `2(n-1)`, because each recursion needs to perform 2 flips and record operations and there are always `n` layers of recursion, but since the base case returns the result directly without inversion, the length of the final operation sequence should be fixed `2(n-1) `.
Obviously, this result is not optimal (shortest). For example, a bunch of pancakes `[3,2,4,1]`. The flip sequence obtained by our algorithm is `[3,4,2,3,1,2]`, but the fastest way to flip should be ` [2,3,4] `:
* Initial state: `[3,2,4,1]`
* Turn over the first two: `[2,3,4,1]`
* Turn over the first three: `[4,3,2,1]`
* Turn over the first 4: `[1,2,3,4]`
If your algorithm is required to calculate the **shortest** operation sequence for sorting biscuits, how do you calculate it? In other words, what is the core idea and what algorithm skills must be used to solve the problem of finding the optimal solution?
May wish to share your thoughts.

View File

@ -1,124 +0,0 @@
# 烧饼排序
烧饼排序是个很有意思的实际问题:假设盘子上有 `n` 块**面积大小不一**的烧饼,你如何用一把锅铲进行若干次翻转,让这些烧饼的大小有序(小的在上,大的在下)?
![](../pictures/pancakeSort/1.jpg)
设想一下用锅铲翻转一堆烧饼的情景,其实是有一点限制的,我们每次只能将最上面的若干块饼子翻转:
![](../pictures/pancakeSort/2.png)
我们的问题是,**如何使用算法得到一个翻转序列,使得烧饼堆变得有序**
首先,需要把这个问题抽象,用数组来表示烧饼堆:
![](../pictures/pancakeSort/title.png)
如何解决这个问题呢?其实类似上篇文章 [递归反转链表的一部分](../数据结构系列/递归反转链表的一部分.md),这也是需要**递归思想**的。
### 一、思路分析
为什么说这个问题有递归性质呢?比如说我们需要实现这样一个函数:
```java
// cakes 是一堆烧饼,函数会将前 n 个烧饼排序
void sort(int[] cakes, int n);
```
如果我们找到了前 `n` 个烧饼中最大的那个,然后设法将这个饼子翻转到最底下:
![](../pictures/pancakeSort/3.jpg)
那么,原问题的规模就可以减小,递归调用 `pancakeSort(A, n-1)` 即可:
![](../pictures/pancakeSort/4.jpg)
接下来,对于上面的这 `n - 1` 块饼,如何排序呢?还是先从中找到最大的一块饼,然后把这块饼放到底下,再递归调用 `pancakeSort(A, n-1-1)`……
你看,这就是递归性质,总结一下思路就是:
1、找到 `n` 个饼中最大的那个。
2、把这个最大的饼移到最底下。
3、递归调用 `pancakeSort(A, n - 1)`
base case`n == 1` 时,排序 1 个饼时不需要翻转。
那么,最后剩下个问题,**如何设法将某块烧饼翻到最后呢**
其实很简单,比如第 3 块饼是最大的,我们想把它换到最后,也就是换到第 `n` 块。可以这样操作:
1、用锅铲将前 3 块饼翻转一下,这样最大的饼就翻到了最上面。
2、用锅铲将前 `n` 块饼全部翻转,这样最大的饼就翻到了第 `n` 块,也就是最后一块。
以上两个流程理解之后,基本就可以写出解法了,不过题目要求我们写出具体的反转操作序列,这也很简单,只要在每次翻转烧饼时记录下来就行了。
### 二、代码实现
只要把上述的思路用代码实现即可,唯一需要注意的是,数组索引从 0 开始,而我们要返回的结果是从 1 开始算的。
```java
// 记录反转操作序列
LinkedList<Integer> res = new LinkedList<>();
List<Integer> pancakeSort(int[] cakes) {
sort(cakes, cakes.length);
return res;
}
void sort(int[] cakes, int n) {
// base case
if (n == 1) return;
// 寻找最大饼的索引
int maxCake = 0;
int maxCakeIndex = 0;
for (int i = 0; i < n; i++)
if (cakes[i] > maxCake) {
maxCakeIndex = i;
maxCake = cakes[i];
}
// 第一次翻转,将最大饼翻到最上面
reverse(cakes, 0, maxCakeIndex);
res.add(maxCakeIndex + 1);
// 第二次翻转,将最大饼翻到最下面
reverse(cakes, 0, n - 1);
res.add(n);
// 递归调用
sort(cakes, n - 1);
}
void reverse(int[] arr, int i, int j) {
while (i < j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++; j--;
}
}
```
通过刚才的详细解释,这段代码应该是很清晰了。
算法的时间复杂度很容易计算,因为递归调用的次数是 `n`,每次递归调用都需要一次 for 循环,时间复杂度是 O(n),所以总的复杂度是 O(n^2)。
**最后,我们可以思考一个问题​**:按照我们这个思路,得出的操作序列长度应该为​ `2(n - 1)`,因为每次递归都要进行 2 次翻转并记录操作,总共有 `n` 层递归,但由于 base case 直接返回结果,不进行翻转,所以最终的操作序列长度应该是固定的 `2(n - 1)`
显然,这个结果不是最优的(最短的),比如说一堆煎饼 `[3,2,4,1]`,我们的算法得到的翻转序列是 `[3,4,2,3,1,2]`,但是最快捷的翻转方法应该是 `[2,3,4]`
初始状态 [3,2,4,1]
翻前 2 个:[2,3,4,1]
翻前 3 个:[4,3,2,1]
翻前 4 个:[1,2,3,4]
如果要求你的算法计算排序烧饼的**最短**操作序列,你该如何计算呢?或者说,解决这种求最优解法的问题,核心思路什么,一定需要使用什么算法技巧呢?
不妨分享一下你的思考。
坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章:
![labuladong](../pictures/labuladong.jpg)