2026牛客多校第九场 题解 DFI

赛时写了三道题,补了两道题。

赛中光速会了 D,但没写出来,呜呜呜呜呜

直接从第三道题开始写题解。

I

题意简述:

  • 每回合可以叠 \(x\) 点毒,或直伤 \(y\) 点
  • 毒会结算当前毒层数的伤害,随后毒数 -1
  • 问最小杀死怪的回合数

那么有一个很显然的观察:如果在 \(X\) 回合能击杀怪物,假设用了 \(a\) 次毒, \(b\) 次直伤,由于你发现直伤在哪里用都一样,不如直接拉到最后面使用,这样能最大化的叠毒。

对于 \(a\not=1\) 的情况,你发现叠差不多 \(\sqrt n\) 回合一定能杀死怪物。对于 \(a=1\) 的情况,我一定直接使用直伤,因此回合数是 \(\lceil \frac{n}{b}\rceil\)。

所以枚举叠毒的回合数 \(i\),后面的回合补刀。

你发现假设叠完毒后的 \(k\) 回合内,能对它造成的伤害是 \(\sum_{j=du}^{max(du-k+1,0)} j +kb\),那么可以二分它死的回合,其中 \(du\) 表示当前毒的层数,可以维护出来的。

D

首先你可以观察到一个性质:

考虑每个节点的深度 \(dep_x\),然后在此时刻出现了某个点,那么在 \(dep_x+t\) 相等的点是有可能相遇的,但不是一定能够相遇损耗的。例如以下情况,在 \(t=0\) 时刻生成了 A 点和B 点,在 \(t=2\) 时刻生成了 C 点,但是由于 \(t=1\) 时刻 \(A\) 和 \(B\) 损耗掉了,于是不会和 C 相撞,这样 最后 C 点得以保留。

image

进入本题正题,不妨按 \(key= dep_x +t\) 对点集进行分组,这样一来,其他集合里的点是不可能与这组点集中的点相撞,每组点是独立的。而每一组中,如果有点能存活,一定是恰好一个。

其次,你要考虑到一个性质:若两个点能够相遇,没有其他阻碍,那么一定在其 LCA 处相遇。如果只考虑暴力的做法呢,那我会考虑记录一个数组 \(f_u\),表示 \(u\) 及其子树内,是否存在这样的点 \(f_u\) ,使得 \(f_u\) 成功存活了下来。然后做转移,枚举 \(u\) 子树内所有的顶点 \(v\),记一下 \(f_v\not=0\) 的个数,若是唯一的,那么将 \(f_u\) 设置成 \(f_v\),否则将子树内所有的 \(f_v\) 都置空。

真的需要这么多点吗?

可以考虑对于每一个分组,都处理出一颗虚树,可以详见虚树的定义。

建出虚树后,至多有 \(2k\) 个顶点,可以在这棵树上做上述的 dp,而且由于每个顶点的父节点一定是它与某个其他节点的 LCA 节点,所以这里的转移可以直接枚举邻边转移。

时间复杂度瓶颈在于构建虚树,总题是 \(O(n\log n)\) 级别的。

