OSDN Git Service

refactor[binary.md]: 二分
author夜轮_NachtgeistW <NachtgeistW@protonmail.com>
Sun, 27 Sep 2020 13:53:25 +0000 (21:53 +0800)
committer夜轮_NachtgeistW <NachtgeistW@protonmail.com>
Sun, 27 Sep 2020 13:53:25 +0000 (21:53 +0800)
docs/basic/binary.md

index 074d7e6..c3538f0 100644 (file)
@@ -1,14 +1,32 @@
+本页面将简要介绍二分查找,由二分法衍生的三分法以及二分答案。
+
 ## 二分法
 
-### 二分查找
+### 简介
+
+二分查找(英语:binary search),也称折半搜索(英语:half-interval search)、对数搜索(英语:logarithmic search),是用来在一个有序数组中查找某一元素的算法。
 
-二分搜索,也称折半搜索、二分查找,是用来在一个有序数组中查找某一元素的算法。
+### 工作原理
 
 以在一个升序数组中查找一个数为例。
 
-它每次考察数组当前部分的中间元素,如果中间元素刚好是要找的,就结束搜索过程;如果中间元素小于所查找的值,那么左侧的只会更小,不会有所查找的元素,只需要到右侧去找就好了;如果中间元素大于所查找的值,同理,右侧的只会更大而不会有所查找的元素,所以只需要到左侧去找。
+它每次考察数组当前部分的中间元素,如果中间元素刚好是要找的,就结束搜索过程;如果中间元素小于所查找的值,那么左侧的只会更小,不会有所查找的元素,只需到右侧查找;如果中间元素大于所查找的值同理,只需到左侧查找。
+
+### 性质
+
+#### 时间复杂度
+
+二分查找的最优时间复杂度为$O(1)$。
 
-在二分搜索过程中,每次都把查询的区间减半,因此对于一个长度为 $n$ 的数组,至多会进行 $O(\log n)$ 次查找。
+二分查找的平均时间复杂度和最坏时间复杂度均为$O(\log n)$。因为在二分搜索过程中,算法每次都把查询的区间减半,所以对于一个长度为 $n$ 的数组,至多会进行 $O(\log n)$ 次查找。
+
+#### 空间复杂度
+
+迭代版本的二分查找的空间复杂度为$O(1)$。
+
+递归(无尾调用消除)版本的二分查找的空间复杂度为$O(\log n)$。
+
+### 代码实现
 
 ```cpp
 int binary_search(int start, int end, int key) {
@@ -29,14 +47,14 @@ int binary_search(int start, int end, int key) {
 }
 ```
 
-??? note
+???+note
     对于 $n$ 是有符号数的情况,当你可以保证 $n\ge 0$ 时, `n >> 1` 比 `n / 2` 指令数更少。
 
-注意,这里的有序是广义的有序,如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看作是一种有序(如果把满足条件看做 $1$ ,不满足看做 $0$ ,至少对于这个条件的这一维度是有序的)。换言之,二分搜索法可以用来查找满足某种条件的最大(最小)的值。
+### 最大值最小化
 
-如果我们要求满足某种条件的最大值的最小可能情况(最大值最小化)呢?首先的想法是从小到大枚举这个作为答案的「最大值」,然后去判断是否合法。要是这个答案是单调的就好了,那样就可以使用二分搜索法来更快地找到答案
+注意,这里的有序是广义的有序,如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看作是一种有序(如果把满足条件看做 $1$ ,不满足看做 $0$ ,至少对于这个条件的这一维度是有序的)。换言之,二分搜索法可以用来查找满足某种条件的最大(最小)的值
 
-要想使用二分搜索法来解这种「最大值最小化」的题目,需要满足以下三个条件:
+è¦\81æ±\82满足æ\9f\90ç§\8dæ\9d¡ä»¶ç\9a\84æ\9c\80大å\80¼ç\9a\84æ\9c\80å°\8få\8f¯è\83½æ\83\85å\86µï¼\88æ\9c\80大å\80¼æ\9c\80å°\8få\8c\96ï¼\89ï¼\8cé¦\96å\85\88ç\9a\84æ\83³æ³\95æ\98¯ä»\8eå°\8få\88°å¤§æ\9e\9a举è¿\99个ä½\9c为ç­\94æ¡\88ç\9a\84ã\80\8cæ\9c\80大å\80¼ã\80\8dï¼\8cç\84¶å\90\8eå\8e»å\88¤æ\96­æ\98¯å\90¦å\90\88æ³\95ã\80\82è\8b¥ç­\94æ¡\88å\8d\95è°\83ï¼\8cå°±å\8f¯ä»¥ä½¿ç\94¨äº\8cå\88\86æ\90\9cç´¢æ³\95æ\9d¥æ\9b´å¿«å\9c°æ\89¾å\88°ç­\94æ¡\88ã\80\82å\9b æ­¤ï¼\8cè¦\81æ\83³ä½¿ç\94¨äº\8cå\88\86æ\90\9cç´¢æ³\95æ\9d¥è§£è¿\99ç§\8dã\80\8cæ\9c\80大å\80¼æ\9c\80å°\8få\8c\96ã\80\8dç\9a\84é¢\98ç\9b®ï¼\8cé\9c\80è¦\81满足以ä¸\8bä¸\89个æ\9d¡ä»¶ï¼\9a
 
 1. 答案在一个固定区间内;
 2. 可能查找一个符合条件的值不是很容易,但是要求能比较容易地判断某个值是否是符合条件的;
