参考资料:
算法竞赛进阶指南
前言:(摘自算法竞赛进阶指南)
二分法是一种随处可见,却非常精妙的算法,经常能为我们打开解决问题的突破口。二分的基础的用法是在单调序列或单调函数中进行查找。因此当问题的答案具有单调性时,就可以通过二分把求解转化为判定(根据复杂度理论,判定的难度小于求解),这使得二分的运用范围变得更广泛,进一步地,我们还可以扩展到通过三分法去解决单峰函数的极值以及相关问题。
据说只有10%的程序员能写对二分。二分的实现方法多种多样,但是其细节之处确实需要仔细考虑。对于整数域上的二分,需要注意终止边界,左右区间取舍时的开闭情况,避免漏掉答案或者造成死循环,对于实数域上的二分,需要注意精度问题。一般来说,熟练的掌握自己的一种正确写法即可。
二分基础:
二分查找有序数组内的元素:
二分最最基本的作用就是在一个有序的数组中,找到你所需要的关键字。下面就用几张表来展示一下这个过程。
比如给你一个数组(元素为:1 3 4 7 15 23 31 102 102),让你去找到第一个102对应的下标值
我们用L,R来记录还有可能出现待查找关键字的区间[L,R],用mid记录区间的中点,用ans来记录已经找到的答案(如果还没找到就是-1)。则查找过程如下:
初始的情况下,102可能出现的区间为[0,8]
通过比较mid和102,102可能出现的区间为[5,8]
通过比较mid和102,102可能出现的区间为[7,8]
通过比较mid和102,发现找到了答案,因为我们需要找第一个102,所以可能存在答案的区间就是当前区间的前半段。
区间变为[7,6],不存在该区间,查找完成。
怎么样,在数组中的查找是不是很简单。类比可知,求最后一个102的方法。
二分查找有序数组内的元素的代码:
二分有很多种写法,这儿我们介绍一种祖传算法(好像学长们一直都是这么写的),它的好处是很好记忆,好理解,然后查找第一个等于x的元素和查找最后一个等于x的元素代码差距很小,只需要改动一点点。
查找最后一个等于x的元素:
int search(const int *a, int n, int key) {
int l = 0, r = n - 1, mid = 0, ans = 0;
while (l <= r) {
mid = (l + r) / 2;
if (a[mid] <= key) {
l = mid + 1;
ans = mid;
} else
r = mid - 1;
}
return a[ans] == key ? ans : -1;
}
查找第一个等于x的元素:
int search(const int *a, int n, int key) {
int l = 0, r = n - 1, mid = 0, ans = 0;
while (l <= r) {
mid = (l + r) / 2;
if (a[mid] < key)
l = mid + 1;
else {
ans = mid;
r = mid - 1;
}
}
return a[ans] == key ? ans : -1;
}
实数域上的二分:(注意精度问题)
const double EPS = 1e-5;
double search(const double *a, int n) {
double l = 0, r = n - 1, mid = 0, ans = 0;
while (r - l >= EPS) {
if (check(mid))//判定函数
r = mid;
else
l = mid;
}
return r;
}
二分解决算法问题:
那么,二分在算法竞赛中应该如何去使用呢?二分是一种很巧妙的方法,正如前言所说的,二分可以把求解转化为判定,这样我们就可以大大简化掉一些问题的复杂程度。就比如让你验证一下这个问题算的对不对要比让你求解这个问题简单的多。
那么,二分可以解决什么样的问题呢?
一个宏观的最优化问题也可以抽象为函数,其定义域是该问题下的可行方案,对这些可行方案进行评估得到的数值构成函数的值域,最优解就是评估值最优的方案(不妨设评分越高越优)。假设最优解的评分是S,显然对于所有>S的值,都不存在一个合法的方案到达该评分,否则就与S的最优性矛盾,而对于所有<S的值,一定存在一个合法的方案达到或超过该评分,因为最优解就满足这个条件。
这样问题的值域就具有一种特殊的单调性——在S的一侧合法,在S的另一侧不合法,就像一个在S左侧为0,在S右侧为1的分段函数,可通过二分找到这个S,借助二分,我们把求最优解的问题,转化为给定一个值mid,判定是否存在一个可行方案评分达到mid的问题。
看不懂没关系,下面我们用一个简单的例子来讲解一下。
经典例题:HDU - 1969
题目:
LZH过生日请了f 个朋友来参加生日party,买了m个蛋糕(每个蛋糕都是高度相同,大小不同的圆柱),准备把它平均分给每个人(包括LZH自己),蛋糕可以有剩余,但是每个人只能从同一块蛋糕得到自己的那一份,并且分得的蛋糕体积要一样(形状可以不一样)。问每个人分得的蛋糕的最大的体积是多少。
题目思路:
假设每个人可以分得的蛋糕的最大体积是ans,那么如果分给每个人的蛋糕体积小于ans,那么这个方案一定是可行的(QAQ显然)。如果分给每个人的蛋糕体积大于ans,那么这个方案一定是不可行的(否则ans就不是最优解了),这样ans就是可行与不可行之间的一个分界点,就可以用二分来求解这个问题。
至于怎么判断这个方案可不可行,这就是一个很简单的问题了吧,给定一个mid,计算一个每一个蛋糕可以分成几块,然后累加,得到这些蛋糕一共可以分成多少块,假如说一共可以分成x块,那么如果x>=f+1,那么,这个方案就是可行的,每个人都会有蛋糕吃,否则,方案就是不可行的,有人会吃不到蛋糕。
这个题对精度的要求很高,所以写PI的时候要注意精度问题。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
const int MAXN = 10000 + 10;
const double PI = acos(-1), EPS = 1e-6;
double pie[MAXN];
int n, f;
bool Check(double k) {
int pienum = 0;
for (int i = 0; i < n; i++)
pienum += floor(pie[i] / k);
return pienum >= f + 1;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d %d", &n, &f);
double temp, l = 0, r = 0, mid = 0;
for (int i = 0; i < n; i++) {
scanf("%lf", &temp);
pie[i] = temp * temp * PI;
r = max(r, pie[i]);
}
while (fabs(r - l) >= EPS) {
mid = (l + r) / 2;
if (Check(mid))
l = mid;
else
r = mid;
}
printf("%.4f\n", l);
}
return 0;
}
练习题目:(简单题 难度比较大的可以自行百度)
POJ - 2018 Best Cow Fences
LuoguP2678 跳石头
LuoguP1024 一元三次方程求解
POJ - 3685 Matrix
总结:
二分可以用来求解那种具有单调性的最优化问题
最大值最小化,最小值最大化的问题也经常用二分来解决
二分可以用来优化枚举