未验证 提交 adc05efc 编写于 作者: L labuladong 提交者: GitHub

Merge pull request #168 from Coder2Programmer/english

tranlate koko eating bananas and pancakes sorting. 
# 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;
```
# 如何运用二分查找算法
二分查找到底有能运用在哪里?
最常见的就是教科书上的例子,在**有序数组**中搜索给定的某个目标值的索引。再推广一点,如果目标值存在重复,修改版的二分查找可以返回目标值的左侧边界索引或者右侧边界索引。
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)
# 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.
# 烧饼排序
烧饼排序是个很有意思的实际问题:假设盘子上有 `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)
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册