[CSP-S 2023] 种树

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

二分完成天数,把每个点转成最晚种植日,再用最早截止时间优先判断树上调度。

OJ: luogu

题目 ID: P9755

难度:提高+/省选-

标签:二分贪心树形结构优先队列

创建: 2026-07-06 08:46

更新: 2026-10-07 10:45

形式化题目

给定 nn 片地块构成的树,11 号地块连接入口。每天可以在与已种树地块相邻的空地上种一棵高度为 00 的树(第 11 天只能种 11 号地块)。

若点 ii 在第 dd 天种下,则到第 TT 天它的高度为

∑x=dTmax⁡(bi+x⋅ci, 1)\sum_{x=d}^{T} \max(b_i + x \cdot c_i,\ 1)

(xx 从整个任务的第一天开始计数)。要求所有点高度不低于 aia_i,求最小完成天数 TT。

暴力解法

思路

二分 TT,然后递归枚举所有合法的连通种植顺序:第 day 天在"未种、且与某种下的地块相邻"的地块里任选一个种下,检查每个点种下时是否不晚于自己的最晚种植日。

代码

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-07-06 08:46
 * update_at: 2026-10-01 22:33
 */
// brute.cpp:小数据暴力解,二分天数后递归枚举所有合法的连通种植顺序。
// 只适合 n <= 10 左右的小数据,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 12;

int n;
ll need_h[MAXN]; // 目标高度 a[i]
ll b[MAXN];      // 每天长高 b[i] + x*c[i] 的截距
ll c[MAXN];      // 每天长高的斜率,可为负
bool edge[MAXN][MAXN]; // 邻接矩阵
ll deadline_day[MAXN]; // deadline_day[i]:点 i 的最晚种植日
bool planted[MAXN];    // planted[i]:点 i 是否已经种下

// 等差数列求和:sum_{x=l..r} (bb + cc*x),用 __int128 防溢出
__int128 sum_linear(ll bb, ll cc, ll l, ll r) {
    if (l > r) return 0;
    __int128 cnt = (__int128)r - l + 1;
    __int128 sum_x = (__int128)(l + r) * cnt / 2;
    return (__int128)bb * cnt + (__int128)cc * sum_x;
}

// 点 u 在第 l..r 天的总生长量:sum_{x=l..r} max(b + x*c, 1)
__int128 growth_sum(int u, ll l, ll r) {
    if (l > r) return 0;
    if (c[u] >= 0) return sum_linear(b[u], c[u], l, r);
    ll dec = -c[u];
    ll last_big = (b[u] - 1) / dec;
    ll mid = min(r, last_big);
    __int128 result = 0;
    if (l <= mid) result += sum_linear(b[u], c[u], l, mid);
    if (mid + 1 <= r) result += (__int128)r - (mid + 1) + 1;
    return result;
}

// 二分点 u 的最晚种植日,不够种返回 0,超过 n 天封顶为 n
ll calc_deadline(int u, ll total_day) {
    if (growth_sum(u, 1, total_day) < need_h[u]) return 0;
    ll left = 1, right = total_day;
    while (left < right) {
        ll mid = (left + right + 1) / 2;
        if (growth_sum(u, mid, total_day) >= need_h[u]) left = mid;
        else right = mid - 1;
    }
    if (left > n) return n;
    return left;
}

// 点 u 现在能不能种:要么是入口 1,要么有已经种好的邻居
bool has_planted_neighbor(int u) {
    if (u == 1) return true;
    for (int v = 1; v <= n; v++) {
        if (edge[u][v] && planted[v]) {
            return true;
        }
    }
    return false;
}

// 枚举第 day 天种哪棵树;能铺满 1..n 天就找到了合法顺序
bool dfs_order(int day) {
    if (day == n + 1) {
        return true;
    }
    for (int u = 1; u <= n; u++) {
        if (!planted[u] && has_planted_neighbor(u) && day <= deadline_day[u]) {
            planted[u] = true;
            if (dfs_order(day + 1)) {
                return true;
            }
            planted[u] = false;
        }
    }
    return false;
}

