二分完成天数,把每个点转成最晚种植日,再用最早截止时间优先判断树上调度。
OJ: luogu
题目 ID: P9755
难度:提高+/省选-
标签:二分贪心树形结构优先队列
日期: 2026-07-06 08:46
题意
有一棵以 1 号点连接入口的树。每天最多种一棵树,且只能在已经种树地块的相邻空地种树,所以在以 1 为根后,每个点必须在父亲之后种下。
如果第 i 个点在第 d 天种下,到第 T 天时,它获得的总高度为:
sum_{x=d..T} max(b_i + x*c_i, 1)要求所有点高度都不低于 T。
思路
小数据可以二分 T,然后递归枚举所有合法的连通种植顺序:
// 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,我们可以二分出它的最晚种植日
sum_{x=deadline[i]..T} max(b_i + x*c_i, 1) >= a_i如果从第 1 天种到第 T 天都达不到 T 一定不可行。
接下来问题变成:
在树上安排每天种一个点;
父亲必须早于儿子;
每个点必须不晚于自己的 deadline 被种下。这个调度可以用贪心判断,但优先级不能只看当前点自己的 deadline。如果某个点本身不急,但它的子树里有很急的后代,就应该尽早种它来打开这条分支。
所以对每个点 u 计算:
min_deadline[u] = u 的子树里最小的 deadline每天把当前已经可种的点放进优先队列,优先种 min_deadline 最小的点。这样会优先打开包含紧急节点的子树。种下某个点时,再检查它自己的 deadline 是否已经过期;如果过期,当前 T 不可行。
计算某个点从 d 到 T 的生长量时,要处理 1,所以把区间分成两段:
的部分,用等差数列求和; - 后面不足
1的部分,每天按1计算。
代码
// 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;
}复杂度
二分答案需要
每次检查中,每个点二分一次最晚种植日,复杂度
总时间复杂度为
总结
本题的关键是把“最后高度是否足够”转成每个点的最晚种植日。之后它就变成一个带树上先后约束的截止日期调度问题。
固定答案后,按子树最早截止时间优先的贪心负责判断是否能排出合法种植顺序;外层二分负责找到最小完成天数。