算法与数据结构 2 - 二分

二分

介绍

二分是信息学中运用的较为广泛的一种思想。它的核心是每次操作去掉一半的错误答案,从而在 \(\text{log}_2n\)(在信息学中简称 \(\text{log}n\))的操作次数内查找到答案。

题外话:算法的复杂度

计算机也许足够快,但并非无限快。 ————《算法导论》

现代计算机的运算速度已经达到了很高的水平,但它并不是无限快的。也许 \(2\) 个不同的算法都能解决某个问题,但其中一个运行时间及长或占用内存空间极大,那么在工程中通常会使用另一个更加优秀的算法。复杂度就是为了衡量一个算法的效率而产生的。

在信息学领域,通常使用的是时间复杂度和空间复杂度。它们表示了算法的用时或占用空间随数据规模而增长的趋势。通常使用 \(O(x)\) 记号表示。\(x\) 表示算法执行基本操作(如加减乘除、访问内存等)的规模。

为了简化,计算复杂度通常只会考虑算法耗时最长的一部分,同时省略常数。例如,一个算法存储并遍历了一个长度为 \(n\) 的数组,则它的时间复杂度为 \(O(n)\),空间复杂度为 \(O(n)\)
其他内容可参考:复杂度简介

引入

二分思想的经典应用是在有序数组中查找某一元素。

给定一个升序排列的数组 \(a\),长度为 \(n\),以及元素 \(x\)。求 \(x\) 是否在 \(a\) 中。

这道问题有一个显然的做法:访问 \(a\) 中的每一个元素,检查它们是否和 \(x\) 相等。但这种方法最坏情况下需要查找 \(n\) 次,即时间复杂度为 \(O(n)\),不够优秀,当它运行在普通家用计算机上(每秒执行基本操作次数约为 \(5\times 10^8\) 次)时,对于 \(n \le 10^{10}\) 的数据无法在 \(1\) 秒内完成。

注意到 \(a\) 是升序的,因此在查找时,我们并不需要检查 \(a\) 中的每一个元素:若已知 \(a_i < x\),则对于 \(1\le j < i\),均有 \(a_j < x\)。因此并不需要检查这些元素就能直接得知它们不符合要求。

而什么时候能排除最多的数据呢?显然,最优策略是判断 \(a\) 中间的元素 \(a_{\text{mid}}\)。因为显然无论如何,都至少能排除 \(\frac{n-1}{2}\) 个不等于 \(x\) 的数据。

因此,二分查找算法就呼之欲出了:

long long binary_search(long long x) {
  long long ret = -1, l = 1, r = n; //  未搜索到则返回 -1;已知 a[l] ≤ x ≤ a[r]
  while (l <= r) {
    long long mid = (l + r) / 2; // 区间的中点
    if (a[mid] < x)
      l = mid + 1; // 排除 [l, mid]
    else if (a[mid] > x)
      r = mid - 1; // 排除 [mid, r]
    else { // 找到答案
      ret = mid;
      break;
    }
  }
  return ret;
}

进阶

二分只是一种思想,因此它的作用远远不止在数组中查找元素。

从上面的例子中可以看出,想要使用二分,数据必须满足单调性,才可以通过中点排除至少一半的区间。若讲满足某种条件看成 \(1\),不满足看成 \(0\),则很多求区间最值的问题可以看做满足单调性。

总结一下,若想在某一类问题中使用二分,有前提条件:

  1. 答案在一个固定区间内;
  2. 可能查找一个符合条件的值不是很容易,但是要求能比较容易地判断某个值是否是符合条件的(否则二分的效率甚至不如暴力枚举);
  3. 可行解对于区间满足一定的单调性。换言之,如果 \(x\) 是符合条件的,那么有 \(x+1\) 或者 \(x-1\) 也符合条件。(这样下来就满足了上面提到的单调性)

例题:[COCI 2011/2012 #5] EKO / 砍树

伐木工人 Mirko 需要砍 \(M\) 米长的木材。对 Mirko 来说这是很简单的工作,因为他有一个漂亮的新伐木机,可以如野火一般砍伐森林。不过,Mirko 只被允许砍伐一排树。
Mirko 的伐木机工作流程如下:Mirko 设置一个高度参数 \(H\)(米),伐木机升起一个巨大的锯片到高度 \(H\),并锯掉所有树比 \(H\) 高的部分(当然,树木不高于 \(H\) 米的部分保持不变)。Mirko 就得到树木被锯下的部分。例如,如果一排树的高度分别为 \(20,15,10\)\(17\),Mirko 把锯片升到 \(15\) 米的高度,切割后树木剩下的高度将是 \(15,15,10\)\(15\),而 Mirko 将从第 \(1\) 棵树得到 \(5\) 米,从第 \(4\) 棵树得到 \(2\) 米,共得到 \(7\) 米木材。
Mirko 非常关注生态保护,所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助 Mirko 找到伐木机锯片的最大的整数高度 \(H\),使得他能得到的木材至少为 \(M\) 米。换句话说,如果再升高 \(1\) 米,他将得不到 \(M\) 米木材。

可以发现,对于高度 \(h\) 可以获得的木材一定不多于 \(h-1\)。即:设高度为 \(h\) 可以获得的木材为 \(f(h)\),则该函数满足单调不上升。因此这道题就满足单调性,可以用二分法进行求解。

另外,由于这道题的数据范围较大,暴力判断每一个 \(h\) 是否满足要求无法在规定的时间内完成(后文简称“超时”),于是使用更加优秀的二分法。

#include <bits/stdc++.h>
using namespace std;
long long n, m, a[1000001];
long long maxn;
bool check(long long mid) {
	long long res = 0;
	for (long long i = 0; i < n; i++) {
		if (a[i] > mid) res += a[i] - mid;
	}
	return res >= m;
}
int main() {
	scanf("%lld%lld", &n, &m);
	for (int i = 0; i < n; i++) {
		scanf("%lld", a + i);
		maxn = max(maxn, a[i]);
	}
	long long l = 0, r = maxn;
	while (l < r) {
		long long mid = (l + r) >> 1;
		if (check(mid)) {
			l = mid + 1;
		} else {
			r = mid;
		}
	}
	printf("%lld\n", l - 1);
	return 0;
}

于是我们就飞快地通过了这道题。

结束语

二分是应用广泛的一种思想。这种思想很有用,不仅在信息学,更是在其他领域中大放异彩。

作业

  1. [NOIP1999 提高组] 导弹拦截
  2. A-B 数对
posted @ 2024-11-26 10:31  cakn  阅读(22)  评论(0编辑  收藏  举报