// 判断完成天数 total_day 是否可行
bool check(ll total_day) {
    if (total_day < n) return false;
    for (int i = 1; i <= n; i++) {
        deadline_day[i] = calc_deadline(i, total_day);
        if (deadline_day[i] == 0) return false;
        planted[i] = false;
    }
    return dfs_order(1);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> need_h[i] >> b[i] >> c[i];
    }
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        edge[u][v] = edge[v][u] = true;
    }

    // 先倍增找上界,再二分最小完成天数
    ll left = 1, right = 200;
    while (!check(right)) {
        right *= 2;
    }
    while (left < right) {
        ll mid = (left + right) / 2;
        if (check(mid)) right = mid;
        else left = mid + 1;
    }
    cout << left << '\n';
    return 0;
}

复杂度

  • 时间:O(log⁡T⋅n!)O(\log T \cdot n!) 级别,枚举所有合法种植顺序
  • 空间:O(n)O(n)

瓶颈

枚举量阶乘级增长,只能处理 n⩽10n \leqslant 10 左右的小数据。

正解

思路

满分做法仍然二分答案,但检查 TT 时不能枚举顺序。

固定一个完成天数 TT。对每个点 ii,二分出它的最晚种植日 deadline[i]deadline[i]:

∑x=deadline[i]Tmax⁡(bi+x⋅ci, 1)⩾ai\sum_{x=deadline[i]}^{T} \max(b_i + x \cdot c_i,\ 1) \geqslant a_i

如果从第 11 天种到第 TT 天都达不到 aia_i,那么这个 TT 一定不可行。

接下来问题变成:

text
在树上安排每天种一个点;
父亲必须早于儿子;
每个点必须不晚于自己的 deadline 被种下。

这个调度可以用贪心判断,但优先级不能只看当前点自己的 deadline。如果某个点本身不急,但它的子树里有很急的后代,就应该尽早种它来打开这条分支。

所以对每个点 uu 计算:

text
min_deadline[u] = u 的子树里最小的 deadline

每天把当前已经可种的点放进优先队列,优先种 min_deadline 最小的点。这样会优先打开包含紧急节点的子树。种下某个点时,再检查它自己的 deadline 是否已经过期;如果过期,当前 TT 不可行。

计算某个点从 dd 到 TT 的生长量时,要处理 ci<0c_i < 0 的情况。因为每天至少长高 11,所以把区间分成两段:

  • bi+x⋅ci⩾1b_i + x \cdot c_i \geqslant 1 的部分,用等差数列求和;
  • 后面不足 11 的部分,每天按 11 计算。

代码

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-07-06 08:46
 * update_at: 2026-10-01 22:33
 */
// main.cpp:二分完成天数,把每个点转成最晚种植日,再做树上 EDF 调度。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 100005;

int n;
ll need_h[MAXN]; // 目标高度 a[i]
ll b[MAXN];      // 每天长高 b[i] + x*c[i] 的截距
ll c[MAXN];      // 每天长高的斜率,可为负

vector<int> g[MAXN];    // 原树邻接表
vector<int> child[MAXN]; // 以 1 为根的有根树的孩子表
ll deadline_day[MAXN];   // deadline_day[i]:点 i 的最晚种植日
ll min_subtree_deadline[MAXN]; // 子树内最小的 deadline,做 EDF 优先级
vector<int> order_nodes; // BFS 序,倒着遍历就是自底向上
int order_cnt;           // order_nodes 的长度,避免 size() 强转

// 等差数列求和:sum_{x=l..r} (bb + cc*x)
// 值域到 1e27 量级,用 __int128 防溢出(GCC 扩展,OJ 常用)。
__int128 sum_linear(ll bb, ll cc, ll l, ll r) {
    if (l > r) {
        return 0;
    }
    __int128 cnt = (__int128)r - l + 1;
    __int128 sum_x = (__int128)(l + r) * cnt / 2;
    return (__int128)bb * cnt + (__int128)cc * sum_x;
}

// 点 u 在第 l..T 天的总生长量:sum_{x=l..r} max(b + x*c, 1)
// c < 0 时把区间切成两段:b + x*c >= 1 的部分等差求和,后面每天按 1 算。
__int128 growth_sum(int u, ll l, ll r) {
    if (l > r) {
        return 0;
    }
    if (c[u] >= 0) {
        return sum_linear(b[u], c[u], l, r);
    }

    ll dec = -c[u];
    ll last_big = (b[u] - 1) / dec; // x <= last_big 时 b + x*c 至少为 1
    ll mid = min(r, last_big);
    __int128 result = 0;
    if (l <= mid) {
        result += sum_linear(b[u], c[u], l, mid);
    }
    if (mid + 1 <= r) {
        result += (__int128)r - (mid + 1) + 1;
    }
    return result;
}

