[CSP-S 2023] 种树

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

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

OJ: luogu

题目 ID: P9755

难度:提高+/省选-

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

日期: 2026-07-06 08:46

题意

有一棵以 1 号点连接入口的树。每天最多种一棵树,且只能在已经种树地块的相邻空地种树,所以在以 1 为根后,每个点必须在父亲之后种下。

如果第 i 个点在第 d 天种下,到第 T 天时,它获得的总高度为:

text
sum_{x=d..T} max(b_i + x*c_i, 1)

要求所有点高度都不低于 aia_i ,求最小完成天数 T

思路

小数据可以二分 T,然后递归枚举所有合法的连通种植顺序:

cpp
// brute.cpp:小数据暴力解,二分天数后递归枚举所有合法的连通种植顺序。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 12;

int n;
long long need_h[MAXN], b[MAXN], c[MAXN];
bool edge[MAXN][MAXN];
int deadline_day[MAXN];
bool planted[MAXN];

__int128 sum_linear(long long bb, long long cc, long long l, long long 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;
}

__int128 growth_sum(int u, long long l, long long r) {
    if (l > r) return 0;
    if (c[u] >= 0) return sum_linear(b[u], c[u], l, r);
    long long dec = -c[u];
    long long last_big = (b[u] - 1) / dec;
    long long 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;
}

int calc_deadline(int u, long long total_day) {
    if (growth_sum(u, 1, total_day) < need_h[u]) return 0;
    long long left = 1, right = total_day;
    while (left < right) {
        long long 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 (int)left;
}

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

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

bool check(long long 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;
    }

    long long left = 1, right = 200;
    while (!check(right)) {
        right *= 2;
    }
    while (left < right) {
        long long mid = (left + right) / 2;
        if (check(mid)) right = mid;
        else left = mid + 1;
    }
    cout << left << '\n';
    return 0;
}

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

固定一个完成天数 T。对每个点 i,我们可以二分出它的最晚种植日 deadline[i]deadline[i]

text
sum_{x=deadline[i]..T} max(b_i + x*c_i, 1) >= a_i

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

接下来问题变成:

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

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

所以对每个点 u 计算:

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

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

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

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

代码

cpp
// main.cpp:二分完成天数,把每个点转成最晚种植日,再做树上 EDF 调度。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;

int n;
long long need_h[MAXN], b[MAXN], c[MAXN];
vector<int> g[MAXN], child[MAXN];
int deadline_day[MAXN];
int min_subtree_deadline[MAXN];
vector<int> order_nodes;

__int128 sum_linear(long long bb, long long cc, long long l, long long 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;
}

__int128 growth_sum(int u, long long l, long long r) {
    if (l > r) {
        return 0;
    }
    if (c[u] >= 0) {
        return sum_linear(b[u], c[u], l, r);
    }

    long long dec = -c[u];
    long long last_big = (b[u] - 1) / dec; // x <= last_big 时 b-c*x 至少为 1
    long long 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;
}

int calc_deadline(int u, long long total_day) {
    if (growth_sum(u, 1, total_day) < need_h[u]) {
        return 0;
    }

    long long left = 1;
    long long right = total_day;
    while (left < right) {
        long long 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 (int)left;
}

bool check(long long 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;
        }
    }

    for (int i = (int)order_nodes.size() - 1; i >= 0; i--) {
        int u = order_nodes[i];
        min_subtree_deadline[u] = deadline_day[u];
        for (int j = 0; j < (int)child[u].size(); j++) {
            int v = child[u][j];
            min_subtree_deadline[u] = min(min_subtree_deadline[u], min_subtree_deadline[v]);
        }
    }

    priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > q;
    q.push(make_pair(min_subtree_deadline[1], 1));

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

    return true;
}

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);
        for (int i = 0; i < (int)g[u].size(); i++) {
            int v = g[u][i];
            if (v == parent[u]) {
                continue;
            }
            parent[v] = u;
            child[u].push_back(v);
            q.push(v);
        }
    }
}

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();

    long long left = 1;
    long long right = 1000000000LL;
    while (left < right) {
        long long mid = (left + right) / 2;
        if (check(mid)) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }

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

复杂度

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

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

总时间复杂度为 O(log109(nlogT+nlogn))O(\log 10^9 \cdot (n \log T + n \log n)),空间复杂度为 O(n)O(n)

总结

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

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