【UOJ 84】水题走四方(DP)

传送门

题意

给定一棵 \(n\) 个点的有根树。初始有 \(2\) 个人在根结点。每 \(1\) 秒内,每个人要么走向某个儿子,要么停在原地不动。当两人都在某个结点上时,其中一人可以立即传送到另一个人的位置上。最后要求每个结点都被至少经过一次,问总共花费的最小时间。

\(n \le 5\times10^6\)

分析

先说一个样例,可以卡掉若干个假算法:有两根长筷子粘在根上,且靠近根的一端有一些毛刺。答案应该是比一根筷子的长度多一点,而有些算法会给出接近两根筷子的长度。

考虑问题的子状态:两人位于同一点上。此时子树外的点必然已经被遍历过,我们只要考虑子树内花费的最小时间。为了简化问题,我们用逆向思维,考虑最多能省下多少时间。这样我们默认初始方案的用时为整棵树的叶子结点深度之和。

\(f_u\) 为子树答案,\(e_u\) 为子树中的叶子个数。我们枚举下一次集合的地点 \(v\),注意 \(v\) 不一定是 \(u\) 的儿子。那么最优策略一定是:留一个人呆在 \(u\),另一个人把 \(v\) 子树外的结点逛完后传到 \(v\)。注意最后逛到的叶子深度越大越优,因为最后一次逛不用传回 \(u\),而逛的时候之前干等的人就可以开始向 \(v\) 走了。设 \(d_v\)\(v\) 的深度,\(d_m(u, v)\)\(u\) 子树内、\(v\) 子树外的最深叶子的深度。那么可以写出转移式:

\[f_u = \max\{ f_v + (d_v - d_u)e_v - \max(0, d_v - d_m(u, v)) \} \]

直接做是 \(O(n^2)\) 的,突破口在于:大多数转移都是冗余的。先判掉 \(u\) 只有一个儿子 \(v\) 的特例,此时 \(f_{u} = f_v + e_v - 1\)。对其余情况,观察到 \(d_m(u, v) < d_v\) 时肯定不优,因为此时相当于乱逛的人到叶子后还要等另一个人走到 \(v\),那不如刚到叶子就传到 \(v\) 同深度的祖先。这样 \(\max(0, d_v - d_m(u, v))\) 这项就变成了 \(0\)

更强地,\(v\) 只需转移到满足 \(d_m(u, v) \ge d_v\) 的最深的 \(u\)。这个可以反证得到,假设 \(v\) 还会转移给更浅的祖先 \(w\),那么一定可以让 \(v\) 先转移到 \(u\),再从 \(u\) 通过特判的那种转移来转移到 \(w\)。因为 \(e_u - 1 \ge e_v\),所以这样转移不会使答案变劣。

那么我们只要找到每个 \(v\) 对应的 \(u\) 即可。接下来的做法就五花八门了,我写的是长链剖分。从下往上考虑一条长链,维护一个栈,每次把当前的 \(v\) 加入栈顶。当出现短儿子时,我们只要弹掉深度不超过某个数的 \(v\) 即可。最终栈里还剩下一些点,那么它们一定是转移给链顶的父亲。

因为要先算短儿子再算长儿子,我一开始直接写了递归,然后就 MLE 了。因此要用计数排序预处理出 dfn,再非递归地算,这样才能通过(口区)。

实现

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define per(i,a,b) for(int i=a;i>=b;i--)
using namespace std;
typedef long long ll;
const int N=5e6+5;
int n,p[N],d[N]={-1},c[N],mx[N],e[N],lc[N],sc[N],r,s[N],tp[N]={0,1},ct[N],id[N];
char t[N*2];
ll g,f[N];
void up(int u,int v){f[u]=max(f[u],f[v]+ll(d[v]-d[u])*(e[v]-(c[u]==1)));}
int main(){
	scanf("%*d%s",t);
	int v=0;for(int i=0;t[i];i++)t[i]=='('?p[++n]=v,d[n]=d[v]+1,v=n:c[v=p[v]]++;
	per(i,n,1)mx[i]=d[i],e[i]=!c[i],g+=e[i]*d[i];
	per(i,n,2){if(mx[p[i]]<mx[i])mx[p[i]]=mx[i],lc[p[i]]=i;e[p[i]]+=e[i];}
	rep(i,2,n)if(lc[p[i]]==i)tp[i]=tp[p[i]];else tp[i]=i,sc[p[i]]=max(sc[p[i]],mx[i]);
	rep(i,1,n)ct[tp[i]]++;rep(i,1,n)ct[i]+=ct[i-1];per(i,n,1)id[ct[tp[i]]--]=i;
	per(i,n,1){
		int u=id[i];
		if(lc[u]){if(!sc[u])up(u,lc[u]);else for(;r&&d[s[r-1]]<=sc[u];)up(u,s[--r]);}s[r++]=u;
		if(lc[p[u]]!=u)for(;r;)up(p[u],s[--r]);
	}
	printf("%lld",g-f[1]);
	return 0;
}
posted @ 2021-11-14 09:55  alfalfa_w  阅读(99)  评论(0编辑  收藏  举报