QOJ.ac

QOJ

Type: Editorial

Status: Open

Posted by: nullptr_qwq

Posted at: 2026-08-22 13:30:49

Last updated: 2026-08-22 13:37:26

Back to Problem

线性做法

考虑直接在原树上从下往上分析,刻画 $F$ 构成子段的性质。可以认为每个子树上传一个 $[1,size_u]$ 的排列表示内部的顺序,通过分析 $G_{n-1}$ 发现每个子树的 $p$ 一定都是一段区间。

对 $u$ 合法的必要条件就是按照给出的顺序合并 $u$ 的出边,每次的 $F(u)$ 构成区间。最终放在 $p_u$ 左边的子树只需要对上传的序列进行 reverse 然后拼接起来。对于 $u\ne rt$ 考虑,插入边 $(u,fa_u)$ 时此刻 $u$ 内部的顺序构成 $[1,size_u]$ 的一段前缀,否则 $F(fa_u)$ 此刻无法构成区间。

最终 $u$ 出边的结构一定是:$x_1,x_2,\cdots,x_l,u,y_1,y_2,\cdots,y_r$,其中 $x,y$ 构成子节点的一组划分,各自递归到子问题中。然后每一侧一定是放完靠近 $u$ 的子树后才能接着开始放下一个子树。设 $a_u$ 表示 $(u,fa_u)$ 的加入时刻,$b_u=\max_{v\in subtree(u)}a_v$,对 $x$ 有 $a_{x_i}>b_{x_i+1}$,$y$ 有 $b_{y_i}\leq a_{y_{i+1}}$。然后考虑 $a_u$ 即 $(u,fa_u)$ 的限制,如果在 $x$ 一侧就一定需要 $x$ 都填写完毕,此时有 $a_u>b_{x_1}$。

此时这个结构比较好,考虑直接按照子节点的 $a_v$ 从大到小排序,从两边向中间填写,记录两侧各自对 $b$ 的限制,每次往较小的填上当前子树一定不劣,如果填不进去就无解。而 $a_u$ 会作为其中一类的初始限制。

由于输入已经排序好,时间复杂度 $O(n)$。

感觉说的不太是人话,建议看代码

void solve(){
    cin>>n;
    F(_,1,n-1){ int u,v;cin>>u>>v,g[u].push_back(v),a[v]=_; }
    F(i,1,n)if(!a[i])rt=i;
    auto dfs0=[&](auto&self,int u)->void{
        b[u]=a[u];
        for(int&v:g[u])self(self,v),chkmax(b[u],b[v]);
    }; dfs0(dfs0,rt);
    a[rt]=n;
    bool flag=0;
    auto dfs=[&](auto&self,int u,int op)->void{
        if(flag)return;
        reverse(all(g[u]));
        array<int,2>f={inf,inf};
        vector<int>vec[2];
        F(o,0,1)vec[o].clear();
        f[op^1]=a[u];
        for(int&v:g[u]){
            int c;
            if(b[v]<min(f[0],f[1]))c=f[0]<f[1]?0:1;
            else if(b[v]<f[0])c=0;
            else if(b[v]<f[1])c=1;
            else return flag=1,void();
            vec[c].push_back(v),f[c]=a[v];
        }
        for(int&v:vec[0])self(self,v,0);
        ans[u]=++tim;
        reverse(all(vec[1]));
        for(int&v:vec[1])self(self,v,1);
    }; dfs(dfs,rt,0);
    if(flag)return cout<<"No\n",void();
    cout<<"Yes\n";
    F(i,1,n)cout<<ans[i]<<' ';
    cout<<'\n';
}

Comments

No comments yet.