三分与单峰函数
二分寻找单调序列的分界点,三分寻找单峰或单谷函数的极值点。
三分模板本身并不长,真正需要判断的是目标函数为什么只会先变好、再变坏。有些题的凸性直接写在函数式里,有些题要经过代价转换才能看出来;如果导数或离散差分具有单调性,还能把三分改写成二分。
这一篇从实数三分与整数三分开始,接着整理导数二分、离散斜率以及嵌套搜索。正文不依赖配图,重点放在单峰性的证明、边界处理和目标函数的构造。
三分到底在找什么
单峰函数与单谷函数
设函数$f(x)$在区间$[l,r]$上先单调递增,经过极值点$p$以后单调递减:
$$
x_1<x_2\le p\Longrightarrow f(x_1)\le f(x_2)
$$
$$
p\le x_1<x_2\Longrightarrow f(x_1)\ge f(x_2)
$$
这样的函数称为单峰函数,$p$是最大值点。
如果函数先减后增,则称为单谷函数,$p$是最小值点。凸函数通常对应单谷,凹函数通常对应单峰,但写题时不能只凭图像判断,仍然需要说明单调性为什么只改变一次。
二分在一个位置判断左右,需要这个判断本身单调;三分同时取两个位置,通过函数值的大小排除一侧区间。
两个三分点
在区间$[l,r]$中取:
$$
m_1=l+\frac{r-l}{3},\qquad
m_2=r-\frac{r-l}{3}
$$
显然有$l<m_1<m_2<r$。
先考虑单谷函数,也就是寻找最小值。如果$f(m_1)\le f(m_2)$,最小值不可能严格位于$m_2$右侧。否则函数从$m_1$走到$m_2$已经没有变小,继续向右只会更大,因此可以令:
$$
r=m_2
$$
反过来,如果$f(m_1)>f(m_2)$,说明函数在这段仍然下降,最小值不可能位于$m_1$左侧,可以令:
$$
l=m_1
$$
求单峰函数最大值时,比较方向正好相反:
| 目标 | 条件 | 保留区间 |
|---|---|---|
| 最小值 | $f(m_1)\le f(m_2)$ | $[l,m_2]$ |
| 最小值 | $f(m_1)>f(m_2)$ | $[m_1,r]$ |
| 最大值 | $f(m_1)\le f(m_2)$ | $[m_1,r]$ |
| 最大值 | $f(m_1)>f(m_2)$ | $[l,m_2]$ |
三分不是根据$m_1$或$m_2$是否等于答案来移动,而是根据两点之间的走势排除不可能存在极值的一侧。
复杂度
每轮三分以后,候选区间至多保留原来的$\frac{2}{3}$。设初始区间长度为$V$,要求区间长度不超过$\varepsilon$,迭代次数满足:
$$
V\left(\frac{2}{3}\right)^K\le\varepsilon
$$
因此:
$$
K=O\left(\log_{3/2}\frac{V}{\varepsilon}\right)
$$
若一次计算$f(x)$需要$O(T)$,实数三分的总时间复杂度为:
$$
O\left(T\log\frac{V}{\varepsilon}\right)
$$
三分和二分都是对数级。三分每轮保留$\frac{2}{3}$,并且通常要计算两个函数值,所以常数会比二分大。
三种常用写法
实数三分
实数没有相邻位置,不能使用$mid\pm1$。求单谷函数最小值时,可以固定循环次数:
1 | for (int t = 1; t <= 100; t++) { |
循环结束以后,极值点仍然位于$[l,r]$中。通常取:
1 | double p = (l + r) / 2; |
如果题目要求极值点,输出$p$;如果要求极值,输出 calc(p)。
也可以写成:
1 | double eps = 1e-10; |
固定循环次数不需要反复调整 eps,也不会因为浮点数无法继续细分而陷入死循环,竞赛中通常更省心。
整数三分
整数三分最容易出现的问题是$m_1$与$m_2$重合。如果区间已经很短,继续三分可能不再缩小。
更稳妥的写法是先把区间缩小到常数长度,再直接枚举:
1 | while (r - l > 6) { |
最后枚举不是多余步骤。它同时解决了取整误差、平台区间以及最优点落在相邻整数之间的问题。
导数与离散差分
对于可导的凸函数$f(x)$,最小值左侧通常满足$f’(x)<0$,右侧满足$f’(x)>0$。如果导数的符号只改变一次,就可以二分第一个满足:
$$
f’(x)\ge0
$$
的位置:
1 | for (int t = 1; t <= 100; t++) { |
求凹函数最大值时,方向相反。这里二分的不是$f(x)$,而是导数符号形成的分界点。
整数函数没有普通导数,可以观察离散差分:
$$
\Delta f(x)=f(x+1)-f(x)
$$
对于单谷整数函数,极小值左侧有$\Delta f(x)<0$,右侧有$\Delta f(x)\ge0$。因此可以二分第一个满足$f(x)\le f(x+1)$的位置。
导数二分和三分的渐进复杂度相同,但二分每轮只保留一半区间,通常只计算一次导数或两个相邻函数值,常数更小。前提是导数或差分容易计算,而且符号变化确实单调。
嵌套搜索
三分套三分
如果目标函数包含两个连续变量,可以先固定一个变量,再优化另一个变量。
设$F(x,y)$关于$y$是单谷函数。固定$x$以后,内层三分得到:
$$
G(x)=\min_y F(x,y)
$$
如果$G(x)$关于$x$仍然单谷,外层就可以继续三分$x$。
两层搜索都进行$K$轮,单次计算$F(x,y)$需要$O(T)$,总时间复杂度为:
$$
O(K^2T)
$$
嵌套以前必须分别证明两层的单谷性。固定一个变量以后能三分,并不代表优化后的外层函数一定还能三分。
二分套三分
另一种常见结构是外层二分答案,内层三分求当前答案下的最优代价。
设我们要寻找最小的$X$。固定$X$后,需要计算:
$$
G(X)=\min_y F(X,y)
$$
如果$F(X,y)$关于$y$单谷,就用三分计算$G(X)$;如果$G(X)\le K$关于$X$单调,就在外层二分$X$。
结构可以概括为:
1 | 二分答案 X |
若外层范围长度为$V$,内层范围长度为$W$,一次计算代价需要$O(T)$,总时间复杂度为:
$$
O(T\log V\log W)
$$
例题与典型应用
下面七道题依次对应实数模板、整数三分、导数二分、整数凸代价、三分套线性扫描、三分套三分以及二分套三分。
| 题目 | 搜索对象 | 核心判定或函数 |
|---|---|---|
| 洛谷 P3382 三分 | 多项式最大值点 | 保证先增后减 |
| AtCoder ABC279 D Freefall | 操作次数 | 整数单谷函数 |
| AtCoder ARC054 B Moore 定律 | 等待时间 | 二分导数变号点 |
| Codeforces 1355E Restorer Distance | 最终高度 | 调整代价单谷 |
| Codeforces 578C Weakness and Poorness | 整体平移量 | 最大绝对子段和 |
| 洛谷 P2571 传送带 | 两条线段上的位置 | 三分套三分 |
| 洛谷 P15260 Balancing the Barns G | 最小不平衡度 | 二分套三分 |
例题一:洛谷 P3382 三分
题目链接:洛谷 P3382 三分
给定一个$n$次多项式,并保证它在区间$[l,r]$中先增后减,求最大值点。
题目已经给出单峰性,因此直接使用实数三分。每次计算多项式时使用霍纳法:
$$
a_nx^n+a_{n-1}x^{n-1}+\cdots+a_0
=(((a_nx+a_{n-1})x+a_{n-2})x+\cdots)+a_0
$$
这样一次 calc(x) 只需要$O(n)$,不需要反复调用幂函数。
点击展开完整代码
1 | /* Fufffh */ |
固定循环次数为$K$,时间复杂度为$O(Kn)$,空间复杂度为$O(n)$。这里$K=100$,可以视为常数。
这道题只负责建立模板。真正遇到三分题时,还需要自己证明目标函数为什么单峰或单谷。
例题二:AtCoder ABC279 D Freefall
题目链接:AtCoder ABC279 D Freefall
执行一次操作需要$B$时间,并使重力参数增加$1$。执行$x$次操作以后,总时间为:
$$
f(x)=Bx+\frac{A}{\sqrt{x+1}}
$$
其中$x$只能取非负整数。把$x$暂时看作实数,有:
$$
f’(x)=B-\frac{A}{2(x+1)^{3/2}},\qquad
f’'(x)=\frac{3A}{4(x+1)^{5/2}}>0
$$
因此$f(x)$是凸函数,在整数定义域上同样呈单谷形态。
当$x\ge\frac{A}{B}$时:
$$
f(x)>Bx\ge A=f(0)
$$
因此只需要搜索:
$$
0\le x\le\left\lfloor\frac{A}{B}\right\rfloor
$$
这是整数定义域,三分到区间长度不超过$6$以后,必须枚举剩余位置。
点击展开完整代码
1 | /* Fufffh */ |
三分次数为$O(\log\frac{A}{B})$,每次计算函数为$O(1)$,空间复杂度为$O(1)$。
这道题也能通过求导估计极小值附近的位置,但整数三分更适合练习“缩小区间后枚举”的写法。
例题三:AtCoder ARC054 B Moore 定律
题目链接:AtCoder ARC054 B Moore 定律
如果等待$x$年再开始计算,计算机速度会变成原来的$2^{x/1.5}$倍,总完成时间为:
$$
f(x)=x+\frac{P}{2^{x/1.5}}
$$
直接对$f(x)$三分当然可以,但这道题更适合展示导数二分。
求导得到:
$$
f’(x)=1-\frac{P\ln2}{1.5}\cdot2^{-x/1.5}
$$
第二项随$x$增加而减小,所以$f’(x)$单调递增。极小值左侧$f’(x)<0$,右侧$f’(x)\ge0$,可以二分导数的变号位置。
由$P\le10^{18}$可知最优等待时间不会超过$100$年,因此搜索区间取$[0,100]$即可。
点击展开完整代码
1 | /* Fufffh */ |
二分轮数为$K$,时间复杂度为$O(K)$,空间复杂度为$O(1)$。
求导以后也可以直接解出零点,但保留二分更有代表性:只要能够判断导数符号,就不要求零点具有方便的闭式表达式。
例题四:Codeforces 1355E Restorer Distance
题目链接:Codeforces 1355E Restorer Distance
有$n$根高度为$h_i$的柱子,可以增加、删除或移动砖块,三种操作的单位代价分别为$A,R,M$。要求把所有柱子变成相同高度,并最小化总代价。
先处理移动代价。如果先删除一块砖,再在另一根柱子上增加一块砖,代价为$A+R$,所以真正有效的移动代价是:
$$
M\leftarrow\min(M,A+R)
$$
固定最终高度$x$,记需要增加的砖块数量为$add$,能够删除的砖块数量为$del$:
$$
add=\sum_{h_i<x}(x-h_i)
$$
$$
del=\sum_{h_i>x}(h_i-x)
$$
其中$\min(add,del)$块砖可以直接移动,剩余部分只能单独增加或删除,因此:
$$
cost(x)
=\min(add,del)M
+(add-\min(add,del))A
+(del-\min(add,del))R
$$
随着$x$增加,删除需求不断减少,增加需求不断增大,单位高度带来的边际代价不会下降,所以$cost(x)$是整数单谷函数。
点击展开完整代码
1 | /* Fufffh */ |
设高度值域为$V$,一次 calc 需要$O(n)$,总时间复杂度为$O(n\log V)$,空间复杂度为$O(n)$。
这道题也可以比较$cost(x)$与$cost(x+1)$,二分离散差分变号的位置。两种写法复杂度相同,差分二分的常数更小,整数三分则更接近目标函数本身。
例题五:Codeforces 578C Weakness and Poorness
题目链接:Codeforces 578C Weakness and Poorness
给定序列$a_1,a_2,\ldots,a_n$,选择实数$x$,把每个元素变成$a_i-x$。一个连续子段的代价是子段和的绝对值,要求所有子段代价中的最大值尽量小。
固定子段$[l,r]$,其和为:
$$
\sum_{i=l}^{r}(a_i-x)
=\sum_{i=l}^{r}a_i-(r-l+1)x
$$
这是关于$x$的一次函数,取绝对值以后是凸函数。所有子段对应的凸函数再取最大值,仍然是凸函数,因此目标函数:
$$
g(x)=\max_{1\le l\le r\le n}
\left|\sum_{i=l}^{r}(a_i-x)\right|
$$
是单谷函数,可以实数三分$x$。
问题只剩下如何在线性时间内计算$g(x)$。对于变换后的数组,分别求最大子段和$mx$与最小子段和$mn$,那么:
$$
g(x)=\max(mx,-mn)
$$
两者都可以用一次线性扫描完成。
点击展开完整代码
1 | /* Fufffh */ |
固定循环次数为$K$,一次 calc 需要$O(n)$,总时间复杂度为$O(Kn)$,空间复杂度为$O(n)$。
这道题的三分模板仍然只有几行,难点在于证明“所有子段绝对值函数的最大值”具有凸性,并把一次函数计算压到$O(n)$。
例题六:洛谷 P2571 传送带
题目链接:洛谷 P2571 传送带
平面上有两条传送带$AB$与$CD$。在$AB$上的速度为$P$,在$CD$上的速度为$Q$,其余位置的速度为$R$,要求从$A$到$D$的最短时间。
设离开第一条传送带的位置为$E$,进入第二条传送带的位置为$F$。总时间为:
$$
T(E,F)
=\frac{|AE|}{P}
+\frac{|EF|}{R}
+\frac{|FD|}{Q}
$$
用$x\in[0,1]$表示$E$在线段$AB$上的比例位置,用$y\in[0,1]$表示$F$在线段$CD$上的比例位置。
线段上的点是参数$x,y$的线性函数,任意两点的欧氏距离都是凸函数,因此$T(x,y)$是联合凸函数。固定$x$以后,可以三分$y$得到当前$E$对应的最短时间;对$y$取最小值后,所得函数关于$x$仍然凸,所以外层继续三分$x$。
点击展开完整代码
1 | /* Fufffh */ |
每层固定循环$K$次,总时间复杂度为$O(K^2)$,空间复杂度为$O(1)$。
这里的参数$x,y$都取$[0,1]$,比直接三分坐标更统一,也不会因为线段方向不同而写出额外分类。
例题七:洛谷 P15260 Balancing the Barns G
题目链接:洛谷 P15260 Balancing the Barns G
第$i$个谷仓有$a_i$捆干草和$b_i$袋饲料。一次操作会同时令:
$$
a_i\leftarrow a_i-1,\qquad b_i\leftarrow b_i+1
$$
恰好执行$K$次操作,要求最小化:
$$
\max(a)-\min(b)
$$
设当前二分的不平衡度为$d$。如果最终令$\min(b)\ge x$,那么还必须满足:
$$
\max(a)\le x+d
$$
对于第$i$个谷仓,至少需要执行:
$$
c_i(x)=\max(0,a_i-x-d,x-b_i)
$$
次操作。三个部分分别对应不操作、降低$a_i$以及提高$b_i$。它们都是关于$x$的一次函数,取最大值以后形成凸函数,因此:
$$
C_d(x)=\sum_{i=1}^{n}c_i(x)
$$
也是单谷函数,可以在固定$d$以后三分$x$。
如果某个谷仓满足$a_i-b_i>d$,那么允许$x$取值的区间暂时为空。必须先执行:
$$
t_i=\left\lceil\frac{a_i-b_i-d}{2}\right\rceil
$$
次操作,使更新后的$a_i-b_i\le d$。处理以后,令:
$$
L_i=a_i-d,\qquad R_i=b_i
$$
此时$L_i\le R_i$,而第$i$个谷仓的额外代价就是点$x$到区间$[L_i,R_i]$的距离:
| $x$的位置 | 额外代价$c_i(x)$ |
|---|---|
| $x<L_i$ | $L_i-x$ |
| $L_i\le x\le R_i$ | $0$ |
| $x>R_i$ | $x-R_i$ |
把所有$L_i$与$R_i$分别排序并维护前缀和,就能用两次二分在$O(\log n)$内计算$C_d(x)$。内层三分得到最少操作数,再判断它是否不超过剩余的$K$。
随着$d$增大,限制只会放宽,所以可行性关于$d$单调。外层二分第一个可行的$d$,内层三分当前$d$下的最优$x$。
点击展开完整代码
1 | /* Fufffh */ |
外层二分进行$O(\log V)$轮。每轮需要排序端点,并在内层三分中用二分计算代价,总时间复杂度为:
$$
O\left(\log V\left(n\log n+\log W\log n\right)\right)
$$
空间复杂度为$O(n)$。
判定只要求最少操作数不超过$K$。如果还有剩余操作,继续减少某个$a_i$并增加对应的$b_i$不会让不平衡度变大,因此一定能够补足到恰好$K$次。
总结与常见问题
怎样判断一道题能否三分?
可以依次检查四件事:
- 搜索对象是实数、整数,还是某条线段上的比例位置?
- 能否快速计算一个候选位置对应的目标值?
- 目标函数是否只会先变好、再变坏?
- 如果存在嵌套,内外两层的单峰性是否都能证明?
第三点最重要。如果函数出现下降、上升、再次下降,普通三分就会错误地删除真正的最优区间。
常见错误
没有证明单峰性
“看起来像一个碗”不是证明。可以从导数、离散差分、边际代价、凸函数的和或凸函数的最大值入手,说明单调方向为什么只改变一次。
最小值与最大值方向写反
求最小值时,若$f(m_1)\le f(m_2)$,应该删除右侧;求最大值时则删除左侧。写模板以前先在纸上画出趋势,通常比背四种方向可靠。
整数三分没有枚举末尾区间
整数除法可能使$m_1=m_2$,也可能让真实最优点落在两个候选位置之间。把区间缩小到常数长度后直接枚举,代码更稳。
精度刚好等于允许误差
题目允许$10^{-6}$误差,不代表停止条件就应该取$10^{-6}$。计算、函数值比较和最终输出都会产生误差,固定循环$80$到$100$次通常更安全。
混淆极值点与极值
循环结束后,$(l+r)/2$是极值点的近似位置;题目如果要求最小代价,应该输出$f((l+r)/2)$,不能直接输出位置。
calc 内部发生溢出
整数三分中的候选位置经常接近$10^{18}$,它参与乘法或累计以后可能超过 i64。只有最后答案保证放入 i64,不代表中间过程也安全,必要时应使用 i128。
忽略嵌套后的总复杂度
一次三分可能只进行几十轮,但三分套三分会把轮数相乘;如果每次 calc 还要线性扫描,总复杂度可能变成$O(K^2n)$。必须把每一层和单次计算一起分析。
复杂度分析
| 类型 | 单次计算 | 搜索次数 | 总时间复杂度 |
|---|---|---|---|
| 实数三分 | $O(T)$ | $K$轮 | $O(KT)$ |
| 整数三分 | $O(T)$ | $O(\log V)$ | $O(T\log V)$ |
| 导数二分 | $O(T)$ | $K$轮 | $O(KT)$ |
| 离散差分二分 | $O(T)$ | $O(\log V)$ | $O(T\log V)$ |
| 三分套线性扫描 | $O(n)$ | $K$轮 | $O(Kn)$ |
| 三分套三分 | $O(T)$ | 每层$K$轮 | $O(K^2T)$ |
| 二分套三分 | $O(T)$ | 两层对数搜索 | $O(T\log V\log W)$ |






