CSP-S 2023 种树

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

二分完成天数;固定 D 时用求根公式 O(1) 算出每棵树的最晚种下日,再把子树截止时间下压成压缩截止日,按升序排日程即可判定。

OJ: roj

题目 ID: 3679

难度:提高+/省选-

标签:二分答案树贪心数学python

创建: 2026-10-02 15:14

更新: 2026-10-10 11:30

形式化题目

给定 nn 个点的树,11 号点是入口。每片地块 ii 有目标高度 aia_i 与生长参数 bi,cib_i, c_i。种植过程:

  • 第 11 天必须在 11 号点种下一棵高 00 的树;之后每天选一个未种且与已种地块相邻(单条边相连)的地块种树,直到 nn 天全部种完。
  • 第 xx 天(绝对天数,从第 11 天起算)地块 ii 的树长高 gi(x)=max⁡(bi+x⋅ci, 1)g_i(x) = \max(b_i + x \cdot c_i,\ 1),从种下当天开始累计。

求最小的 DD,使存在合法的种植顺序,每棵树在第 DD 天的高度都不低于 aia_i。

样例 1:下图是样例的树形结构,节点标签给出各点的 ai, bi, cia_i,\ b_i,\ c_i。

graph TD
    n1["1: a=12 b=1 c=1"]
    n2["2: a=2 b=4 c=-1"]
    n3["3: a=10 b=3 c=0"]
    n4["4: a=7 b=10 c=-2"]
    n1 --- n2
    n1 --- n3
    n3 --- n4

一种 5 天完成的方案(与题面样例解释一致),表格逐天记录种植动作与当天生长后的累计高度:

天 当天种植 1 号 2 号 3 号 4 号
1 1 号 2 — — —
2 3 号 5 — 3 —
3 4 号 9 — 6 4
4 2 号 14 1 9 6
5 (已种完,只生长) 20 2 12 7

第 5 天四棵树分别达到 20⩾1220 \geqslant 12、2⩾22 \geqslant 2、12⩾1012 \geqslant 10、7⩾77 \geqslant 7,输出 55。

正解

思路

朴素做法与瓶颈。 固定天数 DD 时,最直接的验证是枚举所有合法种植顺序(父点先于子点的排列,数量指数级),逐棵算第 DD 天的高度;退化成"每天在可种点中任选一个"的搜索同样是指数的。瓶颈在于顺序与高度耦合在一起,需要把两者拆开。

第一步:把顺序拆成每棵树自己的"最晚种下日"。 第 pp 天种下的树到第 DD 天的高度是

S(p)=∑x=pDmax⁡(bi+xci, 1)S(p) = \sum_{x=p}^{D} \max(b_i + x c_i,\ 1)

每天生长量都不小于 11,所以 S(p)S(p) 随 pp 增大严格减小——早种永远不亏。于是固定 DD 后每棵树独立地存在一个最晚种下日

ti=max⁡{ p⩽D:S(p)⩾ai }t_i = \max\{\, p \leqslant D : S(p) \geqslant a_i \,\}

若从第 11 天种也到不了 aia_i,则该 DD 直接判不可行。问题转化为:能否给每个点安排互不相同的种植日 pos(i)⩽ti\mathrm{pos}(i) \leqslant t_i,且 11 号点在第 11 天、父点先于子点。

令 m=D−p+1m = D - p + 1(连续生长天数),SS 关于 mm 严格递增,且 SS 是 mm 的分段二次函数,所以不必再对每个 DD 套一层二分,直接解 S(m)⩾aiS(m) \geqslant a_i 的最小 mm 即可 O(1)O(1) 得到 tit_i:

  • c=0c = 0:每天固定长 bb,m=⌈a/b⌉m = \lceil a/b \rceil;
  • c>0c > 0:每项都是 b+xcb + xc,2S(m)=(2b+c(2D+1))m−cm22S(m) = \big(2b + c(2D+1)\big)m - c m^2,解二次不等式取较小根向上取整;
  • c<0c < 0:记 k=⌊(b−1)/(−c)⌋k = \lfloor (b-1)/(-c) \rfloor,第 kk 天之后每天只长 11;尾段按 11 米直接累加,不足时对线性段 n=m−(D−k)n = m - (D-k) 同样解二次方程。

第二步:合法顺序就是树的拓扑序。 已种集合始终是含 11 号点的连通子树,而树上两点间唯一路径必经过父点,所以父点一定先于子点被种;反过来任意"父先于子"的顺序都能逐日实现(每天种的点,其父点已在已种集合里)。因此约束就是 pos(父)<pos(子)\mathrm{pos}(\text{父}) < \mathrm{pos}(\text{子}),且 11 号点在第 11 天。

