「一本通 1.2 例 3」曲线

GitHub跳转原题关系图返回列表

根据凸函数点态最大值仍为凸函数的性质,在区间 [0, 1000] 上通过三分查找求上包络函数的最小值。

OJ: roj

题目 ID: 1435

难度:普及+/提高-

标签:三分查找凸函数

创建: 2026-09-30 10:12

更新: 2026-10-07 11:01

形式化题目

给定 nn 个二次函数 Si(x)=aix2+bix+ciS_i(x) = a_i x^2 + b_i x + c_i(其中 ai≥0a_i \ge 0),定义复合上包络函数:

F(x)=max⁡1≤i≤nSi(x) F(x) = \max_{1 \le i \le n} S_i(x)

求函数 F(x)F(x) 在闭区间 [0,1000][0, 1000] 上的最小值 min⁡x∈[0,1000]F(x)\min_{x \in [0, 1000]} F(x),结果四舍五入保留 4 位小数。

正解

思路

1. 朴素想法的缺陷

若尝试对每条二次曲线单独求顶点 −bi2ai-\frac{b_i}{2a_i} 的最小值,并取它们中的最小值,这是错误的。因为 F(x)F(x) 是所有曲线的点态最大值(上包络),当某一条抛物线处于其自身顶点时,其他抛物线的值可能很大,上包络在该点的值远非全局最小。样例 2 中两条曲线各自顶点处的 FF 值均为 22,但实际最小值出现在两线交点 x=0.5x = 0.5 处,值为 0.50.5。

若尝试在 [0,1000][0, 1000] 内以极小步长离散化暴力枚举,为了保证 4 位小数精度至少需要 107∼10810^7 \sim 10^8 次计算,在 n≤10000n \le 10000 下必定超时。

2. 函数的凸性证明

观察每个单项函数 Si(x)=aix2+bix+ciS_i(x) = a_i x^2 + b_i x + c_i:

  • 由于题目给定 ai≥0a_i \ge 0,抛物线全部开口向上(当 ai=0a_i = 0 时退化为直线);
  • 因此,每个 Si(x)S_i(x) 在 R\mathbb{R} 上都是(下)凸函数。

考虑凸函数的点态最大值函数 F(x)=max⁡1≤i≤nSi(x)F(x) = \max_{1 \le i \le n} S_i(x)。根据凸函数定义,对任意 x1,x2∈[0,1000]x_1, x_2 \in [0, 1000] 及 λ∈[0,1]\lambda \in [0, 1],对每个 ii 均有:

Si(λx1+(1−λ)x2)≤λSi(x1)+(1−λ)Si(x2)≤λF(x1)+(1−λ)F(x2) S_i(\lambda x_1 + (1-\lambda)x_2) \le \lambda S_i(x_1) + (1-\lambda) S_i(x_2) \le \lambda F(x_1) + (1-\lambda) F(x_2)

对不等式最左端关于 ii 取最大值,可得:

F(λx1+(1−λ)x2)≤λF(x1)+(1−λ)F(x2) F(\lambda x_1 + (1-\lambda)x_2) \le \lambda F(x_1) + (1-\lambda) F(x_2)

这证明了若干凸函数的最大值复合函数 F(x)F(x) 仍然是下凸函数。下凸函数在闭区间 [0,1000][0, 1000] 上为单峰(单谷)或单调函数,不存在多个局部极小值。

3. 三分法求解

针对下凸函数在闭区间上求最小值的问题,可以使用三分查找(Ternary Search):

在当前区间 [lo,hi][lo, hi] 内取两个三等分点:

m1=lo+hi−lo3,m2=hi−hi−lo3 m_1 = lo + \frac{hi - lo}{3}, \quad m_2 = hi - \frac{hi - lo}{3}

分别计算 F(m1)F(m_1) 与 F(m2)F(m_2):

  • 若 F(m1)<F(m2)F(m_1) < F(m_2):极小值点不可能在 (m2,hi](m_2, hi] 范围内(若在 m2m_2 右侧,则根据下凸性在 [lo,m2][lo, m_2] 上函数必须递减,会有 F(m1)≥F(m2)F(m_1) \ge F(m_2),产生矛盾)。因此可以舍弃右侧三分之一区间,令 hi←m2hi \leftarrow m_2。
  • 若 F(m1)≥F(m2)F(m_1) \ge F(m_2):同理,极小值点不可能在 [lo,m1)[lo, m_1) 范围内,可以舍弃左侧三分之一区间,令 lo←m1lo \leftarrow m_1。

4. 样例 2 的上包络及收缩示例

以样例 2 为例,S1(x)=2x2S_1(x) = 2x^2,S2(x)=2(x−1)2S_2(x) = 2(x-1)^2:

轮次 lolo hihi m1m_1 m2m_2 F(m1)F(m_1) F(m2)F(m_2) 操作
初始 0.00.0 1000.01000.0 333.3333333.3333 666.6667666.6667 222222.2222222.2 886671.1886671.1 F(m1)<F(m2)  ⟹  hi←m2F(m_1) < F(m_2) \implies hi \leftarrow m_2
迭代收敛至谷底 ≈0.5000\approx 0.5000 ≈0.5000\approx 0.5000 - - - - F(0.5)=0.5000F(0.5) = 0.5000

每轮区间长度均缩短为原来的 23\frac{2}{3}。迭代 100 次后区间长度为 1000×(23)100<10−141000 \times (\frac{2}{3})^{100} < 10^{-14},精度远超题目所需的 4 位小数要求。最终直接返回中点处的 FF 值即可。

代码

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;
}

复杂度

  • 时间复杂度:计算单点函数值 F(x)F(x) 需要遍历 nn 条曲线,单次求值复杂度为 O(n)O(n)。三分查找固定迭代 K=100K = 100 轮,每轮计算 2 次函数值。因此每组测试数据的时间复杂度为 O(K⋅n)O(K \cdot n)。对于 T≤10,n≤10000T \le 10, n \le 10000,总计算次数在 2×1072 \times 10^7 以内,可以在 1s 左右高效通过。
  • 空间复杂度:存储 nn 个二次函数的系数元组列表,空间复杂度为 O(n)O(n),远低于 128MB 的内存限制。

总结

本题的关键在于利用数学性质识别函数的单谷性:

  1. 开口向上的抛物线(ai≥0a_i \ge 0)与直线均为凸函数;
  2. 凸函数的点态最大值函数仍保持下凸性;
  3. 对单谷下凸函数在连续实数区间求最小值,使用实数三分法可以在对数级轮数内精确逼近极小值点。