(贪心、数学、二分)洛谷 P9755 CSPS2023 种树 题解
题意
题目传送门,建议前往阅读题面、样例和部分分。
思路
题目明示我们,不能直接模拟种树和树的生长过程——答案不超过 10910^9109。可以考虑决定种树的次序然后 O(n)O(n)O(n) 种树,外面套一个二分答案 O(logV)O(\log V)O(logV)。
问题转变为能不能在 midmidmid 天内完成。
发现特殊性质 A 是 ci=0c_i=0ci=0,那么树 iii 的生长速度是每天 bib_ibi。我们容易计算,至少要在哪一天要种下树 iii,才能使得 iii 长到 aia_iai。记这个限制为 latei=mid−⌈aibi⌉late_i=mid-\left\lceil \frac{a_i}{b_i} \right\rceillatei=mid−⌈biai⌉。
那么这里有个贪心,我们对 lateilate_ilatei 排序,优先种 lateilate_ilatei 小的显然是优的。
怎么种树呢?题目说,你每天可以选择一个未种树且与某个已种树的地块直接邻接的地块种树。因此对于目标种树点 iii,每次往上跳直到遇到第一个已经被种树的节点,用一个栈记录经过的结点,按照出栈序种树。我们在 nnn 的时间内种完所有树。
由于不会重复种树,种树均摊下来时间复杂度 O(n)O(n)O(n),排序那里 O(nlogn)O(n\log n)O(nlogn)。
bool cmp(node x,node y)
{return x.lat<y.lat;
}
...
memset(vis,0,sizeof(vis));
vis[0]=1;//1的父亲是0
sort(p+1,p+n+1,cmp);
ll tick=0;
for(int i=1;i<=n;i++)
{ll u=p[i].ord;if(vis[u])continue;vis[u]=1;stk.push(u);while(1){if(vis[fat[u]])break;u=fat[u];stk.push(u);vis[u]=1;}while(!stk.empty())//树上从上往下种树{ll tem=stk.top();stk.pop();tick++;if(lat[tem]<tick)return 0;}
}
我们发现树 iii 的生长量很猎奇:max{1,bi+xci}\max\{1,b_i+xc_i\}max{1,bi+xci},其中 xxx 表示整个种树过程的第 xxx 天,也就是说这棵树某天 lll 开始到 rrr 天结束一直生长。于是考虑快速计算 cal(i,l,r)
。
如果 ci≥0c_i\ge 0ci≥0,cal(i,l,r)
=∑x=lrbi+xci=(r−l+1)bi+sum(l,r)ci=\displaystyle\sum _{x=l}^rb_i+xc_i=(r-l+1)b_i+sum(l,r)c_i=x=l∑rbi+xci=(r−l+1)bi+sum(l,r)ci。
其中 sum(l,r)=(l+r)×(r−l+1)÷2sum(l,r)=(l+r)\times(r-l+1)\div 2sum(l,r)=(l+r)×(r−l+1)÷2,等差数列求和容易 O(1)O(1)O(1) 计算。
而 ci<0c_i<0ci<0 的时候一次函数 f(x)=bi+xcif(x)=b_i+xc_if(x)=bi+xci 可能跌到 111 以下,考虑计算 xxx 在哪里最后一次 f(x)≥1f(x)\ge 1f(x)≥1:f(x)=bi+xci≥1f(x)=b_i+xc_i\ge 1f(x)=bi+xci≥1,解得 x≤pos=⌊1−bici⌋x\le pos=\left\lfloor \frac{1-b_i}{c_i} \right\rfloorx≤pos=⌊ci1−bi⌋。
于是 x≤posx\le posx≤pos,max{1,bi+xci}\max\{1,b_i+xc_i\}max{1,bi+xci} 为后者,否则前者。若 l≤pos≤rl\le pos\le rl≤pos≤r,则 cal(i,l,r)
=(pos−l+1)bi+sum(l,pos)ci+r−pos=(pos-l+1)b_i+sum(l,pos)c_i+r-pos=(pos−l+1)bi+sum(l,pos)ci+r−pos。分类讨论 pospospos 与 l,rl,rl,r 的大小关系即可。
ll cal(ll id,ll l,ll r)
{if(c[id]>=0)return (r-l+1)*b[id]+sum(l,r)*c[id];ll pos=(1-b[id])/c[id];if(pos<l)return r-l+1;if(pos>=r)return (r-l+1)*b[id]+sum(l,r)*c[id];return (pos-l+1)*b[id]+sum(l,pos)*c[id]+r-pos;
}
考虑重新计算此处的 lateilate_ilatei。我们发现钦定了一个限制天数 midmidmid,相当于 r=midr=midr=mid,我们想要计算 cal(i,l,mid)
≥ai\ge a_i≥ai 的最小的 lll,即要求 lateilate_ilatei。我们发现 lll 越大 cal
计算结果越小,具有单调性,因此这个 lll 可以二分得到。
for(int i=1;i<=n;i++)
{ll tem=cal(i,1,mid);if(cal(i,1,mid)<a[i])return 0;//在限制内不能长到 a[i]ll il=1,ir=n,ret=1;//因为在n的时间内种完所有树,因此l的二分值域只有[1,n]while(il<=ir){ll imid=(il+ir)>>1;if(cal(i,imid,mid)>=a[i])ret=imid,il=imid+1;else ir=imid-1;}lat[i]=ret;p[i]=(node){ret,i};
}
时间复杂度 O(nlogVlogn)O(n\log V\log n)O(nlogVlogn)。
到这里我们发现,这题有贪心思想还有二分,还有可爱的小奥,综合性比较高捏!
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define i8 __int128
const ll N=1e5+9;
ll n;
vector<ll>G[N];
ll a[N],b[N],c[N];
i8 sum(ll l,ll r)
{return (l+r)*(r-l+1)/2;
}
i8 cal(ll id,ll l,ll r)
{if(c[id]>=0)return (r-l+1)*b[id]+sum(l,r)*c[id];ll pos=(1-b[id])/c[id];if(pos<l)return r-l+1;if(pos>=r)return (r-l+1)*b[id]+sum(l,r)*c[id];return (pos-l+1)*b[id]+sum(l,pos)*c[id]+r-pos;
}
ll fat[N];
void dfs(ll u,ll fa)
{fat[u]=fa;for(auto v:G[u]){if(v==fa)continue;dfs(v,u);}
}
struct node
{i8 lat;ll ord;
}p[N];
i8 lat[N];
bool cmp(node x,node y)
{return x.lat<y.lat;
}
bool vis[N];
stack<ll>stk;
bool check(ll mid)
{while(!stk.empty())stk.pop();for(int i=1;i<=n;i++){ll tem=cal(i,1,mid);if(cal(i,1,mid)<a[i])return 0;ll il=1,ir=n,ret=1;while(il<=ir){ll imid=(il+ir)>>1;if(cal(i,imid,mid)>=a[i])ret=imid,il=imid+1;else ir=imid-1;}lat[i]=ret;p[i]=(node){ret,i};}memset(vis,0,sizeof(vis));vis[0]=1;//1的父亲是0 sort(p+1,p+n+1,cmp);ll tick=0;for(int i=1;i<=n;i++){ll u=p[i].ord;if(vis[u])continue;vis[u]=1;stk.push(u);while(1){if(vis[fat[u]])break;u=fat[u];stk.push(u);vis[u]=1;}while(!stk.empty()){ll tem=stk.top();stk.pop();tick++;if(lat[tem]<tick)return 0;}}return 1;
}
int main()
{scanf("%lld",&n);for(int i=1;i<=n;i++)scanf("%lld%lld%lld",&a[i],&b[i],&c[i]);for(int i=1;i<n;i++){ll u,v;scanf("%lld%lld",&u,&v);G[u].push_back(v);G[v].push_back(u);}dfs(1,0);ll l=n,r=1e9,ans=1e9;while(l<=r){ll mid=(l+r)>>1;if(check(mid))ans=mid,r=mid-1;else l=mid+1;}printf("%lld",ans);return 0;
}