UOJ#84-[UR #7]水题走四方【dp】
正题
题目链接:https://uoj.ac/problem/84
题目大意
有\(n\)个点的一棵树,\(1\)为根,两个人从根节点往下走(只能从深度小的点走到深度大的点)。
两个人每一秒都可以一条边(也可以不移动),或者不消耗时间将一个人传送到另一个人那里。
求遍历整棵树需要的最少时间。
\(1\leq n\leq 5\times 10^6\)
解题思路
什么神仙题。
首先考虑到肯定存在一种方案使得一个人不需要使用传送,因为显然两个人都需要使用传送的方案可以通过交换两个人的某些路径得到有一个人不需要传送的方案。
那么说明其中一个人的路径肯定是一条链,我们将这个人称之为本体,另一个人称为分身。
然后一个粗暴的想法是\(dp\)出一条路径,然后本体每走到一个点就等分身遍历完除了链上的其他子树再往下一个节点走。设\(f_i\)表示走到节点\(i\)的答案。
但是这样显然是会出问题的,对于节点\(x\)不一定是从它的父亲处转移的,因为两个人可以同时移动,当分身最后一次传送后本体可以和分身一起移动然后本体在下一个路口等分身传送。
让本体等分身是一种可能更赚的方案如图(边长代指了中间省略的点数量级别)
显然当本体等待分身遍历完\(1\sim 5\)之后让本体和分身分别走向\(4\)和\(3\)是更优的做法。
可以相当于在链上选择一些关键点,本体只有在关键点时才等待,并且分身最后遍历最深的叶子时本体同时出发。而关键点不连续的条件只有本体等分身的时才会出现,也就是分身最后遍历的叶子深度比下一个关键点的深度要大。
而且显然地如果出现了这种情况那么选择另一个深度更小的祖先作为上一个关键点不可能更优(因为会走同一段)
所以我们只需要对于每个节点\(x\)找到第一个祖先\(y\)满足\(y\)的子树除去\(x\)的子树后存在一个深度比\(x\)大的节点记为\(sf_x\)。
那么每个节点只需从\(sf_x\)转移即可,如果找不到\(sf\)的节点显然对于这样的节点它的父节点只会有一个子节点,那么令\(f_x=f_{fa_x}+1\)即可。
问题是如何快速的求出每个\(sf_x\),注意到对于一个处理好的子树\(x\)只有可能有一段连续的链没有\(sf_x\)(如图红色箭头所指部分)
所以我们只需要从小往上处理维护每个处理好的子树的\(sf\)为\(0\)的链。
然后两棵子树的时候我们直接暴力合并这些部分即可,因为合并完之后肯定只会留下长的那条链多出来的部分。
时间复杂度:\(O(n)\)
code
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const ll N=5e6+10;
ll n,ans,deg[N],dep[N],fa[N],sf[N];
ll siz[N],sum[N],mx[N],nxt[N],f[N];
char s[N<<1];
void addl(ll x,ll y){
deg[x]++;fa[y]=x;
dep[y]=dep[x]+1;
return;
}
signed main()
{
scanf("%lld",&n);
if(n==1)return puts("0")&0;
scanf("%s",s+1);
for(ll i=1,now=0,cnt=0;i<=2*n;i++){
if(s[i]=='(')++cnt,addl(now,cnt),now=cnt;
else now=fa[now];
}
for(ll i=n;i>1;i--){
if(!deg[i])siz[i]=1,sum[i]=mx[i]=dep[i];
ll x,y;
for(x=i;x&&dep[x]<=mx[fa[i]];x=nxt[x])sf[x]=fa[i];
for(y=nxt[fa[i]];y&&dep[y]<=mx[i];y=nxt[y])sf[y]=fa[i];
nxt[fa[i]]=x+y;
sum[fa[i]]+=sum[i];
siz[fa[i]]+=siz[i];
mx[fa[i]]=max(mx[fa[i]],mx[i]);
}
f[1]=0;ans=n*n+100;
for(ll i=2;i<=n;i++){
f[i]=n*n+100;ll x=sf[i];
if(x)f[i]=f[x]+sum[x]-sum[i]-dep[x]*(siz[x]-siz[i]);
if(deg[fa[i]]==1)f[i]=min(f[i],f[fa[i]]+1);
if(!deg[i])ans=min(ans,f[i]);
}
printf("%lld\n",ans);
return 0;
}