单峰函数
单峰函数是指对于一个函数,在它的值域内,只有一个最大值或一个最小值,在极致两侧,函数保证单调,比如二次函数在实数范围内就是一个标准的单峰函数。
但是要注意的是,单峰函数不一定有一条对称轴,比如高中常见的对勾函数在 (0,+∞) 内就是一个单峰函数,但是它没有一条明确的对称轴。
三分查找
What is 三分?
三分是一种查找单峰函数最大或最小值的算法,它的思想和二分差不多,都是通过不断缩小范围来获得最终的答案的,先来简单介绍一下三分的实现过程:
已知某函数顶点的范围(或值域)为 [l,r],顶点坐标为 (m,n),求函数最大值,三分过程如下:
- 先在已经确定的值域内找到两个数 a 和 b(已知 a,b∈[l,r],假设 a<b)。
 
- 比较 f(a) 和 f(b) 的大小,夹逼区间。
 
如何夹逼区间是一个重要的问题,试考虑以下四种情况:
如果 f(a)<f(b),如图一或图三所示,那么,加入我们过于激进,直接让 l=b,对于图三情况,就会发现,m∈/[l,r],无法得到答案,如果我们挪动 r,那么就会发现,无论是让 r=a 还是 r=b,对于图一情况,也都会出现 m∈/[l,r],所以,唯一的能够保证夹逼区间正确的方式就是让 l=a。
另一种情况是 f(b)≤f(a),通过对于图二或者图四的尝试,可以知道我们应让 r=b。
注意:a 和 b 的选取方式有很多种,在这里推荐一种比较简单的选取方式,就是让 a=2l+r,b=2a+r。
浮点数三分
我们通过一个二次函数来模拟一下浮点数三分的实现过程:
现有二次函数 f(x)=x2−2x−3,因式分解,可转化为 f(x)=(x−3)(x+1),由焦点式很容易看出,该二次函数的最小值在 x=1 时取到,为 −4,那么,计算机并不知道二次函数的顶点坐标公式,也不知道如何因式分解,对于它来说,求解其顶点坐标的方式简单来说,就是猜。
假设已知该二次函数定点在 [−10,10] 之内,那么我们开始三分,过程如下:
| id | 
l | 
r | 
a | 
f(a) | 
b | 
f(b) | 
| #01 | 
−10.00 | 
10.00 | 
0.00 | 
−3.00 | 
5.00 | 
12.00 | 
| #02 | 
−10.00 | 
5.00 | 
−2.50 | 
8.25 | 
1.25 | 
−3.94 | 
| #03 | 
−2.50 | 
5.00 | 
1.25 | 
−3.94 | 
3.12 | 
0.52 | 
| #04 | 
−2.50 | 
3.12 | 
0.31 | 
−3.53 | 
1.72 | 
−3.48 | 
| #05 | 
−2.50 | 
1.72 | 
−0.39 | 
−2.07 | 
0.66 | 
−3.89 | 
| #06 | 
−0.39 | 
1.72 | 
0.66 | 
−3.89 | 
1.19 | 
−3.96 | 
| #07 | 
0.66 | 
1.72 | 
1.19 | 
−3.96 | 
1.46 | 
−3.79 | 
| #08 | 
0.66 | 
1.46 | 
1.06 | 
−4.00 | 
1.26 | 
−3.93 | 
| #09 | 
0.66 | 
1.26 | 
0.96 | 
−4.00 | 
1.11 | 
−3.99 | 
| #10 | 
0.66 | 
1.11 | 
0.89 | 
−3.99 | 
1.00 | 
−4.00 | 
| #11 | 
0.89 | 
1.11 | 
1.00 | 
−4.00 | 
1.05 | 
−4.00 | 
| #12 | 
0.89 | 
1.05 | 
0.97 | 
−4.00 | 
1.01 | 
−4.00 | 
| #13 | 
0.97 | 
1.05 | 
1.01 | 
−4.00 | 
1.03 | 
−4.00 | 
| #14 | 
0.97 | 
1.03 | 
1.00 | 
−4.00 | 
1.02 | 
−4.00 | 
| #15 | 
0.97 | 
1.02 | 
0.99 | 
−4.00 | 
1.01 | 
−4.00 | 
上述表格生成代码:
#include <bits/stdc++.h>
#define eps 0.01
using namespace std;
double f(double x) {
	return (x-3.)*(x+1.);
}
signed main() {
	double l=-10,r=10;
	int id=0;
	while(r-l>eps) {
		double a=(l+r)/2.;
		double b=(a+r)/2.;
		printf("#%02d l:%.2f r:%.2f a:%.2f f(a):%.2f0 b:%.2f f(b):%.2f\n",++id,l,r,a,f(a),b,f(b));
		if(f(a)>f(b)) l=a;
		else r=b;
	}
	return 0;
}
浮点数三分练习题:洛谷 P
整数三分
整数三分的实现过程类似于浮点数三分,是在知道取值范围的情况下,找到能使得答案(函数值)最大/最小的整数值,实现的框架与浮点数三分差别不大,只是有些关键点需要注意,我们以函数 y=(x−2.5)(x+1) 为例,加入我们知道它的最小值范围在 [−20,20] 之间,那么,程序最终会找到一个范围,[l,r] 表示函数的最小值可能出现的范围 [l,r],如下表:
| id | 
l | 
r | 
a | 
f(a) | 
b | 
f(b) | 
| #01 | 
−20 | 
20 | 
0 | 
−2.50 | 
10 | 
82.50 | 
| #02 | 
−20 | 
10 | 
−5 | 
30.00 | 
2 | 
−1.50 | 
| #03 | 
−5 | 
10 | 
2 | 
−1.50 | 
6 | 
24.50 | 
| #04 | 
−5 | 
6 | 
0 | 
−2.50 | 
3 | 
2.00 | 
| #05 | 
−5 | 
3 | 
−1 | 
−0.00 | 
1 | 
−3.00 | 
| #06 | 
−1 | 
3 | 
1 | 
−3.00 | 
2 | 
−1.50 | 
| #07 | 
−1 | 
2 | 
0 | 
−2.50 | 
1 | 
−3.00 | 
| #08 | 
0 | 
2 | 
1 | 
−3.00 | 
1 | 
−3.00 | 
| final range: [0,1] | 
 | 
 | 
 | 
 | 
 | 
 | 