// 二分点 u 的最晚种植日:最小的 d 满足 sum(d..T) >= a[u]。
// 种到第 1 天都不够就返回 0;超过 n 天没意义,封顶为 n。
ll calc_deadline(int u, ll total_day) {
    if (growth_sum(u, 1, total_day) < need_h[u]) {
        return 0;
    }

    ll left = 1;
    ll right = total_day;
    while (left < right) {
        ll mid = (left + right + 1) / 2;
        if (growth_sum(u, mid, total_day) >= need_h[u]) {
            left = mid;
        } else {
            right = mid - 1;
        }
    }

    if (left > n) {
        return n;
    }
    return left;
}

// 判断完成天数 total_day 是否可行:
// 每天种一个点、父亲早于儿子、每个点不晚于自己的 deadline。
bool check(ll total_day) {
    if (total_day < n) {
        return false;
    }

    for (int i = 1; i <= n; i++) {
        deadline_day[i] = calc_deadline(i, total_day);
        if (deadline_day[i] == 0) {
            return false;
        }
    }

    // 自底向上求每棵子树里最小的 deadline
    for (int i = order_cnt - 1; i >= 0; i--) {
        int u = order_nodes[i];
        min_subtree_deadline[u] = deadline_day[u];
        int child_cnt = child[u].size();
        for (int j = 0; j < child_cnt; j++) {
            int v = child[u][j];
            min_subtree_deadline[u] = min(min_subtree_deadline[u], min_subtree_deadline[v]);
        }
    }

    // 每天在"可种的点"里种 min_subtree_deadline 最小的,
    // 优先打开包含紧急节点的子树;种下时检查自己的 deadline 有没有过期。
    priority_queue<pair<ll, int>, vector<pair<ll, int> >, greater<pair<ll, int> > > heap;
    heap.push(make_pair(min_subtree_deadline[1], 1));

    for (int day = 1; day <= n; day++) {
        if (heap.empty()) {
            return false;
        }
        int u = heap.top().second;
        heap.pop();
        if (deadline_day[u] < day) {
            return false;
        }
        int child_cnt = child[u].size();
        for (int j = 0; j < child_cnt; j++) {
            int v = child[u][j];
            heap.push(make_pair(min_subtree_deadline[v], v));
        }
    }

    return true;
}

// 以 1 为根 BFS 建有根树,同时记录 BFS 序
void build_rooted_tree() {
    vector<int> parent(n + 1, 0);
    queue<int> q;
    parent[1] = -1;
    q.push(1);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        order_nodes.push_back(u);
        int adj_cnt = g[u].size();
        for (int i = 0; i < adj_cnt; i++) {
            int v = g[u][i];
            if (v == parent[u]) {
                continue;
            }
            parent[v] = u;
            child[u].push_back(v);
            q.push(v);
        }
    }
    order_cnt = order_nodes.size();
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> need_h[i] >> b[i] >> c[i];
    }
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    build_rooted_tree();

    // 二分答案:保证 1e9 天内有解
    ll left = 1;
    ll right = 1000000000LL;
    while (left < right) {
        ll mid = (left + right) / 2;
        if (check(mid)) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }

    cout << left << '\n';
    return 0;
}

复杂度

二分答案需要 O(log⁡109)O(\log 10^9) 次检查。

每次检查中,每个点二分一次最晚种植日,复杂度 O(nlog⁡T)O(n \log T);调度优先队列复杂度 O(nlog⁡n)O(n \log n)。

总时间复杂度 O(log⁡109⋅(nlog⁡T+nlog⁡n))O(\log 10^9 \cdot (n \log T + n \log n)),空间复杂度 O(n)O(n)。

总结

本题的关键是把"最后高度是否足够"转成每个点的最晚种植日。之后它就变成一个带树上先后约束的截止日期调度问题。

固定答案后,按子树最早截止时间优先的贪心负责判断是否能排出合法种植顺序;外层二分负责找到最小完成天数。