二分与三分
问题引入
假如计算机随机生成了一个在 \([1,100]\) 之间的数字 \(x\),由你来猜,但是只给你 \(8\) 次机会,每次会提醒你猜的数比生成的数大还是小,你如何保证一定能在 \(8\) 次以内猜中?
-
策略一:从小往大依次猜 \(1,2,3,\dots\),最坏情况下,如果数字是 \(100\),你需要猜 \(100\) 次!
-
策略二:从大往小猜,情况如上,最坏情况是 \(1\),也需要猜 \(100\) 次!
-
策略三:随机猜,更不好把握次数!
我们考虑这个问题的时候,不妨反过来想,如何能够有把握地淘汰尽可能多的数?如果每次我们都猜正中间的数字,则我们一定能淘汰掉一半!
- 策略四:对于当前剩下的区间 \([l,r]\),我们猜第 \(mid=(l+r)/2\) 个数字
- 如果 \(a[mid]<x\),则淘汰掉 \([l,mid]\) 区间内所有的数,在 \([mid+1, r]\) 区间内继续猜;
- 如果 \(a[mid]>x\),则淘汰掉 \([mid, r]\) 区间内所有的数,在 \([l,mid-1]\) 区间内继续猜;
- 如果 \(a[mid]==x\),结束。
这种策略,我们称为“二分查找”,由于每次都可以使查询的区间缩小一半,所以对于 \(n\) 个元素的区间,其查询效率为 \(O(log\ n)\)。
大家需要注意,使用“二分查找”的前提是:数组是有序的,通常在使用前可能需要先排序。
二分查找
二分查找是一个基础的算法,也叫折半查找。
二分查找就是将查找的数值和子数组的中间值做比较:
如果被查找的键等于中间值,找到元素,算法结束;
如果被查找的键小于中间值,就在左子数组中继续查找;
如果被查找的键大于中间值,就在右子数组中继续查找;
所谓中间值,就是此次查找区间内位于正中间的数值,如区间为[l,r],则中间值的位置为
(l+r)/2。
下面是一个查找字母 P 和 Q 的过程:

例:输入一个 \(100\) 以内的整数 \(k\),通过二分查找猜出 \(k\) 的值,看看用了几次。
#include <iostream>
int a[107];
int main() {
for (int i = 1; i <= 100; ++i)
a[i] = i;
int k;
scanf("%d", &k);
int lo = 1, hi = 100, cnt = 0;
while (lo <= hi) {
int mid = (lo + hi) >> 1;
printf("%d: %d\n", ++cnt, mid);
if (a[mid] == k) return 0;
if (a[mid] < k) lo = mid + 1;
else hi = mid - 1;
}
return 0;
}
- 有时,我们并非二分查找某个具体的值,而是去查找最大数的最小值,或最小数的最大值,这时候,我们通常只有在二分查找结束的时候才能获取数值。
#include <iostream>
int a[107];
bool valid(int k) {
// 判断k是否是合法的,如果合法返回true,否则返回false
}
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
int left = 1, right = 100;
while (left <= right) {
int mid = (left + right) >> 1;
// 求满足条件的最大值
if (valid(mid)) left = mid + 1;
else right = mid - 1;
}
printf("%d\n", right);
return 0;
}
#include <iostream>
int a[107];
bool valid(int k) {
// 判断k是否是合法的,如果合法返回true,否则返回false
}
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
int left = 1, right = 100;
while (left <= right) {
int mid = (left + right) >> 1;
// 求满足条件的最小值
if (valid(mid)) right = mid - 1;
else left = mid + 1;
}
printf("%d\n", left);
return 0;
}
简单地记作:求最大数的最小值是l,求最小数的最大值是r
《算法进阶指南》
书中李煜东给出了另一种颇为巧妙的方法:
//在单调递增序列 a 中查找 >=x 的数中最小的一个(即 x 或 x 的后继)
while (l < r) {
int mid = (l + r) >> 1;
if (a[mid] >= x) r = mid;
else l = mid + 1;
}
return a[l];
//在单调递增序列 a 中查找 <=x 的数中最大的一个(即 x 或 x 的前驱)
while (l < r) {
int mid = (l + r + 1) >> 1;
if (a[mid] <= x) l = mid;
else r = mid - 1;
}
return a[l];
不难发现:mid = (l+r) >> 1 不会取到 r;mid = (l + r + 1) >> 1 不会取到 l。
我们可以利用这一性质来处理无解的情况,把最初的二分区间 \([1,n]\) 分别扩大为 \([1,n+1]\) 和 \([0,n]\),把 \(a\) 数组的一个越界的下标包含进来。如果最后二分终止于扩大后的这个越界下标上,则说明 \(a\) 中不存在所求的数。
总而言之,正确写出这种二分的流程是:
-
通过分析具体问题,确定左右半段哪一个是可行区间,以及
mid归属哪一半段; -
选择
mid = (l + r) >> 1, r = mid, l = mid + 1和mid = (l + r + 1) >> 1, r = mid - 1, l = mid两个配套形式之一; -
二分终止条件是
l == r,该值就是答案所在位置。
本书使用的这种二分方法的优点是:
始终保持答案位于二分区间内,二分结束条件对应的值恰好在答案所处位置,还可以很自然地处理无解的情况,形式优美。唯一的缺点是由两种形式共同组成,需要认真考虑实际问题选择对应的形式。
STL中的二分查找
需要 #include <algorithm>
lower_bound(spos, endpos+1, value):返回 \([spos, endpos]\) 中第一个大于等于 \(value\) 的地址(不是数组下标);upper_bound(spos, endpos+1, value):返回 \([spos, endpos]\) 中第一个大于 \(value\) 的地址(不是数组下标);
如果想要得到数组下标,需要减去数组的首地址。
其他二分写法
初赛中的“阅读程序”与“完善程序”可能会有关于二分的代码,因此多接触不同的二分写法是有必要的。
三分查找介绍
在区间内用 两个 mid 将区间分成三份,这样的查找算法称为三分查找,也就是三分法,三分法常用于求解单峰函数的最值。
二分查找所面向的搜索序列的要求是:具有单调性(不一定严格单调);没有单调性的序列不能使用二分查找。
与二分查找不同的是,三分法所面向的搜索序列的要求是:序列为一个 凸性函数。通俗来讲,就是该序列 必须有一个最大值(或最小值),在最大值(最小值)的左侧序列,必须满足不严格单调递增(递减),右侧序列必须满足不严格单调递减(递增)。如下图,表示一个有最大值的凸性函数:

如图所示,已知左右端点 \(L,R\),要求找到蓝点的位置。
思路:通过不断缩小 [L,R] 的范围,无限逼近蓝点。
做法:
先取
[L,R]的中点mid;再取
[mid,R]的中点mmid。通过比较
f(mid)与f(mmid)的大小来缩小范围,当最后L=R-1时,再比较下这两个点的值,我们就找到了答案。
- 当
f(mid) > f(mmid)的时候,我们可以断定mmid一定在蓝点的右边。
反证法:
假设 mmid 在蓝点的左边,则 mid 也一定在蓝点的左边,又由 f(mid) > f(mmid) 可推出 mmid < mid,与已知矛盾,故假设不成立。
所以,此时可以将 R = mmid 来缩小范围。
- 当
f(mid) < f(mmid)的时候,我们可以断定mid一定在蓝点的左边。
反证法:
假设 mid 在蓝点的右边,则 mmid 也一定在蓝点的右边,又由 f(mid) < f(mmid) 可推出 mid > mmid,与已知矛盾,故假设不成立。
同理,此时可以将 L = mid 来缩小范围。
int SanFen(int l,int r) {//找凸点
while(l < r-1) {
int mid = (l+r)/2;
int mmid = (mid+r)/2;
if( f(mid) > f(mmid) )
r = mmid;
else
l = mid;
}
return f(l) > f(r) ? l : r;
}
double 类型的三分也基本相同
算法介绍
- 先将区间三分,每个区间的长度为 1/3(right-left)
mid1 = left + (right-left)/3, mid2 = right - (right-left)/3;
- 比较 mid1 和 mid2 谁更靠近极值,如果 mid1 更靠近极值,右区间改为 mid2,否则左区间改为 mid1 (后面的代码都是以求最大值为例)
if (calc(mid1) < calc(mid2)) left = mid1;
else right = mid2;
- 重复 1,2 过程,直到不满足
left + EPS < right,也就是找到最值(EPS=epsilon)。
const double EPS = 1e-6;//0.000001
while (l + EPS < r) {
ml = l + (r-l) / 3;
mr = r - (r-l) / 3;
if (calc(ml) < calc(mr)) l = ml;
else r = mr;
}
还有一种写法,就是两次二分。
double calc(double a) {}
void solve() {
double l, r, ml, mr;
l = MIN, r = MAX;
while (l + EPS < r) {
ml = (l + r) / 2;
mr = (ml + r) / 2;
if (calc(ml) >= calc(mr)) r = mr;
else l = ml;
}
}
如何判断一个函数是单峰函数?
在信息学中,一个区间内,零个或一个偶函数与零个或一个单调函数的复合,通常构成的就是一个单峰函数。注意:单调函数本身也是单峰函数。
#include<cstdio>
const double EPS = 1E-12;
int n;
double a[20];
double solve(double x) {
double sum = 0;
//x^3-3x^2-3x+1=((x-3)*x-3)*x+1
for(int i = 1; i <= n; ++i) {
sum = sum + a[i];
if (i < n) sum *= x;
}
return sum;
}
int main() {
double l, r;
scanf("%d%lf%lf", &n, &l, &r); ++n;
for(int i = 1; i <= n; ++i)
scanf("%lf", a + i);
while(r-l >= EPS) {
double ml = (l + r) / 2;
double mr = (ml + r) / 2;
if(solve(ml) - solve(mr) >= EPS)
r = mr;
else
l = ml;
}
printf("%.5lf\n", l);
return 0;
}
此题需要在二个维度上嵌套三分。

#include <cstdio>
#include <cmath>
#include <algorithm>
typedef std::pair<double, double> Loc;
Loc a, b, c, d;
#define x first
#define y second
const double EPS = 1e-5;
double p, q, r;
double dis(Loc x1, Loc x2) {
return sqrt((x1.x-x2.x)*(x1.x-x2.x) + (x1.y-x2.y)*(x1.y-x2.y));
}
Loc find(Loc x1, Loc x2, double k) {
//通过比例算出具体点的坐标位置
Loc p;
p.x = (x2.x-x1.x)*k + x1.x;
p.y = (x2.y-x1.y)*k + x1.y;
return p;
}
double get_dis(double x, double y) {
//在确定AB上及CD上的位置后,计算出实际路程长度
Loc x1 = find(a, b, x), x2 = find(c, d, y);
return dis(a, x1)/p + dis(x1, x2)/r + dis(x2, d)/q;
}
double calc(double x) {
////对CD段的位置进行比例三分查找
double l = 0, r = 1;
while (l + EPS <= r) {
double ml = l + (r-l)/3, mr = r - (r-l)/3;
if (get_dis(x, ml) > get_dis(x, mr)) l = ml;
else r = mr;
}
return get_dis(x, l);
}
int main() {
scanf("%lf%lf%lf%lf", &a.x, &a.y, &b.x, &b.y);
scanf("%lf%lf%lf%lf", &c.x, &c.y, &d.x, &d.y);
scanf("%lf%lf%lf", &p, &q, &r);
double l = 0, r = 1;
while (l + EPS <= r) {
//对AB段的位置进行比例三分查找
double ml = l + (r-l)/3, mr = r - (r-l)/3;
if (calc(ml) > calc(mr)) l = ml;
else r = mr;
}
printf("%.2f", calc(l));
return 0;
}

浙公网安备 33010602011771号