生成代码:
// author: syl
// language: c++
#include <bits/stdc++.h>
#define eps 0.01
using namespace std;
double f(double x) {
	return (x-2.5)*(x+1.);
}
signed main() {
	int l=-20,r=20;
	int id=0;
	while(r-l>1) {
		int a=(l+r)/2;
		int b=(a+r)/2;
		printf("#%02d l:%d r:%d a:%d f(a):%.2f0 b:%d f(b):%.2f\n",++id,l,r,a,f(a),b,f(b));
		if(f(a)>f(b)) l=a;
		else r=b;
	} printf("final range: [%d,%d]\n",l,r);
	return 0;
}
在得到了一个最终的答案范围 [l,l+1] 时,就很容易得到最小值了,具体做法就是判断一下 f(l) 和 f(l+1) 的大小,然后返回就可以了,所以完整代码如下:
#include <bits/stdc++.h>
#define eps 0.01
using namespace std;
double f(double x) {
	return (x-2.5)*(x+1.);
}
signed main() {
	int l=-20,r=20;
	int id=0;
	while(r-l>1) {
		int a=(l+r)/2;
		int b=(a+r)/2;
		printf("#%02d l:%d r:%d a:%d f(a):%.2f0 b:%d f(b):%.2f\n",++id,l,r,a,f(a),b,f(b));
		if(f(a)>f(b)) l=a;
		else r=b;
	} printf("final range: [%d,%d]\n",l,r);
	printf("%d %.2lf\n",(f(l)>f(r)?r:l),min(f(l),f(r)));
	return 0;
}