#include<bits/stdc++.h>
#define fi first 
#define se second
#define pb push_back
using ll=long long;
using namespace std;
typedef pair<int,int>pii;
//直觉上 最优策略是不是一定是前缀的一段中毒 和 后缀的一段普攻
const int N=1e6+10;
int stk[N]={0};struct VirtualTree{
    int n,LOG,timer;
    vector<vector<pair<int,int>>>g,vt;
    vector<vector<int>>up;
    vector<int>dep,dfn,out,dis,last;
    VirtualTree(int n):n(n),timer(0),g(n+1),vt(n+1),dep(n+1),dfn(n+1),out(n+1),dis(n+1){
        LOG=1;
        while((1LL<<LOG)<=n)LOG++;
        up.assign(LOG,vector<int>(n+1));
    }
    //加入原树边
    void addEdge(int u,int v,int w=1){
        g[u].push_back({v,w});
        g[v].push_back({u,w});
    }
    //DFS预处理dfn、深度、距离和倍增祖先
    void dfs(int u,int fa){
        dfn[u]=++timer;
        up[0][u]=fa?fa:u;
        for(int j=1;j<LOG;j++)up[j][u]=up[j-1][up[j-1][u]];
        for(auto e:g[u]){
            int v=e.first,w=e.second;
            if(v==fa)continue;
            dep[v]=dep[u]+1;
            dis[v]=dis[u]+w;
            dfs(v,u);
        }
        out[u]=timer;
    }
    //预处理整棵原树
    void init(int root=1){
        timer=0;
        dep[root]=0;
        dis[root]=0;
        dfs(root,0);
    }
    //判断u是否为v的祖先
    bool isAncestor(int u,int v){
        return dfn[u]<=dfn[v]&&dfn[v]<=out[u];
    }
    //求两点LCA
    int lca(int u,int v){
        if(isAncestor(u,v))return u;
        if(isAncestor(v,u))return v;
        for(int j=LOG-1;j>=0;j--){
            if(!isAncestor(up[j][u],v))u=up[j][u];
        }
        return up[0][u];
    }
    //求原树两点距离
    int getDist(int u,int v){
        int p=lca(u,v);
        return dis[u]+dis[v]-2*dis[p];
    }
    //建立虚树并返回全部虚树节点
    vector<int>build(vector<int>key){
        for(int u:last)vt[u].clear();
        last.clear();
        if(key.empty())return{};
        sort(key.begin(),key.end(),[&](int x,int y){
            return dfn[x]<dfn[y];
        });
        key.erase(unique(key.begin(),key.end()),key.end());
        int m=key.size();
        for(int i=1;i<m;i++)key.push_back(lca(key[i-1],key[i]));
        sort(key.begin(),key.end(),[&](int x,int y){
            return dfn[x]<dfn[y];
        });
        key.erase(unique(key.begin(),key.end()),key.end());
        vector<int>st;
        for(int u:key){
            if(st.empty()){
                st.push_back(u);
                continue;
            }
            while(!isAncestor(st.back(),u))st.pop_back();
            int p=st.back();
            vt[p].push_back({u,getDist(p,u)});
            st.push_back(u);
        }
        last=key;
        return key;
    }

     void work(){
        int m;
        cin>>m;
        for(int i=1;i<n;i++){
            int u,v;
            cin>>u>>v;
            addEdge(u,v);
        }
        dfs(1,0);
        map<int,vector<pii>>mp;
        map<pii,int>cnt;
        map<pii,int>into;
        // for(int i=1;i<=n;i++)cout<<dep[i]<<' ';cout<<'\n';
        for(int i=1;i<=m;i++){
            int u,s;
            cin>>u>>s;
            cnt[{u,s}]++;
            into[{u,s}]=i;
        }
        set<pii>st;
        vector<int>sepc(m+1,0);
        for(auto [t,c]:cnt){
            auto [u,s]=t;
            if(c>1)st.insert({dep[u]+s,u});
            mp[dep[u]+s].push_back({u,into[{u,s}]});
        }
        vector<int>ans(m+1,0);
        vector<int>f(n+1,0);
        for(auto [x,v]:mp){
            // cout<<x<<":";
            // for(int i=0;i<v.size();i++)cout<<v[i].fi<<' ';cout<<'\n';
            sort(v.begin(),v.end(),[&](pii x,pii y){
                return dfn[x.fi]<dfn[y.fi];
            });
            vector<int>key;
            for(int i=0;i<v.size();i++)key.pb(v[i].fi);
            map<int,int>mp;
            for(int i=0;i<v.size();i++)mp[v[i].fi]=v[i].se;
            // for(int i=0;i<key.size();i++)cout<<key[i]<<' ';cout<<'\n';
            auto vedge=build(key);
            for(int i=0;i<vedge.size();i++)f[vedge[i]]=mp[vedge[i]];
            // 
            // for(int i=1;i<=n;i++){
            //    for(auto [j,w]:vt[i])cout<<i<<' '<<j<<'\n';
            // }
            // cout<<'\n';
             auto dfs2=[&](auto &&dfs2,int u,int fa)->void{
                int son=0+(f[u]>0);
                for(auto [v,w]:vt[u]){
                    // cout<<u<<' '<<v<<" "<<w<<'\n';
                    if(v==fa)continue;
                    dfs2(dfs2,v,u);
                    if(f[v]>0){
                        ++son;
                        f[u]=f[v];
                    }

                }
                if(st.count({x,u}))f[u]=0;
                if(son>1){
                    f[u]=0;//全GG
                }
                // if(son==0)f[u]=0;
                // if(son==0)f[u]=u;//自己
            };
            // for(int i=0;i<=n;i++)cout<<f[i];cout<<'\n';
            // cout<<vedge[0]<<'\n';
            dfs2(dfs2,vedge[0],-1);
            
            // cout<<f[0]<<'\n';
            if(f[vedge[0]]){
                assert(!ans[f[vedge[0]]]);
                ans[f[vedge[0]]]=1;
            }
            for(auto x:vedge ){
                f[x]=0;
            }
            // for(int i=0;i<=n;i++)cout<<f[i];cout<<'\n';

        }


        for(int i=1;i<=m;i++)cout<<ans[i];
    }  
};
void Silverwolf(){
    int n;
    cin>>n;
    VirtualTree tr(n);
    tr.work();
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    // int T;cin>>T;while(T--)
     Silverwolf();
    return 0;
}

