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 点得以保留。

进入本题正题,不妨按 \(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\) 。
基本上是正确的,但是有可能扩散的时候,这两个块还没有被完成覆盖完成。网格中的每个点是每个点扩散的时间。如果按以上策略更新,会错误将右上角和左下角错误地被更新成 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)\)。

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

记 \(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)\),如何快速维护上两个块的合并?使用栈。
时间复杂度是线性的。
浙公网安备 33010602011771号