[UOJ#84]水题走四方

题目大意

一颗树,两个人初始从根节点出发,每一步每个人可以选择原地不动或者走向某个儿子。一步后,一个人可以瞬移到另一个人所在节点上。
最少需要多少步,使得每个节点都被遍历过?

做法

我们可以认为存在一个本体以及一个分身,每次都是分身瞬移到本体的位置。
如果本体A和分身B在节点C分道扬镳,本体A在D节点瞬移至分身B所在的节点E,干脆一开始便让本体A往E走,分身B往D走。因此瞬移的总是分身。
最后一定是停在一个叶子的位置。即本体走过的是一条主链。
本体肯定总是走走停停,不妨把停的位置标记为关键点,那么过程总是这样的:
本体在某个关键点停住,分身开始不断遍历该关键点至下一个关键点间所有延伸子树的叶子,剩最后一个叶子时本体也开始动身前往下一个关键点。
剩的这个叶子当然是深度最大的叶子。
可以设dp f[x]表示从根走到u,沿途延伸子树都被遍历,x是一个关键点,最少步数是多少。
不妨记ds[x]表示x子树内叶子深度和,nl[x]表示x子树内叶子个数,d[x]表示x的深度。
记dm[u,v]表示u是v的祖先,u到v(不包含v)的延伸子树叶子最大深度。
则转移就是枚举上一个关键点。
考虑优化。
假设上一个关键点为u,现在这个关键点为v。
如果分身最后走的那个叶子深度并没有达到v的深度,显然分身到达叶子之后就可以立马瞬移然后和本体共同行走到v。
由于走的是最深的叶子,瞬移回来后的路程都不可能有延伸子树。
那么我们为何不设瞬移回来的那个点为关键点呢?
这样我们就只需要考虑dm[u,v]>=d[v]的u作为关键点。而且要求dm[u,v]要出现在u的延伸子树里(即若w是u的下一个点,有dm[u,v]>dm[w,v])。
然后需要新增一种转移,如果父亲只有一个儿子,可以直接从父亲的f+1转移过来。
然后我们不难考虑到,如果v存在两个可以作为关键点的祖先u1,u2, d[u1]<d[u2] ,上一个关键点就是u1,而不将u2也设置为关键点,一定是不优的,因此我们只需要考虑up[v]表示最深的u满足dm[u,v]>=d[v]。
现在只要能处理up,本题dp可以线性解决。
比较容易直接想到的思路是树上可恢复形单调栈,可以配合长链剖分优化至线性。
我们可以想到一个很简单的方法,每个子树保存一个链表,按照d排序保存不能在子树内找到up的点。
接下来考虑合并一个子树,关键在于合并两个链表。
这里可以发现,两个链表一定会有一个变为空,即只有当前的长儿子链表会不为空,其余的儿子由于不是最长,其链表所有元素都会因为长儿子被T掉。
这样就能做到线性,速度快常数小易理解,代码也非常好写。

#include<cstdio>
#include<algorithm>
#include<cstring>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
using namespace std;
typedef long long ll;
const int maxn=5000000+10;
const ll inf=1000000000000000000;
char s[maxn*2];
int p[maxn],nl[maxn],dm[maxn],d[maxn],son[maxn],nxt[maxn],up[maxn];
ll f[maxn],ds[maxn];
int i,j,k,l,t,n,m,tot,now;
ll ans;
int main(){
    scanf("%d",&n);
    scanf("%s",s+1);
    now=0;
    fo(i,1,n*2)
        if (s[i]=='('){
            p[++tot]=now;
            now=tot;
        }
        else now=p[now];
    fo(i,2,n){
        d[i]=d[p[i]]+1;
        son[p[i]]++;
    }
    fd(i,n,2){
        if (!son[i]){
            ds[i]=dm[i]=d[i];
            nl[i]=1;
        }
        now=i;
        while (now&&d[now]<=dm[p[i]]){
            up[now]=p[i];
            now=nxt[now];
        }
        while (nxt[p[i]]&&d[nxt[p[i]]]<=dm[i]){
            up[nxt[p[i]]]=p[i];
            nxt[p[i]]=nxt[nxt[p[i]]];
        }
        if (now) nxt[p[i]]=now;
        nl[p[i]]+=nl[i];
        ds[p[i]]+=ds[i];
        dm[p[i]]=max(dm[p[i]],dm[i]);
    }
    fo(i,2,n){
        f[i]=inf;
        if (up[i]) f[i]=f[up[i]]+ds[up[i]]-ds[i]-(ll)(nl[up[i]]-nl[i])*d[up[i]];
        if (son[p[i]]==1) f[i]=min(f[i],f[p[i]]+1);
    }
    ans=inf;
    fo(i,1,n)
        if (!son[i]) ans=min(ans,f[i]);
    printf("%lld\n",ans);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值