P9755 [CSP-S 2023] 种树【题解】
题意简述
给定一棵树。每个结点对应一个地块,每天选择一个 未种树 并且 与某个已种树的地块直接邻接(即通过单条道路连接) 的地块种下一棵高度为 000 米的树,若所有地块已种树,无需操作。第一天只能在根节点种树。对于在 iii 地块的树,从种下的当天开始,每天都会生长 max(bi+x×ci,1)max(b_i + x \times c_i,1)max(bi+x×ci,1) 米,特别注意:xix_ixi 是整个任务已经开始的天数,不是种下这棵树的天数。要使得每棵树的高度 ≥ai\ge a_i≥ai ,最少需要多少天?
思路
二分答案。
可以发现一个性质:已种树地块的扩展方向是固定的,必须从根节点向下扩展(如果一个结点已经有树,意味着它到根节点的路径上每个点都已经有树,不必再考虑)。
N≤1e5N \le 1e5N≤1e5 ,暴力枚举每次种哪个结点肯定是不行的。
25pts25pts25pts 的特殊性质 AAA :ci=0c_i = 0ci=0 ,换句话说,第几天种下这棵树对它生长没有任何影响,每天要么生长 bib_ibi ,要么生长 111 。
10pts10pts10pts 的特殊性质 BBB :链,很好做。
15pts15pts15pts 的特殊性质 CCC :除根节点每个结点最多只有一个孩子。
15pts15pts15pts 的特殊性质 DDD :菊花图。
这题如果在赛时,想不出正解应当冲暴力+特殊性质的 80pts80pts80pts ,也是挺划算的。
可以发现一个性质:每个地块每天可以生长的高度是固定的。每个 xxx 对应唯一高度,不为任何因素所干扰。所以是否可以提前预处理出这个东西?但是天数太多了,存不下。
二分的值域是 [n,1e9][n,1e9][n,1e9] 。数学推导一下?
对于每个结点,可以求出一个分界点日期 x0x_0x0 ,此前(后)每天生长取函数值,此后(前)每天生长取 111 。有何意义?设当前二分的答案是 midmidmid ,要判定当前局面是否可行,可以处理出在 midmidmid 天内要长到 aia_iai ,最晚 要在哪一天种下。对于每个地块:
若 c>0c > 0c>0 ,则在 $x_0 = \frac{1-b}{c} $ 天之后,每天生长 b+x×cb+x \times cb+x×c 米,每多一天,每天多生长 ccc 米。
不对, bi≥1b_i \ge 1bi≥1 ,只要 ci>0c_i > 0ci>0 ,每天都取函数值。在第 xix_ixi 天种下,最终生长总高度即为 (mid−xi+1)b+(∑i=ximidi)c(mid - x_i + 1)b + (\sum_{i = x_i}^{mid}i)c(mid−xi+1)b+(∑i=ximidi)c ,其中 (∑i=ximidi)c(\sum_{i = x_i}^{mid}i)c(∑i=ximidi)c 可以简化为 12(xi+mid)(mid−xi+1)c\frac{1}{2}(x_i + mid)(mid - x_i + 1)c21(xi+mid)(mid−xi+1)c
换句话说,这个过程也可以二分?
大致思路好像有了,通过各种乱搞求出每个地块最晚种树的时间点,再用某种搜索来判定能否满足在每个地块的时间限制以前遍历到它(即在此种下树)。先验证这种方法可行性。
在当前 midmidmid 下,假设现已知道每个地块 iii 最晚要在第 xix_ixi 天被遍历。换句话说,用 dfndfndfn 记录被遍历到的时间,判断是否存在一种遍历树的方式,使得 dfni≤xidfn_i \le x_idfni≤xi 。这个操作怎么实现?感觉我这个思路是正解,问题就在乎如何完成上述操作。
首先深度超了肯定不行,怎么遍历都遍历不到。
贪心?xix_ixi 越小的点优先级越大,第一次必然是从根节点往外扩展。不妨以样例为例试着扩展一下?
动态规划???有没有可能通过动态规划来判定?不妨考虑这样设计状态。
设 fif_ifi 表示在树中 iii 号结点最早可以在······不对,这样只考虑单个结点,不考虑别的结点肯定不行。
时间紧迫,问问 AIAIAI 。
其实想到这里,菊花图部分分已经可以做了。吗?
看了眼题解,果然我的思路是正解。而且看到题解中处理方法很妙。
修改定义:设 txt_xtx 表示 xxx 号地块最晚种树时间,将 ttt 数组升序排序,从头遍历:对于当前 iii 地块已经被种树,跳过;否则向上攀爬到 最近的 ,已经种树 的 祖先,将祖先到该结点的所有地块按序种上树。这样的贪心为什么正确?显然没有被种的地块 ttt 一定更大,它们可以延后考虑,那我把它们提前种了也无妨,而当前结点是迫在眉睫的,所以从祖先种到当前结点。卧槽,太妙了。
先前遗留两个问题没考虑:
c=0c = 0c=0 时,$t_x = \left \lceil \frac{a_x}{b_x} \right \rceil $
c<0c < 0c<0 时,在 x0≤⌊1−bc⌋x_0 \le \left \lfloor \frac{1 - b}{c} \right \rfloorx0≤⌊c1−b⌋ 时,取函数值,反之取 111 。求出这个转折点就行。
综上所述,重新理一下思路
思路整理
本题要求最少天数,单调性是显然的。(后续让 deepseekdeepseekdeepseek 证明)
接下来,二分答案 midmidmid,考虑非平方复杂度判断 midmidmid 是否可行:观察数据可以发现,每个地块每天的生长高度不被其他因素影响,所以可以利用这个性质,快速求解出在 midmidmid 范围内,使得指定地块的树长到指定高度的最晚种树时间 txt_xtx,这个过程的思路也很自然。求解 txt_xtx 的过程利用一次函数单调性二分即可。
最后,现已知道每个地块最晚种树时间 txt_xtx ,如何线性判定能否满足每个点的需求?考虑贪心:由于 txt_xtx 越小越紧迫,所以按 txt_xtx 升序排序,对于当前遍历到的结点,如果已经被种,直接跳过;如果没有,跳到 最近的、已被种植的祖先节点 ,将祖先结点到该结点的路径上所有结点依次种树。为什么是对的?首先,txt_xtx 更大的结点不会干扰当前结点的结果,在先前过程中如果把它种了更好。其次,正如我观察到的性质,一个结点要种树,其到根节点的路径上必然都已经被种,这是个必要条件,于是必须将祖先节点到该结点路径上所有结点种树,且这一过程种的每棵树都是必要的,不会产生冗余,浪费时间。
因此,本题的思路已经明朗,时间复杂度为 O(logv×(nlogn+n))O(\log v \times (n \log n + n))O(logv×(nlogn+n)) ,其实复杂度这个东西挺难精确求出的,均摊下来,最终复杂度为 O(nlognlogv)O(n \log n \log v)O(nlognlogv)。
代码实现的时候,二分求解 txtxtx 的过程写的不对,暂时没调出来。下午继续。
重新思考:
1.ci>0c_i > 0ci>0
此时每天生长高度都取函数值 bi+x×cib_i + x \times c_ibi+x×ci ,最后一天生长高度为 bi+mid×cib_i + mid \times c_ibi+mid×ci ,设在第 x0x_0x0 天种下,则总生长高度即为 ((bi+x0×ci)+(bi+mid×ci))×(mid−x0+1)÷2((b_i + x_0 \times c_i) + (b_i + mid \times c_i)) \times (mid - x_0 + 1) \div 2((bi+x0×ci)+(bi+mid×ci))×(mid−x0+1)÷2 。
2.ci<0c_i < 0ci<0
求出分界点 divdivdiv ,使得 [1,div][1,div][1,div] 取函数值生长,(div,mid](div,mid](div,mid] 取 111 。
$b_i + x \times c_i \ge 1 \to $
x×ci≥1−bi→x \times c_i \ge 1 - b_i \tox×ci≥1−bi→
x≤⌊1−bici⌋x \le \left \lfloor \frac{1 - b_i}{c_i} \right \rfloorx≤⌊ci1−bi⌋
代码实现
调了 infinfinf 次。前后历时 555 天 ACACAC。
几个问题:
1.本题数据规模极大,总和生长高度远超 long longlong \space longlong long ,正解应当使用 int128int128int128 。
2.二次重构代码的时候出现了弱智错误,如遍历 txtxtx 数组的时候应当用 vis[pos]vis[pos]vis[pos] 而不是 vis[i]vis[i]vis[i] ,调了半天没调出来。
3.边界条件处理不严谨,包括但不限于 divdivdiv 和 ci==0c_i == 0ci==0 时向上取整的问题,还有处理 ci<0c_i < 0ci<0 时 divdivdiv 是否在 [1,val][1,val][1,val] 区间内的细节问题。
总而言之,本题细节很多,想实现好需要足够细心。
代码太丑陋,放 AIAIAI 整理过的吧。整体框架不变。
#include <bits/stdc++.h>
using namespace std;
typedef __int128 ii;
const int N = 1e5 + 100;
int n;
int fa[N], req[N];
bool vis[N];
struct node1 {
ii a, b, c;
} arr[N];
struct node2 {
ii t;
int id;
} tx[N];
vector<int> gra[N];
void read(ii &res) {
res = 0;
bool neg = false;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') neg = true;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
res = res * 10 + (ch - '0');
ch = getchar();
}
if (neg) res = -res;
}
void dfs(int u, int f) {
fa[u] = f;
for (auto v : gra[u]) {
if (v == f) continue;
dfs(v, u);
}
}
bool cmp(node2 x, node2 y) {
return x.t < y.t;
}
bool check(int val) {
for (int i = 1; i <= n; i++) {
vis[i] = false;
tx[i].id = i;
ii ai = arr[i].a;
ii bi = arr[i].b;
ii ci = arr[i].c;
if (ci > 0) {
int l = 1, r = val, mid, ans = -1;
while (l <= r) {
mid = (l + r) / 2;
ii sum = ((bi + mid * ci) + (bi + val * ci)) * (val - mid + 1) / 2;
if (sum >= ai) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
if (ans == -1) return false;
tx[i].t = ans;
} else if (ci == 0) {
ii t = (ai + bi - 1) / bi; // 正确向上取整
if (t > val) return false;
tx[i].t = val - t + 1;
} else {
// 计算线性增长部分结束的天数
ii div = (1 - bi) / ci;
if ((1 - bi) % ci != 0 && (1 - bi) > 0) div++; // 处理余数,确保div是最后一个满足b_i +x*c_i >=1的x
int l = 1, r = val, mid, ans = -1;
while (l <= r) {
mid = (l + r) / 2;
ii sum = 0;
if (mid <= div) {
ii end = min(div, (ii)val);
ii cnt = end - mid + 1;
ii a1 = bi + mid * ci;
ii an = bi + end * ci;
sum = (a1 + an) * cnt / 2;
sum += max((ii)0, val - end); // 剩余天数每天增长1
} else {
sum = val - mid + 1; // 全部增长为1
}
if (sum >= ai) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
if (ans == -1) return false;
tx[i].t = ans;
}
req[i] = tx[i].t;
}
stack<int> sta;
sort(tx + 1, tx + n + 1, cmp);
vis[0] = true; // 虚拟根节点已访问
int times = 0;
for (int i = 1; i <= n; i++) {
int pos = tx[i].id;
if (vis[pos]) continue;
// 向上查找直到已访问的祖先
while (!vis[pos]) {
sta.push(pos);
pos = fa[pos];
}
// 按顺序种树
while (!sta.empty()) {
pos = sta.top();
sta.pop();
times++;
if (times > req[pos]) {
return false;
}
vis[pos] = true;
}
}
return true;
}
void solve() {
int l = 1, r = 1e9, mid, ans = -1;
while (l <= r) {
mid = (l + r) / 2;
if (check(mid)) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
printf("%d\n", ans);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
read(arr[i].a);
read(arr[i].b);
read(arr[i].c);
}
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d %d", &u, &v);
gra[u].push_back(v);
gra[v].push_back(u);
}
dfs(1, 0);
solve();
return 0;
}
总结
本题耗时长的关键原因:
1.代码中出现变量引用错误的弱智错误。
2.向上下取整运算的不严谨。
3.边界条件考虑不全。
好题。综合考察了二分与贪心。做的过程中明显感觉贪心过程独立思考难以想出,后续要加强贪心的练习。 CNOICNOICNOI 近几年考的贪心有点意思。