三分算法的简单应用
- 三分算法
-
- 三分算法求函数极值
- [P1883 Error Curves - 洛谷](#P1883 Error Curves - 洛谷)
- [P5931 灯泡 - 洛谷](#P5931 灯泡 - 洛谷)
- [P2571 传送带 - 洛谷](#P2571 传送带 - 洛谷)
- OJ参考
三分算法
二分法在单调函数上查找特定值或在有序数组中搜索目标,依赖于函数在区间内具有单调性,从而能够通过一次比较确定目标所在的半区间。
但很多问题需要求凹函数(或凸函数)的极值,凹函数在极值点两侧,函数的单调性相反:一侧递增,另一侧递减。此时用二分法取中点时,无法通过比较中点处的函数值与某个参考值来判断极值点位于中点的左侧还是右侧,因为函数值的大小关系并不能唯一确定方向。
三分法适用于求解凸性函数的极值问题 ,二次函数就是一个典型的单峰函数。 三分法与二分法一样,它会不断缩小答案所在的求解区间。二分法缩小区间利用的原理是函数的单调性,而三分法利用的则是函数的单峰性。
至于为什么不用求导的方式求极值,是因为某些问题中需要求极值点的单峰函数并非一个单独的函数,而是多个函数进行特殊运算得到的函数,甚至是抽象函数或分段函数。此时在函数某些点上可能不可导。
例如求多个单调性不完全相同的一次函数组成的分段函数的最小值的最大值就不能用求导来求解。
三分算法求函数极值
设当前求解的区间为[l,r],令m1 =l+(r-l)/3,m2=r-(r-l)/3,接着计算这两个点的函数值f(m1),f(m2) 之后将两点中函数值更优的那个点称为好点(若计算最大值,则 f(m) 更大的那个点 就为好点,计算最小值同理),而函数值较差的那个点称为坏点。
可以证明,最优点与好点会与坏点同侧。如图所示:

f(m1)>f(m2),则m1是好点,而m2是坏点,因此最后的最优点会与m1 一起在m2的左侧,即我们的求解区间由[l,r]变为了[l,m2]。因此根据这个结论我们可以不停缩小求解区间,直至得出近似解。 与二分一样,我们可以指定三分的次数,或是根据r-l的值来终止算法。
以求上凸单峰函数的最大值为例,三分模板:
cpp
double find(double l, double r) {
const double dlt = 1e-10;
while (r - l > dlt) {
double m1 = l + (r - l) / 3;
double m2 = r - (r - l) / 3;
if (f(m1) < f(m2)) // m1比m2更靠近极值
l = m1;
else
r = m2;
}
return f(l); // 极值点,这里l==r
}
三分算法也可以用在单调函数上,但求得的是 2 个端点的值。常出现的题目大致有求某个拥有具体解析式和定义域的函数的极值和优化枚举,其中的函数需要现场分析和推理,很考验个人的数学能力。
P1883 Error Curves - 洛谷
P1883 【模板】三分 / 函数 / [ICPC 2010 Chengdu R Error Curves - 洛谷](https://www.luogu.com.cn/problem/P1883)
一堆二次函数组合在一起的叠加函数,因为 a > 0 a>0 a>0 ,所以在[0,1000]可能单调递增,也可能是先单调递减,后单调递增。但无论是什么情况,这个函数在[0,1000]都可以看做凹函数或单调函数,然后对这个函数求最小值,可以尝试三分。
cpp
#include <bits/stdc++.h>
using namespace std;
const double dlt = 1e-11;
vector<double> a, b, c;
int n;
inline double f(double x) {
double ans = a[1] * x * x + b[1] * x + c[1];
for (int i = 1; i < a.size(); i++)
ans = max(ans, a[i] * x * x + b[i] * x + c[i]);
return ans;
}
void ac() {
cin >> n;
a.resize(n + 1, 0);
b = c = a;
for (int i = 1; i <= n; i++)
cin >> a[i] >> b[i] >> c[i];
double l = 0, r = 1000.0;
while (r - l > dlt) {
double m1 = f(l + (r - l) / 3);
double m2 = f(r - (r - l) / 3);
if (m1 <= m2) // 凹函数,m1比m2更接近极值
r = r - (r - l) / 3;
else
l = l + (r - l) / 3;
}
printf("%.4lf\n", f(r));
}
int main() {
// freopen("in.in", "r", stdin);
int T = 1;
cin >> T;
while (T--)
ac();
return 0;
}
P5931 灯泡 - 洛谷
P5931 [清华集训 2015 灯泡 - 洛谷](https://www.luogu.com.cn/problem/P5931)
题图如图所示:

但实际上根据 D D D 和 h h h 的变化会出现不同的情况。
当影子仅出现在地面时,如图所示:

根据三角形的相似性,有 L D = L L + x = h H \frac{L}{D}=\frac{L}{L+x}=\frac{h}{H} DL=L+xL=Hh ,交叉相乘再移项得 L = h H − h x L=\frac{h}{H-h}x L=H−hhx 。随着 x x x 增大,当 x + L = D x+L=D x+L=D 时,影子 L L L 最长,否则影子就会有一部分到墙上。
影子 L L L 最长时, x = D − L = H − h H D x=D-L=\frac{H-h}{H}D x=D−L=HH−hD 。也就是说,当影子在地面上时, x = H − h H D x=\frac{H-h}{H}D x=HH−hD 。
当影子出现在墙壁时,如图所示:

根据相似三角形的性质,有
e d f g = D − x D \frac{ed}{fg}=\frac{D-x}{D} fged=DD−x
即 h − L + D − x H − L + D − x = D − x D \frac{h-L+D-x}{H-L+D-x}=\frac{D-x}{D} H−L+D−xh−L+D−x=DD−x 。
2 边同时乘以 2 个分母的乘积得:
h D − L D + D 2 − D x = ( H + D − x ) ( D − x ) − L ( D − x ) hD-LD+D^2-Dx=(H+D-x)(D-x)-L(D-x) hD−LD+D2−Dx=(H+D−x)(D−x)−L(D−x)
将要求的答案 L L L 移项到左边,其余移项到右边得:
L = ( H + D − x ) ( D − x ) − h D − D 2 + D x − x = H D − H x + D 2 − D x − D x + x 2 − h D − D 2 + D x − x = H D − H x − D x + x 2 − h D − x = − x − ( H − h ) D x + H + D \begin{aligned}L=&\frac{(H+D-x)(D-x)-hD-D^2+Dx}{-x}\\=&\frac{HD-Hx+D^2-Dx-Dx+x^2-hD-D^2+Dx}{-x}\\=&\frac{HD-Hx -Dx+x^2-hD }{-x}\\=&-x-\frac{(H-h)D}{x}+H+D\end{aligned} L====−x(H+D−x)(D−x)−hD−D2+Dx−xHD−Hx+D2−Dx−Dx+x2−hD−D2+Dx−xHD−Hx−Dx+x2−hD−x−x(H−h)D+H+D
L = − x − ( H − h ) D x + H + D L=-x-\frac{(H-h)D}{x}+H+D L=−x−x(H−h)D+H+D 是一个对勾函数,在 x ∈ 0 , + ∞ x\in 0,+\\infty x∈0,+∞ 是凹函数。
题目要求最大的影子长度,则函数 L L L 的定义域应为 H − h H D , D \\frac{H-h}{H}D,D HH−hD,D ,在这个范围内, L L L 函数要么是单调函数,要么是凹函数,但无论是哪一个,都能使用三分算法求得极值。
P5931 [清华集训 2015 灯泡 - 洛谷](https://www.luogu.com.cn/problem/P5931) 和1438:灯泡 参考:
cpp
#include <bits/stdc++.h>
using namespace std;
double f(double H, double h, double D, double x) {
return (-x - ((D * (H - h)) / x) + H + D);
}
int main() {
// freopen("in.in", "r", stdin);
int T = 0;
cin >> T;
while (T--) {
double H, h, D;
cin >> H >> h >> D;
double L = (H - h) * D / H, R = D;
while (R - L > 1e-9) {
double m1 = L + (R - L) / 3, m2 = R - (R - L) / 3;
if (f(H, h, D, m1) < f(H, h, D, m2))
L = m1; // m2更靠近极值点
else
R = m2;
}
printf("%.3lf\n", f(H, h, D, L));
}
return 0;
}
P2571 传送带 - 洛谷
P2571 [SCOI2010 传送带 - 洛谷](https://www.luogu.com.cn/problem/P2571)
题目测试样例如图:

容易想到若点 E E E 固定,则 F F F 从 C C C 移动 到 D D D ,整体移动时间由长变短,再变长,这是一个单峰函数,此时可以用三分算法。
但因为 E E E 也是动点,所以也要对 E E E 进行枚举。但对 E E E 的轨迹用二分枚举的话就无法获得下一步行动,所以对 E E E 的轨迹也用三分。
这题可以求函数解析式,但最后的结果是个二元函数,同样是使用三分优化 E E E 和 F F F 的枚举,这里就不推导了。
参考程序:
cpp
#include <bits/stdc++.h>
using namespace std;
using f64 = double;
f64 ax, ay, bx, by, cx, cy, dx, dy, P, Q, R;
bool eq(f64 x, f64 y) {
return fabs(x - y) < 1e-9;
}
void midl(f64 &aim, f64 &x, f64 &y) { // 左端点收缩
aim = x + (y - x) / 3;
};
void midr(f64 &aim, f64 &x, f64 &y) { // 右端点收缩
aim = y - (y - x) / 3;
};
f64 dis(f64 x1, f64 y1, f64 x2, f64 y2) { // 计算2点间距
f64 x = x1 - x2, y = y1 - y2;
return sqrt(x * x + y * y);
}
f64 tim(f64 x1, f64 y1, f64 x2, f64 y2) { // 计算费时
f64 dab = dis(x1, y1, ax, ay);
f64 dcd = dis(x2, y2, dx, dy);
f64 d12 = dis(x1, y1, x2, y2);
return dab / P + dcd / Q + d12 / R;
}
f64 TCD(f64 x, f64 y) { // 枚举F点
f64 lx = cx, ly = cy, rx = dx, ry = dy;
f64 mlx, mly, mrx, mry;
while (!(eq(lx, rx) && eq(ly, ry))) {
midl(mlx, lx, rx), midl(mly, ly, ry);
midr(mrx, lx, rx), midr(mry, ly, ry);
if (tim(x, y, mlx, mly) < tim(x, y, mrx, mry))
rx = mrx, ry = mry; // ml更接近最优解
else
lx = mlx, ly = mly;
}
return tim(x, y, lx, ly);
}
f64 TAB() { // 枚举E点
f64 lx = ax, ly = ay, rx = bx, ry = by;
f64 mlx, mly, mrx, mry;
while (!(eq(lx, rx) && eq(ly, ry))) {
midl(mlx, lx, rx), midl(mly, ly, ry);
midr(mrx, lx, rx), midr(mry, ly, ry);
if (TCD(mlx, mly) < TCD(mrx, mry)) // ml更接近最优解
rx = mrx, ry = mry;
else
lx = mlx, ly = mly;
}
return TCD(mlx, mly);
}
int main() {
// freopen("in.in", "r", stdin);
cin >> ax >> ay >> bx >> by;
cin >> cx >> cy >> dx >> dy;
cin >> P >> Q >> R;
printf("%.2lf", TAB());
return 0;
}
这题解法很多,有模拟退火、暴力枚举、三分嵌套三分,这里只举例三分嵌套三分。
OJ参考
P1883 【模板】三分 / 函数 / [ICPC 2010 Chengdu R Error Curves - 洛谷](https://www.luogu.com.cn/problem/P1883)
P5931 [清华集训 2015 灯泡 - 洛谷](https://www.luogu.com.cn/problem/P5931)
P2571 [SCOI2010 传送带 - 洛谷](https://www.luogu.com.cn/problem/P2571)