OSDN Git Service

Update binary.md
authorCaoBowen <42204218+CBW2007@users.noreply.github.com>
Thu, 6 Dec 2018 11:59:36 +0000 (19:59 +0800)
committerGitHub <noreply@github.com>
Thu, 6 Dec 2018 11:59:36 +0000 (19:59 +0800)
docs/basic/binary.md

index b615ff5..3876be1 100644 (file)
@@ -1,6 +1,6 @@
-二分法
+## 二分法
 
-二分查找
+### 二分查找
 
 二分搜索,也称折半搜索、二分查找,是用来在一个有序数组中查找某一元素的算法。
 
 
 它每次考察数组当前部分的中间元素,如果中间元素刚好是要找的,就结束搜索过程;如果中间元素小于所查找的值,那么左侧的只会更小,不会有所查找的元素,只需要到右侧去找就好了;如果中间元素大于所查找的值,同理,右侧的只会更大而不会有所查找的元素,所以只需要到左侧去找。
 
-在二分搜索过程中,每次都把查询的区间减半,因此对于一个长度为 n 的数组,至多会进行 O(\\log n) 次查找。
-
-    int binary_search(int start, int end, int key) {
-      int ret = -1;  // 未搜索到数据返回-1下标
-      int mid;
-      while (start <= end) {
-        mid = start + ((end - start) >> 1);  //直接平均可能会溢出,所以用这个算法
-        if (arr[mid] < key)
-          start = mid + 1;
-        else if (arr[mid] > key)
-          end = mid - 1;
-        else {  // 最后检测相等是因为多数搜索情况不是大于就是小于
-          ret = mid;
-          break;
-        }
-      }
-      return ret;  // 单一出口
+在二分搜索过程中,每次都把查询的区间减半,因此对于一个长度为 $n$ 的数组,至多会进行 $O(\log n)$ 次查找。
+
+```c++
+int binary_search(int start, int end, int key) {
+  int ret = -1;  // 未搜索到数据返回-1下标
+  int mid;
+  while (start <= end) {
+    mid = start + ((end - start) >> 1);  //直接平均可能会溢出,所以用这个算法
+    if (arr[mid] < key)
+      start = mid + 1;
+    else if (arr[mid] > key)
+      end = mid - 1;
+    else {  // 最后检测相等是因为多数搜索情况不是大于就是小于
+      ret = mid;
+      break;
     }
+  }
+  return ret;  // 单一出口
+}
+```
 
 ??? note
 
-    >> 1 比 / 2 速度快一些
+```
+`>> 1` 比 `/ 2` 速度快一些
+```
 
-注意,这里的有序是广义的有序,如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看作是一种有序(如果把满足条件看做 1,不满足看做 0,至少对于这个条件的这一维度是有序的)。换言之,二分搜索法可以用来查找满足某种条件的最大(最小)的值。
+注意,这里的有序是广义的有序,如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看作是一种有序(如果把满足条件看做 $1$,不满足看做 $0$,至少对于这个条件的这一维度是有序的)。换言之,二分搜索法可以用来查找满足某种条件的最大(最小)的值。
 
 如果我们要求满足某种条件的最大值的最小可能情况(最大值最小化)呢?首先的想法是从小到大枚举这个作为答案的「最大值」,然后去判断是否合法。要是这个答案是单调的就好了,那样就可以使用二分搜索法来更快地找到答案。
 
 要想使用二分搜索法来解这种「最大值最小化」的题目,需要满足以下三个条件:
 
-1.  答案在一个固定区间内;
-2.  可能查找一个符合条件的值不是很容易,但是要求能比较容易地判断某个值是否是符合条件的;
-3.  可行解对于区间满足一定的单调性。换言之,如果 x 是符合条件的,那么有 x + 1 或者 x - 1 也符合条件。(这样下来就满足了上面提到的单调性)
+1. 答案在一个固定区间内;
+2. 可能查找一个符合条件的值不是很容易,但是要求能比较容易地判断某个值是否是符合条件的;
+3. 可行解对于区间满足一定的单调性。换言之,如果 $x$ 是符合条件的,那么有 $x + 1$ 或者 $x - 1$ 也符合条件。(这样下来就满足了上面提到的单调性)
 
 当然,最小值最大化是同理的。
 
 二分法把一个寻找极值的问题转化成一个判定的问题(用二分搜索来找这个极值)。类比枚举法,我们当时是枚举答案的可能情况,现在由于单调性,我们不再需要一个个枚举,利用二分的思路,就可以用更优的方法解决「最大值最小」、「最小值最大」。这种解法也成为是「二分答案」,常见于解题报告中。
 
-STL 的二分查找
+### STL 的二分查找
 
-补充一个小知识点, 对于一个有序的 array 你可以使用 std::lower_bound() 来找到第一个大于等于你的值的数, std::upper_bound() 来找到第一个大于你的值的数。
+补充一个小知识点, 对于一个有序的 array 你可以使用 `std::lower_bound()` 来找到第一个大于等于你的值的数, `std::upper_bound()` 来找到第一个大于你的值的数。
 
 请注意,必须是有序数组,否则答案是错误的。
 
-关于具体使用方法,请参见 STL 页面
+关于具体使用方法,请参见 [STL 页面](/ds/stl/)
 
-二分答案
+### 二分答案
 
 二分是非常好的搜索算法,许多线性查找题目都能够转换为二分答案。
 
-来看一看一道例题 Luogu  P1873 砍树,我们可以从 1 到 1000000000(10 亿)来枚举答案,但是这种朴素写法肯定拿不到满分,因为从 1 跑到 10 亿太耗时间。我们可以对答案进行 1 到 10 亿的二分,其中,每次都对其进行检查可行性(一般都是使用贪心法)。这就是二分答案。
+来看一看一道例题[Luogu  P1873 砍树](https://www.luogu.org/problemnew/show/P1873),我们可以从1到1000000000(10亿)来枚举答案,但是这种朴素写法肯定拿不到满分,因为从1跑到10亿太耗时间。我们可以对答案进行1到10亿的二分,其中,每次都对其进行检查可行性(一般都是使用贪心法)。**这就是二分答案。**
 
 下面就是例题的参考答案。
 
-    int a[1000005];
-    int n,m;
-    bool check(int k)//检查可行性,k为锯片高度
+```c++
+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 x)
+{
+    int l=1,r=1000000001;//因为是左闭右开的,所以10亿要加1
+    while (l+1<r)//如果两点不相邻
     {
-        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 x)
-    {
-        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(m);
-        return 0;
+        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(m);
+    return 0;
+}
+```
 
 看完了上面的代码,你肯定会有两个疑问:
 
-1.  为何搜索区间是左闭右开的?
+1. 为何搜索区间是左闭右开的?
 
 因为搜到最后,会这样(以合法的最大值为例):
 
+![](./images/binary-final-1.png)
+
+然后会
+
+![](./images/binary-final-2.png)
+
 合法的最小值恰恰相反。
 
-1.  为何返回左边值?
+1. 为何返回左边值?
 
 如上图
 
-三分法
+## 三分法
 
-    mid = left + (right - left >> 1);
-    midmid = mid + (right - mid >> 1);  // 对右侧区间取半
-    if (cal(mid) > cal(midmid))
-      right = midmid;
-    else
-      left = mid;
+```c++
+mid = left + (right - left >> 1);
+midmid = mid + (right - mid >> 1);  // 对右侧区间取半
+if (cal(mid) > cal(midmid))
+  right = midmid;
+else
+  left = mid;
+```
 
 三分法可以用来查找凸函数的最大(小)值。
 
 画一下图好理解一些(图待补)
 
--   如果 mid 和 midmid 在最大(小)值的同一侧:
-    那么由于单调性,一定是二者中较大(小)的那个离最值近一些,较远的那个点对应的区间不可能包含最值,所以可以舍弃。
--   如果在两侧:
-    由于最值在二者中间,我们舍弃两侧的一个区间后,也不会影响最值,所以可以舍弃。
+- 如果 `mid` 和 `midmid` 在最大(小)值的同一侧:
+  那么由于单调性,一定是二者中较大(小)的那个离最值近一些,较远的那个点对应的区间不可能包含最值,所以可以舍弃。
+- 如果在两侧:
+  由于最值在二者中间,我们舍弃两侧的一个区间后,也不会影响最值,所以可以舍弃。
 
-分数规划
+## 分数规划
 
-分数规划是这样一类问题,每个物品有两个代价 c_i,d_i,要求通过某种方式选出若干个,使得 \\frac{\\sum{c_i}}{\\sum{d_i}} 最大或最小。
+分数规划是这样一类问题,每个物品有两个代价 $c_i$,$d_i$,要求通过某种方式选出若干个,使得 $\frac{\sum{c_i}}{\sum{d_i}}$ 最大或最小。
 
 经典的例子是 最优比率环、最优比率生成树 等等。
 
-二分法
-
-比如说我们要求的是最小的,记 L 为最优的答案,对这个式子做一些变换:
-
-L \\geq \\frac{\\sum{c_i}}{\\sum{d_i}}
-
-把分母乘过去,把右侧化为 0:
-
-{\\sum{d_i}} \\times L - {\\sum{c_i}} \\geq 0
+### 二分法
 
+比如说我们要求的是最小的,记 $L$ 为最优的答案,对这个式子做一些变换:
+$$
+L \geq \frac{\sum{c_i}}{\sum{d_i}}
+$$
+把分母乘过去,把右侧化为 $0$:
+$$
+{\sum{d_i}} \times L - {\sum{c_i}} \geq 0
+$$
 即:
+$$
+{\sum_{i=1}^N{d_i}} \times L - {\sum_{i=1}^N{c_i}} \geq 0
+$$
 
-{\\sum_{i=1}^N{d_i}} \\times L - {\\sum_{i=1}^N{c_i}} \\geq 0
-
-\\sum\_{i=1}^N{d_i \\times L - c_i} \\geq 0
+$$
+\sum_{i=1}^N{d_i \times L - c_i} \geq 0
+$$
 
-不难发现,如果 L' 比 L 要小,上式左端的值会更大一些。
+不难发现,如果 $L'$ 比 $L$ 要小,上式左端的值会更大一些。
 
-所以要求得最小的 L,我们要求的就变成了让上式左端最接近 0 的 L
+所以要求得最小的 $L$,我们要求的就变成了让上式左端最接近 $0$ 的 $L$
 
-不难发现左端的式子是随 L 变化而单调变化的,所以可以通过二分法来解决。
+不难发现左端的式子是随 $L$ 变化而单调变化的,所以可以通过二分法来解决。
 
-Dinkelbach 算法
+### Dinkelbach 算法
 
-Dinkelbach 算法是每次用上一轮的答案当做新的 L 来输入,不断地迭代,直至答案收敛。
+Dinkelbach 算法是每次用上一轮的答案当做新的 $L$ 来输入,不断地迭代,直至答案收敛。