P9907 [COCI 2023/2024 #1] Mostovi 题解
看到删边的问题,我们就很自然的想到 tarjan 和 dfs 树,当然这题是用 dfs 树来解决的。
我们建完树之后分为树边和非树边,这里可以分别讨论:
若为树边(\(dep_u<dep_v\)):
\(u\) 为根:
- \(u\) 有三个及以上的儿子,\(+1\)。
- 有两个儿子,若 \(v\) 不是孤点,\(+1\)。
- 若只有一个儿子,如果 \(v\) 有其他儿子,\(+1\)。
\(u\) 不为根:
- 需要判断 \(u\) 的儿子中除了 \(v\) 所有的子节点的子树中至少有一个需要与 \(u\) 的祖先连边。
- 还需要判断 \(v\) 的所有子节点的子树中至少有一个需要与 \(u\) 的祖先连边。
- 若上面的条件都符合,则不用 \(+1\)。
若此为非树边( \(dep_v<dep_u\)):
\(v\) 为根 :
- 一个儿子,往下判断 \(u\) 的儿子是否存在在 \(v\) 和 \(u\) 这条链上的点即可。
- 两个儿子或以上,\(+1\)。
\(v\) 不为根:
-
若 \(v\) 的其他有一个子树与 \(v\) 的祖先不连,\(+1\)。
-
\(u\) 的一个子树没有返祖边,\(+1\)。
-
上和中两种点的连通性,使用 dfs 序加线段树维护区间最值:
-
若上中连通,则全部连通,不用 \(+1\)。
-
否则若它为叶子或没有一个为与中和上都连的一个子树,\(+1\)。
-
详细的实现可以看代码,本题细节很多,代码用了启发式合并和线段树的做法:
#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N=3E5+5,inf=1e9;
int n,m;
vector<int>e[N],e1[N],e2[N];//树边和非树边
int cnt[N],dep[N],vis[N],ans,st1[N][20],cn,mi[N],L[N],R[N],id[N],Mi[N];
int MI[N];
int f[N],g[N];
//用来记录每一个点的子节点的子树中能与这个节点的祖先连上非树边的个数
int dp[N];
//dp来记录这个点的子树中最大的小于它的父亲的深度
int find(int x,int y) {
for(int i=19; i>=0; --i)
if(dep[st1[x][i]] > dep[y])x=st1[x][i];
return x;
}//查找x的祖先是y的儿子
set<int>st[N],tmp;
bool cmp(int x,int y){
return st[x].size() >st[y].size() ;
}
void dfs1(int u,int fa) {
dep[u]=dep[fa]+1,id[L[u]=++cn]=u,vis[u]=1,st1[u][0]=fa;
for(int i=1; i<20; i++)st1[u][i]=st1[st1[u][i-1]][i-1];
for(int v:e[u]) {
if(v==fa)continue;
if(vis[v]) {
if(dep[v] < dep[u]) {
e2[u].push_back(v);
st[u].insert(dep[v]);
mi[u]=min(mi[u],dep[v]);
}
}//返祖边
else {
dfs1(v,u);
e1[u].push_back(v);
cnt[u]++;
}//树边
}
for(int v:e1[u]) {
if(st[v].size() && *st[v].begin() <dep[u] )dp[v] =*--st[v].lower_bound(dep[u]);
//else dp[v]=inf;
if(*st[v].begin() < dep[u] && st[v].size() )
f[u]++;
if(*st[v].begin() < dep[u]-1 && st[v].size() ) g[u]++;
Mi[v]=*st[v].begin();
}
sort(e1[u].begin() ,e1[u].end() ,cmp);
for(int v:e1[u]) {
if(st[v].size() >st[u].size() ) swap(st[u],st[v]);
for(int o:st[v]) st[u].insert(o);
st[v].clear() ;
}
R[u]=cn;
}
struct SEG {
#define ls p<<1
#define rs p<<1|1
int c[N<<2];
void pushup(int p) {
c[p]=min(c[ls],c[rs]);
}
void build(int p,int l,int r) {
if(l==r) return void (c[p]=mi[id[l]]);
int mid=l+r>>1;
build(ls,l,mid),build(rs,mid+1,r);
pushup(p);
}
int query(int p,int l,int r,int L,int R) {
if(R<L)return inf;
if(L<=l&&r<=R)return c[p];
int mid=l+r>>1;
if(L<=mid && R>mid)return min(query(ls,l,mid,L,R),query(rs,mid+1,r,L,R));
if(L<=mid)return query(ls,l,mid,L,R);
return query(rs,mid+1,r,L,R);
}
} seg;
void dfs2(int u,int fa) {
for(int v:e1[u]) {
if(u==1) {
if(cnt[u]>=3)ans++;
else if(cnt[u]==2 &&cnt[v])ans++;
else if(cnt[u]==1 &&cnt[v]>1)ans++;
} else {
//首先判断u中其他的子树是否满足条件
int tmp=f[u];
if(Mi[v]<dep[u] &&Mi[v] )tmp--;
if(tmp!=cnt[u]-1) {
ans++;
continue;
}
if(!cnt[v])continue;
//再判断v的子树中是否存在不可以连到u的祖先的子树
if(g[v]!=cnt[v])ans++;
}
}//树边
for(int v:e2[u]) {
// u是下面的点,v 是上面的点
if(v==1) {
//这时没有上面的点
if(cnt[v]==1) {
for(int t:e1[u]) {
if(dp[t] <= dep[v]) {
ans++;
break;
}
//判断u的子树的点是否存在非树边在中间
}
}//有且仅有一个儿子
else ans++;
} else {
int op=0;
int tmp=f[v];
int F=find(u,v);
if(Mi[F]<dep[v] &&Mi[F])tmp--;
if(tmp!=cnt[v]-1) {
ans++;
continue;
}//在v有其他的子树,而这些子树不可以与fa_v及其祖先相连,跳。
for(int t:e1[u]) {
if(Mi[t]<dep[v] &&Mi[t])continue;
if(dp[t]>dep[v] &&dp[t])continue;
ans++,op=1;
break;
}//判断u的子树中是否拥有不能连到中间或者上面的
if(op)continue;
if(min(seg.query(1,1,n,L[F],L[u]-1),seg.query(1,1,n,R[u]+1,R[F])) >=dep[v]) {
int fl=0;
for(int t:e1[u]) {
if( Mi[t]<dep[v] && dp[t]>dep[v]) {
fl=1;
break;
}
}
if(!fl)ans++;
}//若中上不连通则需要根据下面的子树是否与上中连通来判断,否则直接连通。
}
}
//非树边
for(int v:e1[u])
dfs2(v,u);
return ;
}
signed main() {
scanf("%d%d",&n,&m);
for(int i=1,u,v; i<=m; i++) {
scanf("%d%d",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
memset(mi,0x3f,sizeof mi);
dfs1(1,0);
seg.build(1,1,n);
dfs2(1,0);
cout<<ans<<endl;
return 0;
}

浙公网安备 33010602011771号