第三步:截止时间要沿祖先链下压。 只按各点自己的 tit_i 排日程会出错:点 ii 若排在第 pos(i)\mathrm{pos}(i) 天,它的每个后代 ww 至少要等到 pos(i)+(dw−di)\mathrm{pos}(i) + (d_w - d_i) 天才种得下(dd 为深度),而 ww 又必须满足 pos(w)⩽tw\mathrm{pos}(w) \leqslant t_w,于是 ii 实际受到全子树的限制

pos(i)⩽tw−(dw−di)(w 取遍 i 的子树,含 i 自己)\mathrm{pos}(i) \leqslant t_w - (d_w - d_i) \qquad (w \text{ 取遍 } i \text{ 的子树,含 } i \text{ 自己})

取最紧的一个,定义压缩截止时间(子树内最紧的后代截止被"下压"到 ii 身上):

Ti=min⁡( tw−(dw−di) )=di+min⁡w∈子树(i)(tw−dw)T_i = \min\big(\, t_w - (d_w - d_i) \,\big) = d_i + \min_{w \in \text{子树}(i)} (t_w - d_w)

第四步:判定定理。 把 T1,…,TnT_1, \dots, T_n 升序记为 T[1]⩽⋯⩽T[n]T_{[1]} \leqslant \cdots \leqslant T_{[n]},合法日程存在 当且仅当 对每个 jj 都有 T[j]⩾jT_{[j]} \geqslant j:

  • 必要性:任何合法日程中每个点都满足 pos(v)⩽Tv\mathrm{pos}(v) \leqslant T_v(上式对每个后代 ww 成立,取最紧的即得)。TT 最小的 jj 个点需要 jj 个互不相同的天数,且每个都不超过 T[j]T_{[j]},这些天只能取自 [1,T[j]][1, T_{[j]}],故 T[j]⩾jT_{[j]} \geqslant j。
  • 充分性:按 TT 升序把第 jj 个点安排在第 jj 天。祖先的 TT 严格小于后代(深度多 11、取 min 的范围只缩不扩),所以这个顺序天然父先于子;又 pos(v)=rank(v)⩽Tv⩽tv\mathrm{pos}(v) = \mathrm{rank}(v) \leqslant T_v \leqslant t_v,截止也满足;nn 天恰好各用一次。11 号点的 TT 最小(深度 00 且取全树 min),总是排第 11 天,与"第 11 天只能种 11 号点"吻合。

在样例上分别代入 D=4D = 4 与 D=5D = 5 验证判定(先对每点算 tit_i,再按下压式算 TT):

点 深度 dd ti (D=4)t_i\ (D{=}4) Ti (D=4)T_i\ (D{=}4) ti (D=5)t_i\ (D{=}5) Ti (D=5)T_i\ (D{=}5)
1 0 2 0 3 1
2 1 3 3 4 4
3 1 1 1 2 2
4 2 2 2 3 3

D=4D = 4 时升序 T=[0,1,2,3]T = [0, 1, 2, 3],第 11 个 0<10 < 1,不可行;D=5D = 5 时升序 T=[1,2,3,4]T = [1, 2, 3, 4] 全部不小于下标,可行,且按升序给出的种植顺序恰好是 1→3→4→21 \to 3 \to 4 \to 2,与样例解释一致。

第五步:二分答案。 DD 越大每棵树生长期越长,tit_i 不减、判定单调(可行集是后缀),对 DD 在 [1,109][1, 10^9] 二分(题面保证 10910^9 天内有解)。

代码

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-10 11:30
 * update_at: 2026-10-10 11:30
 */

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
vector<ll>a,b,c; vector<vector<int> > children; vector<int> depthv, orderv;
ll isqrt_ll(__int128 x){ long double y=(long double)x; ll r=sqrt(y); while((__int128)(r+1)*(r+1)<=x) r++; while((__int128)r*r>x) r--; return r;}
ll ceil_div(ll x,ll y){return (x+y-1)/y;}
ll min_grow_days(ll aa,ll bb,ll cc,ll D){ if(cc==0) return ceil_div(aa,bb); ll q=-cc; if(cc>0 || (bb-1)/q>=D){ ll B=2*bb+cc*(2*D+1); if(cc>0){ __int128 delta=(__int128)B*B-(__int128)8*cc*aa; if(delta<0) return D+1; ll sq=isqrt_ll(delta); ll m=(B-sq+2*cc-1)/(2*cc); while(m>1 && (__int128)B*(m-1)-(__int128)cc*(m-1)*(m-1)>=2*(__int128)aa) m--; while(m<=D && (__int128)B*m-(__int128)cc*m*m<2*(__int128)aa) m++; return m;} ll m=max((isqrt_ll((__int128)B*B+(__int128)8*q*aa)-B+2*q-1)/(2*q),1LL); while(m<=D && (__int128)B*m+(__int128)q*m*m<2*(__int128)aa) m++; return m; }
    ll k=(bb-1)/q, m0=D-k; if(aa<=m0) return aa; if(k==0) return D+1; ll B2=2*bb+cc*(2*k+1); ll nn=max((isqrt_ll((__int128)B2*B2+(__int128)8*q*(aa-m0))-B2+2*q-1)/(2*q),1LL); while(nn<=k && (__int128)B2*nn+(__int128)q*nn*nn<2*(__int128)(aa-m0)) nn++; return nn+m0;}
