根据凸函数点态最大值仍为凸函数的性质,在区间 [0, 1000] 上通过三分查找求上包络函数的最小值。
OJ: roj
题目 ID: 1435
难度:普及+/提高-
标签:三分查找凸函数
创建: 2026-09-30 10:12
更新: 2026-10-07 11:01
形式化题目
给定
求函数
正解
思路
1. 朴素想法的缺陷
若尝试对每条二次曲线单独求顶点
若尝试在
2. 函数的凸性证明
观察每个单项函数
- 由于题目给定
,抛物线全部开口向上(当 时退化为直线); - 因此,每个
在 上都是(下)凸函数。
考虑凸函数的点态最大值函数
对不等式最左端关于
这证明了若干凸函数的最大值复合函数
3. 三分法求解
针对下凸函数在闭区间上求最小值的问题,可以使用三分查找(Ternary Search):
在当前区间
分别计算
- 若
:极小值点不可能在 范围内(若在 右侧,则根据下凸性在 上函数必须递减,会有 ,产生矛盾)。因此可以舍弃右侧三分之一区间,令 。 - 若
:同理,极小值点不可能在 范围内,可以舍弃左侧三分之一区间,令 。
4. 样例 2 的上包络及收缩示例
以样例 2 为例,
| 轮次 | 操作 | ||||||
|---|---|---|---|---|---|---|---|
| 初始 | |||||||
| 迭代收敛至谷底 | - | - | - | - |
每轮区间长度均缩短为原来的
代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 10:12
# update_at: 2026-09-30 10:12
import sys
def peak(fs: list[tuple[int, int, int]], x: float) -> float:
"""上包络 F(x) = max(S_i(x)) 在 x 处的函数值。"""
return max(a * x * x + b * x + c for a, b, c in fs)
def ternary_min(fs: list[tuple[int, int, int]]) -> float:
"""在 [0,1000] 上三分收缩区间,返回凸函数 F 的最小值。
F 是一堆开口向上抛物线(含退化直线)的逐点最大值,仍是凸函数:
每次 F(m1) < F(m2) 说明谷底只能在 [lo, m2],否则只能在 [m1, hi]。
100 轮后区间长 1000·(2/3)^100,远小于答案精度所需的 5e-11。
"""
lo, hi = 0.0, 1000.0
for _ in range(100):
m1 = lo + (hi - lo) / 3
m2 = hi - (hi - lo) / 3
if peak(fs, m1) < peak(fs, m2):
hi = m2
else:
lo = m1
return peak(fs, (lo + hi) / 2)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
T = next(data)
for _ in range(T):
n = next(data)
fs = [(next(data), next(data), next(data)) for _ in range(n)] # n 条曲线 (a,b,c),元组内从左到右求值
out.append(f"{ternary_min(fs):.4f}")
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 00:59
* update_at: 2026-10-05 01:00
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
typedef long long ll;
ll n;
ll a[MAXN]; // 第 i 条曲线的二次项系数
ll b[MAXN]; // 第 i 条曲线的一次项系数
ll c[MAXN]; // 第 i 条曲线的常数项
// 上包络 F(x) = max_i (a_i * x^2 + b_i * x + c_i)
double envelope(double x) {
double best = -1e18;
for (ll i = 0; i < n; i++) {
double value = a[i] * x * x + b[i] * x + c[i];
if (value > best) {
best = value;
}
}
return best;
}
// 每条曲线开口向上(a_i >= 0),凸函数的点态最大值仍是凸函数,
// 所以 F 在 [0,1000] 上单谷,直接三分求最小值。
double ternary_min() {
double lo = 0.0;
double hi = 1000.0;
for (int iter = 0; iter < 100; iter++) {
double m1 = lo + (hi - lo) / 3;
double m2 = hi - (hi - lo) / 3;
if (envelope(m1) < envelope(m2)) {
hi = m2; // 谷底在左三分之二区间内
} else {
lo = m1; // 谷底在右三分之二区间内
}
}
return envelope((lo + hi) / 2);
}
void solve() {
ll t;
cin >> t;
for (ll k = 0; k < t; k++) {
cin >> n;
for (ll i = 0; i < n; i++) {
cin >> a[i] >> b[i] >> c[i];
}
printf("%.4f\n", ternary_min());
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}复杂度
- 时间复杂度:计算单点函数值
需要遍历 条曲线,单次求值复杂度为 。三分查找固定迭代 轮,每轮计算 2 次函数值。因此每组测试数据的时间复杂度为 。对于 ,总计算次数在 以内,可以在 1s 左右高效通过。 - 空间复杂度:存储
个二次函数的系数元组列表,空间复杂度为 ,远低于 128MB 的内存限制。
总结
本题的关键在于利用数学性质识别函数的单谷性:
- 开口向上的抛物线(
)与直线均为凸函数; - 凸函数的点态最大值函数仍保持下凸性;
- 对单谷下凸函数在连续实数区间求最小值,使用实数三分法可以在对数级轮数内精确逼近极小值点。