F

题目简述:

  • 有一个 \(n\times n\) 的网格,初始时每一行恰好有一盏灯是亮的
  • 每一时刻,如果一盏灯旁边的四盏灯中,有至少两个亮着的,那么这盏灯会亮起
  • 求最终所有灯都亮起的最小时刻,若不能全部亮起则输出 -1

初始时,对于每个区间,我们记为 \([p_i,p_i]\),从前往后扫,若当前区间和上一个区间值域相邻,则立刻合并区间,并计算贡献。

我们记 \(a,b,c,d\) 为四个角被更新了答案的最小时间,\(mx\) 记录四个角中的最大答案。因此需要做一个七元组的合并问题。

有以下两种合并方式:

第一种方式如下图所示,红色节点第一次被更新的时间是 \(max(A.d,B.a)+1\),记第一个块目前的大小是 \(n_1\) ,第二个块目前的大小是 \(n_2\),如是你想,右上角和左下角是不是就被更新成了 \(max(A.d,B.a)+1+n_1+n_2 -1\) 。

image-20260902092711809

基本上是正确的,但是有可能扩散的时候,这两个块还没有被完成覆盖完成。网格中的每个点是每个点扩散的时间。如果按以上策略更新,会错误将右上角和左下角错误地被更新成 3,但由于在更新路径中遇到一个中途中还没有被更新的点,于是更新会暂停下来等待。那怎么办呢?办法很简单,这种情况,我们右上和左下角的点的答案至少是 \(A.b+n_2 -1\),中间停顿下来的过程在这一步也被算了。记录 \(X=max(A.b+n_2-1,max(A.d,B.a)+1+n_1+n_2 -1)\),那么这个新的块被更新成了 \((A.a,X,X,B.d)\)。

image

第二种合并块的情况如下:

image

记 \(X=max(max(A.a+n_1),max(A.b,B.c )+n_1+n_2)\)

那么这个块被更新成了 \((X,B.b,A.c,X)\)。

最终若只剩下恰好一个块,则找到了答案,答案为 \(max(a,b,c,d)\),如何快速维护上两个块的合并?使用栈。

时间复杂度是线性的。

导航