[CSP-S 2023] 种树
题目描述
你是一个森林养护员,有一天,你接到了一个任务:在一片森林内的地块上种树,并养护至树木长到指定的高度。
森林的地图有 n n n 片地块,其中 1 1 1 号地块连接森林的入口。共有 n − 1 n-1 n−1 条道路连接这些地块,使得每片地块都能通过道路互相到达。最开始,每片地块上都没有树木。
你的目标是:在每片地块上均种植一棵树木,并使得 i i i 号地块上的树的高度生长到不低于 a i a_i ai 米。
你每天可以选择一个未种树且与某个已种树的地块直接邻接(即通过单条道路相连)的地块,种一棵高度为 0 0 0 米的树。如果所有地块均已种过树,则你当天不进行任何操作。特别地,第 1 1 1 天你只能在 1 1 1 号空地种树。
对每个地块而言,从该地块被种下树的当天开始,该地块上的树每天都会生长一定的高度。由于气候和土壤条件不同,在第 x x x 天, i i i 号地块上的树会长高 max ( b i + x × c i , 1 ) \max(b_i + x \times c_i, 1) max(bi+x×ci,1) 米。注意这里的 x x x 是从整个任务的第一天,而非种下这棵树的第一天开始计算。
你想知道:最少需要多少天能够完成你的任务?
输入格式
输入的第一行包含一个正整数 n n n,表示森林的地块数量。
接下来 n n n 行:每行包含三个整数 a i , b i , c i a_i, b_i, c_i ai,bi,ci,分别描述一片地块,含义如题目描述中所述。
接下来 n − 1 n-1 n−1 行:每行包含两个正整数 u i , v i u_i, v_i ui,vi,表示一条连接地块 u i u_i ui 和 v i v_i vi 的道路。
输出格式
输出一行仅包含一个正整数,表示完成任务所需的最少天数。
样例 #1
样例输入 #1
4
12 1 1
2 4 -1
10 3 0
7 10 -2
1 2
1 3
3 4
样例输出 #1
5
提示
【样例 1 解释】
第 1 1 1 天:在地块 1 1 1 种树,地块 1 1 1 的树木长高至 2 2 2 米。
第 2 2 2 天:在地块 3 3 3 种树,地块 1 , 3 1, 3 1,3 的树木分别长高至 5 , 3 5, 3 5,3 米。
第 3 3 3 天:在地块 4 4 4 种树,地块 1 , 3 , 4 1, 3, 4 1,3,4 的树木分别长高至 9 , 6 , 4 9, 6, 4 9,6,4 米。
第 4 4 4 天:在地块 2 2 2 种树,地块 1 , 2 , 3 , 4 1, 2, 3, 4 1,2,3,4 的树木分别长高至 14 , 1 , 9 , 6 14, 1, 9, 6 14,1,9,6 米。
第 5 5 5 天:地块 1 , 2 , 3 , 4 1, 2, 3, 4 1,2,3,4 的树木分别长高至 20 , 2 , 12 , 7 20, 2, 12, 7 20,2,12,7 米。
【样例 2】
见选手目录下的 tree/tree2.in
与 tree/tree2.ans
。
【样例 3】
见选手目录下的 tree/tree3.in
与 tree/tree3.ans
。
【样例 4】
见选手目录下的 tree/tree4.in
与 tree/tree4.ans
。
【数据范围】
对于所有测试数据有: 1 ≤ n ≤ 1 0 5 , 1 ≤ a i ≤ 1 0 18 , 1 ≤ b i ≤ 1 0 9 , 0 ≤ ∣ c i ∣ ≤ 1 0 9 , 1 ≤ u i , v i ≤ n 1 ≤ n ≤ 10^5,1 ≤ a_i ≤ 10^{18}, 1 ≤ b_i ≤ 10^9,0 ≤ |c_i| ≤ 10^9, 1 ≤ u_i, v_i ≤ n 1≤n≤105,1≤ai≤1018,1≤bi≤109,0≤∣ci∣≤109,1≤ui,vi≤n。保证存在方案能在 1 0 9 10^9 109 天内完成任务。
特殊性质 A:对于所有 1 ≤ i ≤ n 1 ≤ i ≤ n 1≤i≤n,均有 c i = 0 c_i = 0 ci=0;
特殊性质 B:对于所有 1 ≤ i < n 1 ≤ i < n 1≤i<n,均有 u i = i , v i = i + 1 u_i = i,v_i = i + 1 ui=i,vi=i+1;
特殊性质 C:与任何地块直接相连的道路均不超过 2 2 2 条;
特殊性质 D:对于所有 1 ≤ i < n 1 ≤ i < n 1≤i<n,均有 u i = 1 u_i = 1 ui=1。
考场上写了性质 A A A 和性质 B B B,虽然都写挂了,不过确实离正解不远了,所以在讲正解之前先把这两个性质讲一下。
性质 A
题目中告诉我们 c i = 0 c_i=0 ci=0 ,这说明烦人的一次函数不见了,进一步的,我们可以直接算出每一个点需要多少的生长时间。求出来后,仿效P4437的贪心结论,全局中一个最大生长时间的点(设为 x x x )一定会在他父亲被选完后立即被选,我们不妨把 x x x 与 f a [ x ] fa[x] fa[x] 缩成一个点,点权为 m a x ( t [ x ] + s i z e [ f a [ x ] ] , t [ f a [ x ] ] ) max(t[x]+size[fa[x]],t[fa[x]]) max(t[x]+size[fa[x]],t[fa[x]])( s i z e [ x ] size[x] size[x] 表示 x x x 这个点缩完点后的集合大小, + s i z e [ f a [ x ] ] +size[fa[x]] +size[fa[x]] 的含义是表明在父亲所在集合都种完树后再在 x x x 上种树 ,考场上就是这里写挂了。。),这样我们开一个堆,一直合并即可。时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn) ,靠这个思路就可以把整道题做完了。
for(int i=2;i<=n;i++)
q.push(make_pair(t[i],i));
while(!q.empty())
{
int x=q.top().second;
q.pop();
if(flag[x])
continue;
flag[x]=1;//注意每个点用完以后打上标记,防止一个点重复合并使答案异常化。
t[get(F[x])]=max(t[get(F[x])],t[x]+siz[get(F[x])]);
fa[x]=get(F[x]);
siz[get(F[x])]+=siz[x];
if(get(F[x])!=1)
q.push(make_pair(t[get(F[x])],get(F[x])));
}
printf("%lld",t[1]);
不过这一步在赛后发现可以优化。
优化:
把所有操作整体考虑,不难发现如果一个点
x
x
x 被选为全局最大生长时间的点时,之后被选的点实际上就是
f
a
[
x
]
fa[x]
fa[x] ,因为合并后
f
a
[
x
]
fa[x]
fa[x] 的点权一定大于
x
x
x 的点权,而
x
x
x 的点权又是之前一步最大的,所以
f
a
[
x
]
fa[x]
fa[x] 的点权一定是最大的。这样一直往上跳,直到之前用过的点。那么我们把所有点的价值计算出来之后从大到小排个序,从前往后访问数组,如果一个点还未加入操作序列,就把他和他的所有未被标记的祖先(一定是一段连续的链,所以往上跳的过程中遇到标记过的直接
b
r
e
a
k
break
break 来保证复杂度)加入操作序列,注意是从上往下加。
设点
x
x
x 在操作序列中的位置是 s[x] ,最后的
a
n
s
=
max
i
=
1
n
s
[
i
]
+
t
[
i
]
−
1
ans=\max\limits_{i=1}^ns[i]+t[i]-1
ans=i=1maxns[i]+t[i]−1
时间复杂度为
O
(
n
)
O(n)
O(n)。
另一种证明方式:
我们考虑不经过缩点的过程,直接证明上面贪心步骤的正确性。
若
t
[
x
]
>
t
[
y
]
t[x]>t[y]
t[x]>t[y] ,则有
s
[
x
]
<
s
[
y
]
s[x]<s[y]
s[x]<s[y] 。
用微扰排序的思想,我们尝试证明
s
[
x
]
>
s
[
y
]
s[x]>s[y]
s[x]>s[y] 的安排比
s
[
x
]
<
s
[
y
]
s[x]<s[y]
s[x]<s[y] 一定不优。
设
x
x
x 的前驱的个数为
n
u
m
x
num_x
numx ,
y
y
y 的前驱的个数为
n
u
m
y
num_y
numy
若
s
[
x
]
<
s
[
y
]
s[x]<s[y]
s[x]<s[y] ,则
x
,
y
x,y
x,y 对答案的贡献为
a
n
s
1
=
m
a
x
(
n
u
m
x
+
t
[
x
]
,
n
u
m
x
+
1
+
n
u
m
y
+
t
[
y
]
)
ans1=max(num_x+t[x],num_x+1+num_y+t[y])
ans1=max(numx+t[x],numx+1+numy+t[y])
若
s
[
x
]
>
s
[
y
]
s[x]>s[y]
s[x]>s[y] ,则
x
,
y
x,y
x,y 对答案的贡献为
a
n
s
2
=
m
a
x
(
n
u
m
y
+
t
[
y
]
,
n
u
m
y
+
1
+
n
u
m
x
+
t
[
x
]
)
=
n
u
m
y
+
1
+
n
u
m
x
+
t
[
x
]
ans2=max(num_y+t[y],num_y+1+num_x+t[x])=num_y+1+num_x+t[x]
ans2=max(numy+t[y],numy+1+numx+t[x])=numy+1+numx+t[x]
显然
a
n
s
1
≤
a
n
s
2
ans1\le ans2
ans1≤ans2 ,所以
s
[
x
]
<
s
[
y
]
s[x]<s[y]
s[x]<s[y] 的决策一定不劣。
性质 B
性质
B
B
B 告诉我们这是一条链,也就是说我们不需要考虑操作顺序的问题了。我们假设点
i
i
i 的生长时间区间为
[
L
i
,
R
i
]
[L_i,R_i]
[Li,Ri]
显然
L
i
L_i
Li 是定的,我们需要知道的是最小的
R
x
R_x
Rx 是多少。
发现
b
i
+
x
×
c
i
b_i + x \times c_i
bi+x×ci 其实是一段等差数列,所以我们可以二分
R
i
R_i
Ri ,用等差数列求和公式来
O
(
1
)
O(1)
O(1) 算出生长高度,然后与
a
i
a_i
ai 进行比较来验证
R
i
R_i
Ri 是否合法。但是它是
m
a
x
(
b
i
+
x
×
c
i
,
1
)
max(b_i + x \times c_i,1)
max(bi+x×ci,1) ,有可能不是一段完整的等差数列,怎么办呢?
开始大力分类讨论:
当
c
i
>
=
0
c_i>=0
ci>=0 时,
a
n
s
=
(
b
i
+
L
i
×
c
i
+
b
i
+
R
i
×
c
i
)
×
(
R
i
−
L
i
+
1
)
/
2
ans=(b_i+L_i\times c_i+b_i+R_i\times c_i)\times(R_i-L_i+1)/2
ans=(bi+Li×ci+bi+Ri×ci)×(Ri−Li+1)/2
当
c
i
<
0
c_i<0
ci<0 时,
b
i
+
x
×
c
i
≥
1
⇒
x
≤
⌊
1
−
b
i
c
i
⌋
b_i+x\times c_i\ge1 \Rightarrow x\le\left \lfloor\frac{1-b_i}{c_i}\right\rfloor
bi+x×ci≥1⇒x≤⌊ci1−bi⌋ 我们可以算出在什么时候取
b
i
+
x
×
c
i
b_i+x\times c_i
bi+x×ci ,什么时候取
1
1
1 。
设
t
=
⌊
1
−
b
i
c
i
⌋
t=\left \lfloor\frac{1-b_i}{c_i}\right\rfloor
t=⌊ci1−bi⌋ 。
\qquad
当
R
i
<
=
t
R_i<=t
Ri<=t 时,
a
n
s
=
(
b
i
+
L
i
×
c
i
+
b
i
+
R
i
×
c
i
)
×
(
R
i
−
L
i
+
1
)
/
2
ans=(b_i+L_i\times c_i+b_i+R_i\times c_i)\times(R_i-L_i+1)/2
ans=(bi+Li×ci+bi+Ri×ci)×(Ri−Li+1)/2
\qquad
当
L
i
>
t
L_i>t
Li>t 时,
a
n
s
=
R
i
−
L
i
+
1
ans=R_i-L_i+1
ans=Ri−Li+1
\qquad
当
L
i
<
=
t
L_i<=t
Li<=t 且
R
i
>
t
R_i>t
Ri>t 时,
a
n
s
=
(
b
i
+
L
i
×
c
i
+
b
i
+
t
×
c
i
)
×
(
t
−
L
i
+
1
)
/
2
+
R
i
−
t
ans=(b_i+L_i\times c_i+b_i+t\times c_i)\times(t-L_i+1)/2+R_i-t
ans=(bi+Li×ci+bi+t×ci)×(t−Li+1)/2+Ri−t
这样我们就可以直接递推加二分的求解出链的情况。
正解
我们发现性质
A
A
A 是考虑操作序列,不考虑等差数列,性质
B
B
B 是考虑等差数列,不考虑操作序列,我们尝试把两种做法结合一下。
对于变量,一个点的点权不太好算,再加上答案显然具有单调性,我们考虑二分答案。这样我们可以按照链的做法把每个点需要在第几天之前种下给计算出来,虽然不是生长时间了,但稍微思考一下发现这个点权和生长时间没什么太大区别,我们从小到大排序,然后仿照性质
A
A
A 的做法求出操作序列,最后验证二分出来的答案是否合法就行。
时间复杂度:
O
(
n
l
o
g
2
n
)
O(nlog^2n)
O(nlog2n)
#include<bits/stdc++.h>
using namespace std;
int n,head[100010],tot,F[100010];
bool v[100010];
vector<pair<int,int> > q;
struct node
{
int to,nex;
} edge[200000];
struct node1
{
long long a,b,c,t;
} a[100010];
void add(int x,int y)
{
edge[++tot].nex=head[x];
edge[tot].to=y;
head[x]=tot;
}
bool Check(int id,int l,int r)
{
if(a[id].c>=0)
{
__int128 h=(__int128)(a[id].b+l*a[id].c+a[id].b+r*a[id].c)*(r-l+1)/2;
if(h>=a[id].a)
return 1;
return 0;
}
if(l>(1-a[id].b)/a[id].c)
{
if(r-l+1>=a[id].a)
return 1;
return 0;
}
if(r<=(1-a[id].b)/a[id].c)
{
__int128 h=(__int128)(a[id].b+l*a[id].c+a[id].b+r*a[id].c)*(r-l+1)/2;
if(h>=a[id].a)
return 1;
return 0;
}
int t=(1-a[id].b)/a[id].c;
__int128 h=(__int128)(a[id].b+l*a[id].c+a[id].b+t*a[id].c)*(t-l+1)/2+r-t;
if(h>=a[id].a)
return 1;
return 0;
}
int work(int id,int x)
{
int l=0,r=x;
while(l+1<r)
{
int mid=(l+r)/2;
if(Check(id,mid,x))
l=mid;
else
r=mid;
}
if(Check(id,r,x))
return r;
return l;
}
bool check(int x)
{
q.clear();
for(int i=1;i<=n;i++)
{
int jl=work(i,x);
if(jl==0)
return 0;
q.push_back(make_pair(jl,i));
}
sort(q.begin(),q.end());
memset(v,0,sizeof v);
v[0]=1;
int now=0;
for(int i=0;i<q.size();i++)
{
int id=q[i].second;
while(!v[id])
{
now++;
v[id]=1;
id=F[id];
}
if(now>q[i].first)
return 0;
}
return 1;
}
void dfs(int x,int fa)
{
F[x]=fa;
for(int i=head[x];i;i=edge[i].nex)
{
if(edge[i].to==fa)
continue;
dfs(edge[i].to,x);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%lld%lld%lld",&a[i].a,&a[i].b,&a[i].c);
for(int i=1;i<n;i++)
{
int U,V;
scanf("%d%d",&U,&V);
add(U,V);
add(V,U);
}
dfs(1,0);
int l=1,r=1e9;
while(l+1<r)
{
int mid=(l+r)/2;
if(check(mid))
r=mid;
else
l=mid;
}
if(check(l))
printf("%d",l);
else
printf("%d",r);
return 0;
}
总结:一道题没有思路的时候,先去想想性质,或许会有不错的发现。