二分完成天数,把每个点转成最晚种植日,再用最早截止时间优先判断树上调度。
OJ: luogu
题目 ID: P9755
难度:提高+/省选-
标签:二分贪心树形结构优先队列
创建: 2026-07-06 08:46
更新: 2026-10-07 10:45
形式化题目
给定
若点
(
暴力解法
思路
二分 day 天在"未种、且与某种下的地块相邻"的地块里任选一个种下,检查每个点种下时是否不晚于自己的最晚种植日。
代码
/**
* 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;
}复杂度
- 时间:
级别,枚举所有合法种植顺序 - 空间:
瓶颈
枚举量阶乘级增长,只能处理
正解
思路
满分做法仍然二分答案,但检查
固定一个完成天数
如果从第
接下来问题变成:
在树上安排每天种一个点;
父亲必须早于儿子;
每个点必须不晚于自己的 deadline 被种下。这个调度可以用贪心判断,但优先级不能只看当前点自己的 deadline。如果某个点本身不急,但它的子树里有很急的后代,就应该尽早种它来打开这条分支。
所以对每个点
min_deadline[u] = u 的子树里最小的 deadline每天把当前已经可种的点放进优先队列,优先种 min_deadline 最小的点。这样会优先打开包含紧急节点的子树。种下某个点时,再检查它自己的 deadline 是否已经过期;如果过期,当前
计算某个点从
的部分,用等差数列求和; - 后面不足
的部分,每天按 计算。
代码
/**
* 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;
}复杂度
二分答案需要
每次检查中,每个点二分一次最晚种植日,复杂度
总时间复杂度
总结
本题的关键是把"最后高度是否足够"转成每个点的最晚种植日。之后它就变成一个带树上先后约束的截止日期调度问题。
固定答案后,按子树最早截止时间优先的贪心负责判断是否能排出合法种植顺序;外层二分负责找到最小完成天数。