P9755 [CSP-S 2023] 种树 题解
简化题意
给定一棵 \(n\) 个节点的树,\(1\) 号节点为根。初始所有节点未种树。
每天可以选择一个未种树且与已种树节点相邻的节点种树(第一天只能种 \(1\) 号节点)。若所有节点已种树,则当天不操作。
对于节点 \(i\),种下后第 \(x\) 天(从第一天算起)高度增加 \(\max(b_i + x \times c_i, 1)\)。
求使所有节点 \(i\) 的树高不低于 \(a_i\) 的最少天数。
做法
首先看完题目,答案显然是存在单调性的,\(i\) 天可以达成目标则 \(i+1\) 天也可以达成目标,所以考虑二分答案。
性质A
\(c_i\) 为 \(0\),说明可以算出来每个树需要多久才会长达标,因此我得出每棵树至少要在哪一天种下才会达标(记作 \(ne_i\))。 对于这一棵树,肯定是寻找最小的 \(ne_i\) 优先让它生长,由于需要它的父节点先种下,所以我们从它往上找,直到走到节点 \(1\) 或者是一个种过树的节点,然后在一次种下树即可。最后判断种下的时间是否达标。
点代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=100005;
int n;
int a[N],b[N],c[N];
vector<int>g[N];
struct no{
int id,mst;
}A[N];
bool cmp(no x,no y){
return x.mst<y.mst;
}
int fa[N];
int st[N],top;
int co[N],tot;
void subA_init(int u,int f){
fa[u]=f;
for(auto i:g[u]){
if(i!=f)subA_init(i,u);
}
return;
}
bool check(int x){
for(int i=1;i<=n;i++){
A[i]={i,x-(int)ceil(1.0*a[i]/b[i])+1};
// if(x==6)cout<<A[i].mst<<' ';
if(A[i].mst<=0)return 0;
co[i]=0;
}
tot=0;
co[1]=++tot;
sort(A+1,A+n+1,cmp);
for(int i=1;i<=n;i++){
if(co[A[i].id])continue;
int x=A[i].id;
while(!co[x]){
st[++top]=x;
x=fa[x];
}
for(int j=top;j>=1;j--)co[st[j]]=++tot;
top=0;
}
for(int i=1;i<=n;i++)if(co[A[i].id]>A[i].mst)return 0;
return 1;
}
void subA(){
subA_init(1,0);
int l=0,r=1000000000;
while(l+1<r){
int mid=(l+r)>>1;
if(check(mid))r=mid;
else l=mid;
}
cout<<r;
return;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i]>>b[i]>>c[i];
int Sum1=0,Sum2=0,Sum3=0;
for(int i=1;i<n;i++){
int u,v;cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
if(v!=u+1)Sum2++;
if(u!=1)Sum3++;
}
for(int i=1;i<=n;i++){
if(c[i])Sum1++;
}
if(!Sum1)subA();
// if(!Sum2)subB();
// if(!Sum3)subD();
return 0;
}
性质B
给的是一条链,也就是可以种树的顺序确定了,我们可以写一个函数判断第 \(i\) 棵树从 \(l\) 时刻生长到 \(r\) 时刻可以长高多少,分三种情况讨论 :
代码
__int128 checkB(int i,__int128 l,__int128 r){
if(c[i]>=0)return (2*b[i]+c[i]*l+c[i]*r)*(r-l+1)/2;//直接算
__int128 t=(1-b[i])/c[i];
if(l>t)return r-l+1;//全是1
if(r<t)return (2*b[i]+c[i]*l+c[i]*r)*(r-l+1)/2;//全不是1
return (2*b[i]+c[i]*l+c[i]*t)*(t-l+1)/2+r-t;//各一半
}
由于生长高度随时间变化而变化,所以是一个梯形,直接求面积即可,然后 \(t\) 则是根据 \(b_i-c_i \times t >=1\) 转换而成。
正解
有了这两个公式就可以直接得到正解,将两个部分分拼到一起即可,用二分得到 \(ne\) 数组。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=100005;
int n;
int a[N],b[N],c[N];
vector<int>g[N];
__int128 get(int i,__int128 l,__int128 r){
if(c[i]>=0)return (b[i]*2+c[i]*l+c[i]*r)*(r-l+1)/2;
__int128 t=(1-b[i])/c[i];//变成1的前一天
if(t<l)return r-l+1;
else if(t>=r)return (b[i]*2+c[i]*l+c[i]*r)*(r-l+1)/2;
return (b[i]*2+c[i]*l+c[i]*t)*(t-l+1)/2+r-t;
}
int sta(int i,int x){
int l=1,r=x+1;
while(l+1<r){
int mid=(l+r)>>1;
if(get(i,mid,x)>=a[i])l=mid;
else r=mid;
}
return l;
}
struct no{
int id,ne;
}q[N];
bool cmp(no x,no y){
return x.ne<y.ne;
}
int co[N];
int st[N],top,tot;
int nd[N];
int fa[N];
void dfs(int u,int f){
fa[u]=f;
for(auto i:g[u]){
if(i==f)continue;
dfs(i,u);
}
return;
}
bool check(int x){
for(int i=1;i<=n;i++){
if(get(i,1,x)<a[i])return 0;//一定要加这句话,不然就是40pts
co[i]=0;nd[i]=sta(i,x);
q[i]={i,nd[i]};
}
sort(q+1,q+n+1,cmp);
tot=0;
co[1]=++tot;
for(int i=1;i<=n;i++){
int pos=q[i].id;
if(co[pos])continue;
top=0;
while(!co[pos]){
st[++top]=pos;
pos=fa[pos];
}
for(int j=top;j>=1;j--){
co[st[j]]=++tot;
if(co[st[j]]>nd[st[j]])return 0;
}
}
return 1;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[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);
}
dfs(1,0);
int l=0,r=1000000000;
while(l+1<r){
int mid=(l+r)>>1;
if(check(mid))r=mid;
else l=mid;
}
cout<<r;
return 0;
}

浙公网安备 33010602011771号