@@ -44,73 +62,91 @@ int binary_search(int start, int end, int key) {
 
 当然,最小值最大化是同理的。
 
-二分法把一个寻找极值的问题转化成一个判定的问题(用二分搜索来找这个极值)。类比枚举法,我们当时是枚举答案的可能情况,现在由于单调性,我们不再需要一个个枚举,利用二分的思路,就可以用更优的方法解决「最大值最小」、「最小值最大」。这种解法也成为是「二分答案」,常见于解题报告中。
-
 ### STL 的二分查找
 
-补充一个小知识点,对于一个有序的 array 你可以使用 `std::lower_bound()` 来找到第一个大于等于你的值的数, `std::upper_bound()` 来找到第一个大于你的值的数。
-
-请注意,必须是有序数组,否则答案是错误的。
+C++ 标准库中实现了查找首个不小于给定值的元素的函数 [`std::lower_bound`](https://zh.cppreference.com/w/cpp/algorithm/lower_bound)和查找首个大于给定值的元素的函数 [`std::upper_bound`](https://zh.cppreference.com/w/cpp/algorithm/upper_bound),二者均定义于头文件 `<algorithm>` 中。
 
-关于具体使用方法,请参见 [STL 页面](../lang/csl/index.md) 
+二者均采用二分实现,所以调用前必须保证元素有序
 
 ### 二分答案
 
-解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。如果我们把这里的枚举换成二分,就变成了“二分答案”。
+解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。若满足单调性,则满足使用二分法的条件。把这里的枚举换成二分,就变成了“二分答案”。
 
-来看一看一道例题 [Luogu P1873 砍树](https://www.luogu.com.cn/problem/P1873) ,我们可以在 1 到 1000000000(10 亿)中枚举答案,但是这种朴素写法肯定拿不到满分,因为从 1 跑到 10 亿太耗时间。我们可以对答案进行 1 到 10 亿的二分,其中,每次都对其进行检查可行性(一般都是使用贪心法)。 **这就是二分答案。** 
+???+note "[Luogu P1873 砍树](https://www.luogu.com.cn/problem/P1873)"
+    伐木工人米尔科需要砍倒M米长的木材。这是一个对米尔科来说很容易的工作,因为他有一个漂亮的新伐木机,可以像野火一样砍倒森林。不过,米尔科只被允许砍倒单行树木。
 
-下面就是例题的参考答案
+    米尔科的伐木机工作过程如下:米尔科设置一个高度参数H(米),伐木机升起一个巨大的锯片到高度H,并锯掉所有的树比H高的部分(当然,树木不高于H米的部分保持不变)。米尔科就行到树木被锯下的部分
 
-```cpp
-int a[1000005];
-int n, m;
-bool check(int k) {  // 检查可行性,k为锯片高度
-  long long sum = 0;
-  for (int i = 1; i <= n; i++)       // 检查每一棵树
-    if (a[i] > k)                    // 如果树高于锯片高度
-      sum += (long long)(a[i] - k);  // 累加树木长度
-  return sum >= m;                   // 如果满足最少长度代表可行
-}
-int find() {
-  int l = 1, r = 1000000001;  // 因为是左闭右开的,所以10亿要加1
-  while (l + 1 < r) {         // 如果两点不相邻
-    int mid = (l + r) / 2;    // 取中间值
-    if (check(mid))           // 如果可行
-      l = mid;                // 升高锯片高度
-    else
-      r = mid;  // 否则降低锯片高度
-  }
-  return l;  // 返回左边值
-}
-int main() {
-  cin >> n >> m;
-  for (int i = 1; i <= n; i++) cin >> a[i];
-  cout << find();
-  return 0;
-}
-```
+    例如,如果一行树的高度分别为20,15,10和17,米尔科把锯片升到15米的高度,切割后树木剩下的高度将是15,15,10和15,而米尔科将从第1棵树得到5米,从第4棵树得到2米,共得到7米木材。
 
-看完了上面的代码,你肯定会有两个疑问:
+    米尔科非常关注生态保护,所以他不会砍掉过多的木材。这正是他为什么尽可能高地设定伐木机锯片的原因。帮助米尔科找到伐木机锯片的最大的整数高度H,使得他能得到木材至少为M米。换句话说,如果再升高1米,则他将得不到M米木材。
 
-1.  为何搜索区间是左闭右开的?
+??? note "解题思路"
+    我们可以在 1 到 1,000,000,000(10 亿)中枚举答案,但是这种朴素写法肯定拿不到满分,因为从 1 跑到 10 亿太耗时间。我们可以对答案进行 1 到 10 亿的二分,然后,每次都对其进行检查可行性(一般都是使用贪心法)。 **这就是二分答案。** 
 
-    因为搜到最后,会这样(以合法的最大值为例):
+??? note "参考代码"
+    ```cpp
+    int a[1000005];
+    int n, m;
+    bool check(int k) {  // 检查可行性,k为锯片高度
+      long long sum = 0;
+      for (int i = 1; i <= n; i++)       // 检查每一棵树
+        if (a[i] > k)                    // 如果树高于锯片高度
+          sum += (long long)(a[i] - k);  // 累加树木长度
+      return sum >= m;                   // 如果满足最少长度代表可行
+    }
+    
+    int find() {
+      int l = 1, r = 1000000001;  // 因为是左闭右开的,所以10亿要加1
+      while (l + 1 < r) {         // 如果两点不相邻
+        int mid = (l + r) / 2;    // 取中间值
+        if (check(mid))           // 如果可行
+          l = mid;                // 升高锯片高度
+        else
+          r = mid;  // 否则降低锯片高度
+      }
+      return l;  // 返回左边值
+    }
+    
+    int main() {
+      cin >> n >> m;
+      for (int i = 1; i <= n; i++) cin >> a[i];
+      cout << find();
+      return 0;
+    }
+    ```
+
+    看完了上面的代码,你肯定会有两个疑问:
+
+    1. 为何搜索区间是左闭右开的?
 
-    ![](./images/binary-final-1.png)
+        因为搜到最后,会这样(以合法的最大值为例):
 
-    然后会
+        ![](./images/binary-final-1.png)
 
-    ![](./images/binary-final-2.png)
+        然后会
 
-    合法的最小值恰恰相反。
+        ![](./images/binary-final-2.png)
 
-2.  为何返回左边值?
+        合法的最小值恰恰相反。
 
-    如上图
+    2.  为何返回左边值?
+
+        同上。
 
 ## 三分法
 
+### 简介
+
+三分法可以用来查找凸函数的最大(小)值。
+
+画一下图好理解一些(图待补)
+
+- 如果 `lmid` 和 `rmid` 在最大(小)值的同一侧:由于单调性,一定是二者中较大(小)的那个离最值近一些,较远的那个点对应的区间不可能包含最值,所以可以舍弃。
+- 如果在两侧:由于最值在二者中间,我们舍弃两侧的一个区间后,也不会影响最值,所以可以舍弃。
+
+### 代码实现
+
 ```cpp
 lmid = left + (right - left >> 1);
 rmid = lmid + (right - lmid >> 1);  // 对右侧区间取半
@@ -120,19 +156,12 @@ else
   left = lmid;
 ```
 
-三分法可以用来查找凸函数的最大(小)值。
-
-画一下图好理解一些(图待补)
-
--   如果 `lmid` 和 `rmid` 在最大(小)值的同一侧:
-    那么由于单调性,一定是二者中较大(小)的那个离最值近一些,较远的那个点对应的区间不可能包含最值,所以可以舍弃。
--   如果在两侧:
-    由于最值在二者中间,我们舍弃两侧的一个区间后,也不会影响最值,所以可以舍弃。
-
 ## 分数规划
 
-分数规划是这样一类问题,每个物品有两个属性 $c_i$ , $d_i$ ,要求通过某种方式选出若干个,使得 $\frac{\sum{c_i}}{\sum{d_i}}$ 最大或最小。
+参见:[分数规划](../misc/frac-programming.md) 
+
+分数规划通常描述为下列问题:每个物品有两个属性 $c_i$ , $d_i$ ,要求通过某种方式选出若干个,使得 $\frac{\sum{c_i}}{\sum{d_i}}$ 最大或最小。
 
-经典的例子有 最优比率环、最优比率生成树 等等。
+经典的例子有最优比率环、最优比率生成树等等。
 
-分数规划可以用二分法来解决,详情参见 [分数规划](../misc/frac-programming.md) 页面
+分数规划可以用二分法来解决。