bool feasible(ll D){int n=a.size(); vector<ll> t(n),mv(n); for(int i=0;i<n;i++){ll mm=min_grow_days(a[i],b[i],c[i],D); if(mm>D) return false; t[i]=D-mm+1;} for(int idx=(int)orderv.size()-1;idx>=0;idx--){int u=orderv[idx]; ll best=t[u]-depthv[u]; for(int j=0;j<(int)children[u].size();j++) if(mv[children[u][j]]<best) best=mv[children[u][j]]; mv[u]=best;} vector<ll> rankv(n); for(int u=0;u<n;u++) rankv[u]=depthv[u]+mv[u]; sort(rankv.begin(),rankv.end()); for(int j=0;j<n;j++) if(rankv[j]<j+1) return false; return true;}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); int n; if(!(cin>>n)) return 0; a.resize(n); b.resize(n); c.resize(n); for(int i=0;i<n;i++) cin>>a[i]>>b[i]>>c[i]; vector<vector<int> > g(n); for(int i=0;i<n-1;i++){int u,v;cin>>u>>v;u--;v--;g[u].push_back(v);g[v].push_back(u);} vector<int> parent(n,-1), st; depthv.assign(n,0); children.assign(n,vector<int>()); parent[0]=-2; st.push_back(0); while(!st.empty()){int u=st.back();st.pop_back(); orderv.push_back(u); for(int i=0;i<(int)g[u].size();i++){int v=g[u][i]; if(parent[v]==-1){parent[v]=u; depthv[v]=depthv[u]+1; children[u].push_back(v); st.push_back(v);}}} ll lo=1,hi=1000000000LL; while(lo<hi){ll mid=(lo+hi)/2; if(feasible(mid)) hi=mid; else lo=mid+1;} cout<<lo<<'\n'; return 0;}
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://roj.ac.cn  https://rbook.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 15:19
# update_at: 2026-10-02 15:19

import sys
from math import isqrt


def min_grow_days(a: int, b: int, c: int, D: int) -> int:
    """固定 D 天完成时,这棵树最少要连续生长几天(含种下当天);返回值大于 D 表示做不到。

    第 x 天长高 max(b + x*c, 1),连续生长 m 天(第 D-m+1 天种下)的总高度 S(m) 随 m 递增,
    这里直接解 S(m) >= a 的最小 m,避免对每个 D 再套一层二分。
    """
    if c == 0:
        return -(-a // b)  # 每天固定长 b 米,向上取整

    q = -c
    if c > 0 or (b - 1) // q >= D:
        # [1, D] 内每天生长量 b + x*c 都不小于 1:S(m) = (B*m - c*m*m)/2
        B = 2 * b + c * (2 * D + 1)
        if c > 0:
            delta = B * B - 8 * c * a
            if delta < 0:
                return D + 1  # 二次函数始终低于 a,从第 1 天种也长不到
            # 2S(m) >= 2a 即 c*m^2 - B*m + 2a <= 0,最小 m 是较小根向上取整
            m = (B - isqrt(delta) + 2 * c - 1) // (2 * c)  # 根的上界,再向下校准
            while m > 1 and B * (m - 1) - c * (m - 1) * (m - 1) >= 2 * a:
                m -= 1
            while m <= D and B * m - c * m * m < 2 * a:
                m += 1
            return m
        # c < 0 但整段都线性:q*m^2 + B*m >= 2a,取较大根向上取整
        m = max((isqrt(B * B + 8 * q * a) - B + 2 * q - 1) // (2 * q), 1)
        while m <= D and B * m + q * m * m < 2 * a:
            m += 1
        return m

    # c < 0:第 k 天之后生长量掉到 1,前段线性、后段按 1 米计
    k = (b - 1) // q  # 最后一个满足 b + x*c >= 1 的天
    m0 = D - k        # 尾部按 1 米生长的天数
    if a <= m0:
        return a  # 只靠尾段每天 1 米就够
    if k == 0:
        return D + 1  # 全程每天只长 1 米,a > D 时完不成
    # 设线性段长 n = m - m0,2(S - m0) = B2*n + q*n*n,解较大根
    B2 = 2 * b + c * (2 * k + 1)
    n = max((isqrt(B2 * B2 + 8 * q * (a - m0)) - B2 + 2 * q - 1) // (2 * q), 1)
    while n <= k and B2 * n + q * n * n < 2 * (a - m0):
        n += 1
    return n + m0


def feasible(D: int, a: list[int], b: list[int], c: list[int],
             children: list[list[int]], depth: list[int], order: list[int]) -> bool:
    """判断能否在第 D 天完成:先算每棵树最晚种下日 t,再检查全树可行的种植日程。"""
    n = len(a)
    t = [0] * n
    for i in range(n):
        m = min_grow_days(a[i], b[i], c[i], D)
        if m > D:
            return False  # 这棵树在 D 天内长不到 a_i
        t[i] = D - m + 1  # 最晚种下日

    # T[u] = depth[u] + 子树内 min(t[w] - depth[w]):祖先必须不晚于它,后代才能全部赶上
    mv = [0] * n
    for u in reversed(order):  # 后序:孩子的 mv 已经算好
        best = t[u] - depth[u]
        for v in children[u]:
            if mv[v] < best:
                best = mv[v]
        mv[u] = best

    # 全部 n 个 T 升序排成日程:第 j 个必须能排在第 j 天(T 升序天然是先祖先的拓扑序)
    rank = sorted(depth[u] + mv[u] for u in range(n))
    return all(val >= j for j, val in enumerate(rank, 1))


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)

    a = [0] * n
    b = [0] * n
    c = [0] * n
    for i in range(n):
        a[i], b[i], c[i] = next(data), next(data), next(data)

    g: list[list[int]] = [[] for _ in range(n)]
    for _ in range(n - 1):
        u, v = next(data) - 1, next(data) - 1
        g[u].append(v)
        g[v].append(u)

    # 以 1 号点为根:先序(父在子前)+ 深度 + 子女表,迭代写法避开十万层递归
    parent = [-1] * n
    depth = [0] * n
    children: list[list[int]] = [[] for _ in range(n)]
    order: list[int] = []
    stack = [0]
    parent[0] = -2  # 根标记为已访问
    while stack:
        u = stack.pop()
        order.append(u)
        for v in g[u]:
            if parent[v] == -1:
                parent[v] = u
                depth[v] = depth[u] + 1
                children[u].append(v)
                stack.append(v)

    lo, hi = 1, 10**9  # 题面保证 1e9 天内存在方案
    while lo < hi:
        mid = (lo + hi) // 2
        if feasible(mid, a, b, c, children, depth, order):
            hi = mid
        else:
            lo = mid + 1
    print(lo)


if __name__ == "__main__":
    solve()

复杂度

  • 时间:二分约 3030 轮,每轮 O(n)O(n) 求各点 tit_i、O(n)O(n) 一次后序取子树 min、O(nlog⁡n)O(n \log n) 排序,合计 O(nlog⁡n⋅log⁡109)O(n \log n \cdot \log 10^9);n⩽105n \leqslant 10^5,实测最大测例约 1.8 s1.8\,\mathrm{s}(本地单测例限时 20 s20\,\mathrm{s},20 组真实数据全部 PASS;OJ 限时 1000 ms1000\,\mathrm{ms} 对 Python 偏紧)。
  • 空间:O(n)O(n) 存树与三个数组,实测峰值约 100 MB<128 MB100\,\mathrm{MB} < 128\,\mathrm{MB}(aia_i 达 101810^{18}、求根判别式到 103610^{36} 量级,Python 整数免溢出;C++ 中需用 __int128 或比较前先做溢出保护,其余做法相同)。

总结

  • 固定 DD 后"早种不亏",每棵树有独立的最晚种下日 tit_i;S(m)S(m) 是分段二次且随 mm 递增,用求根公式 O(1)O(1) 解出,避免二分套二分。
  • 合法种植顺序恰是树的拓扑序(父先于子);点 ii 的真实截止被全子树下压为 Ti=di+min⁡w∈子树(i)(tw−dw)T_i = d_i + \min_{w \in \text{子树}(i)} (t_w - d_w)。
  • 判定定理:TT 升序第 jj 个排第 jj 天可行   ⟺  T[j]⩾j\iff T_{[j]} \geqslant j 对所有 jj 成立;必要性是鸽笼,充分性给出显式日程(TT 升序天然是拓扑序)。
  • 边界:n=1n = 1 时判定退化为 t1⩾1t_1 \geqslant 1;c<0c < 0 时生长量会掉到 11,按尾段处理;任一棵在第 11 天种也达不到 aia_i 时该 DD 立即判负。
  • 验证:样例输出 55,仓库 20 组真实测试数据(tree1–tree20)输出全部一致。