双连通分量学习笔记+杂题
图论系列:
前言:
もしも明日がくるのなら
あなたと花を育てたい
もしも明日がくるのなら
あなたと愛を語りたい
走って 笑って 転んで
相关题单:https://www.luogu.com.cn/training/641352
一.割点与桥
双连通分量是针对无向图来说的,有向图是强连通分量。在了解双连通分量之前需要先了解无向图中的割点与桥。
1.割点:
(1)定义:
割点:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。
如下图中的点 2 就是一个割点,因为删去点 2 之后原图就变为 2 个连通分量了。
(2)求法:
一般使用 Tarjan 算法求解(因为一个一个判断时间复杂度太高了,只能利用一些性质)还是考虑同强连通一样,在无向图中跑一个 dfs 生成树,维护两个数组。
-
在
的子树内。 -
经过一条不是树边的边,能够从
的子树内的点到达的点。
统计出 dfn,low 数组之后,由于在一个 dfs 树内,
但是对于 dfs 生成树的起点不适用,对于 dfs 生成树的起点只需要判断其有几个不互相连通的儿子即可,只要有两个互相不连通的儿子,dfs 生成树的起点就是割点。
模板题代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e5+5;
int n,m,num,root;
int dfn[M],low[M];
bool vis[M];
int cnt=0;
struct N{
int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
inline void dfs(int u,int f)
{
dfn[u]=low[u]=++num;//初始化dfn,low数组
int siz=0;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
if(!dfn[v])
{
dfs(v,u),++siz;//每一次遍历到这,就说明不连通的儿子数+1,因为如果当前这个儿子与另外一个已经遍历过的儿子连通了,那么那个儿子会优先将这个点遍历,而不会留给根节点来遍历
low[u]=min(low[u],low[v]);//儿子们能到的点,当前点也可以到达
if(low[v]>=dfn[u]&&u!=root) vis[u]=1;//如果满足条件并且不是 dfs 生成树的根节点
}
else low[u]=min(low[u],dfn[v]);//经过一条不是树边的边,能够到达的 dfn 值最小的点
}
if(u==root&&siz>1) vis[u]=1;//特判根节点
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1,a,b;i<=m;++i) cin>>a>>b,add(a,b),add(b,a);//无向图
for(int i=1;i<=n;++i)
{
if(!dfn[i]) root=i,dfs(i,0);//没保证是个无向连通图
}
int ans=0;
for(int i=1;i<=n;++i) ans+=vis[i];
cout<<ans<<"\n";
for(int i=1;i<=n;++i) if(vis[i]) cout<<i<<" ";
return 0;
}
2.割边:
(1)定义:
割边:和割点差不多,叫做桥。对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。
如下图中的红色的边就是割边,因为删去
(2)求法:
和割点差不多,只要改一处:判断条件是
修改判断的理由是可能出现重边。以上图为例,如果
那么我们对于每一条边赋一个边权
代码:
inline void tarjan(int u,int pre)
{
dfn[u]=low[u]=++num;
s.push(u);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(p[i].val==pre) continue;
if(!dfn[v])
{
tarjan(v,p[i].val);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]);//代表这条 u -> v 的边就是割边了,可能会进行一些操作,后面讲。
}
else low[u]=min(low[u],dfn[v]);
}
}
//其余的同割点,不用特判根节点
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1,a,b;i<=m;i++)
{
cin>>a>>b;
add(a,b,i),add(b,a,i);//给每条边赋一个边权
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
return 0;
}
二.双连通分量
1.相关定义:
边双连通:在一张连通的无向图中,对于两个点
点双连通:在一张连通的无向图中,对于两个点
边双连通分量:对于一个无向图中的极大边双连通的子图,我们称这个子图为一个 边双连通分量。
点双连通分量:对于一个无向图中的极大点双连通的子图,我们称这个子图为一个 点双连通分量。
2.性质:
(1)边双连通:
一个边双连通分量内没有割边。(根据定义)
由于边双连通分量是由一个个割边分隔开来,所以每一个点只属于一个边双连通分量。
因为每个点只属于一个边双连通分量,所以边双连通具有传递性,若
将每个边双连通分量缩成一个点,保留不同边双连通分量之间的边,最后会形成一颗树/森林。(因为对于无向图,去掉重边&自环之后,如果边数大于点数-1,那么必定会形成一个环,而在环上的所有点边双连通,形成一个大的边双连通分量,所以最后一定是一颗树/森林)。
图内两个之间的割边,就是两个点所在的边双连通分量代表的点,在缩完点之后形成的那颗树,两点相连经过的边。
(2)点双连通
两个点双最多只有一个公共点,且一定是割点。(根据定义,如果两个点双之间有两个公共点,那么分属两个点双的两个点就至少有一条路径分别经过这两个公共点相连,会形成一个大点双)。
若两点双有交,那么交点一定是割点。(由上面的那个性质推出)
由于点双之间具有公共点,所以点双不具有传递性。
一个点是割点当且仅当它属于超过一个点双,由一条边直接相连的两点点双连通。(根据后半句可以推出一条边恰属于一个点双,刚好同割边相反)。
对于一个点双,它在 DFS 搜索树中 dfn 值最小的点一定是割点或者树根。对于这个性质,分类讨论:
-
当这个点为割点时,它一定是点双连通分量的根,因为一旦包含它的父节点,他仍然是割点。
-
当这个点为树根时:
-
有两个及以上子树,它是一个割点。
-
只有一个子树,它是一个点双连通分量的根。
-
它没有子树,视作一个点双。
-
需要注意:两点之间任意一条路径上的所有割点,不一定就是两点之间的所有必经点。
3.求法:
(1)边双连通分量
由于已经知道如何求割边了,那么割边分割出来的一块块连通块就是一个个边双连通分量。类似求强连通分量,在 dfs 的时候用一个栈存下当前遍历过的点,如果判断出
模板题 代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e6+5;
int n,m,num;
int low[M],dfn[M];
vector<vector<int>> ans;
stack<int> s;
int cnt=0;
struct N{
int to,next,val;
}; N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b,p[cnt].val=c;
}
inline void push(int pos)//表示直到弹到pos才结束
{
vector<int> res;int x;
while(x!=pos)
{
res.push_back((x=s.top()));
s.pop();
}
ans.push_back(res);
}
inline void tarjan(int u,int pre)
{
dfn[u]=low[u]=++num;
s.push(u);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(p[i].val==pre) continue;
if(!dfn[v])
{
tarjan(v,p[i].val);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) push(v);//是割边
}
else low[u]=min(low[u],dfn[v]);
}
}//实质上是求割边的同时将原图分成了一个个连通块
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1,a,b;i<=m;i++)
{
cin>>a>>b;
add(a,b,i),add(b,a,i);
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0),push(i);//将剩下的点弹出
cout<<ans.size()<<"\n";
for(auto it:ans)
{
cout<<it.size()<<" ";
for(auto x:it) cout<<x<<" ";
cout<<"\n";
}
return 0;
}
(2)点双连通分量
类似边双连通分量的求法,但是略有不同。如果一个点
这时候不需要向求割点那样特判根节点,但是要特判单独的一个点。
模板题代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5,N=2e6+5;
int n,m,root,num;
int dfn[M],low[M];
stack<int> s;
vector<vector<int>> ans;
vector<int> res;
int cnt=0;
struct edge{
int to,next;
};edge p[N<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
inline void push(int u,int f)//弹到 u,但是 f 点也要加进去
{
res.clear();int x;
while(1)
{
res.push_back((x=s.top()));
s.pop();
if(x==u) break;
}
res.push_back(f);
ans.push_back(res);
}
inline void dfs(int u,int f)
{
dfn[u]=low[u]=++num;
s.push(u);
int siz=0;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
if(!dfn[v])
{
++siz;
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]) push(v,u);//通过 u -> v 判断出 u 是个割点
}
else low[u]=min(low[u],dfn[v]);
}
if(!f&&!siz)//特判单点
{
res.clear();
res.push_back(u),ans.push_back(res);
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1,a,b;i<=m;++i) cin>>a>>b,add(a,b),add(b,a);
for(int i=1;i<=n;++i)
{
if(!dfn[i])
{
while(!s.empty()) s.pop();
root=i,dfs(i,0);//root其实没啥用
}
}
cout<<ans.size()<<"\n";
for(auto res:ans)
{
cout<<res.size()<<" ";
for(int it:res) cout<<it<<" ";
cout<<"\n";
}
return 0;
}
4.习题:
CF1986F Non-academic Problem
给定一张无向图,可以删去任意一条边,询问最后图内可达性点对最少能为多少。
判断一道题需要使用点双还是边双求解,就观察题目询问的/进行操作的和什么相关。这道题明显就是和边相关,考虑使用边双。
对于一张无向图,如果删去一条非割边,并不会影响任一点的可达性,原图可达性点对不变,自然不优。由于要求最小,所以一定删去一条割边较优,并且这条割边减少的可达性点对最多。对于一条割边,删去这条割边,那么其连接的两边就会形成两个连通块
对于原图缩点,记录一下每个边双包含的点数,保留不同边双的边,形成一个森林。此时对于每一棵树,统计一下其包含的点数,记作
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#define int long long
#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e5+5;
int T,n,m,num,tot,totsiz,ans,res;
int dfn[M],low[M],col[M],siz[M];
bool vis[M];
stack<int> s;
vector<int> e[M];
int cnt=0;
struct N{
int to,next,val;
}; N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b,p[cnt].val=c;
}
inline void push(int u)
{
++tot;int x;
while(1)
{
col[(x=s.top())]=tot,++siz[tot];//给每个点染色&记录边双大小
s.pop();
if(x==u) break;
}
}
inline void dfs(int u,int pre)
{
dfn[u]=low[u]=++num;
s.push(u);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(p[i].val==pre) continue;
if(!dfn[v])
{
dfs(v,p[i].val);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) push(v);
}
else low[u]=min(low[u],dfn[v]);
}
}
inline void dfs2(int u,int f)
{
vis[u]=1;
for(int v:e[u])
{
if(v==f) continue;
dfs2(v,u);
siz[u]+=siz[v];
}
}
inline void dfs3(int u,int f)
{
for(int v:e[u])
{
if(v==f) continue;
ans=max(ans,siz[v]*(totsiz-siz[v]));//当前这条边做的贡献
dfs3(v,u);
}
}
inline void clear()
{
cnt=num=tot=ans=res=0;
for(int i=1;i<=n;i++)
{
head[i]=dfn[i]=low[i]=col[i]=siz[i]=vis[i]=0;
e[i].clear();
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>T;
while(T--)
{
cin>>n>>m,clear();
for(int i=1,a,b;i<=m;i++) cin>>a>>b,add(a,b,i),add(b,a,i);
for(int i=1;i<=n;i++)
{
if(!dfn[i]) dfs(i,0),push(i);//点双缩点
}
for(int u=1;u<=n;u++)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(col[u]==col[v]||v<=u) continue;
e[col[u]].push_back(col[v]),e[col[v]].push_back(col[u]);//对于不同的边双保留边(有时候重边可能会影响答案,需要用个map判下重边)
}
}
for(int i=1;i<=tot;i++)
{
if(!vis[i])
{
dfs2(i,0);//由于原图可能不连通,每一颗树单独处理出totsiz
totsiz=siz[i],res+=totsiz*(totsiz-1)/2;//对于当前这棵树的任意两个点肯定都是连通的
dfs3(i,0);
}
}
cout<<res-ans<<"\n";
}
return 0;
}
CF1000E We Need More Bosses
对于一张无向连通图,找到两个点
还是与边相关,考虑边双。必须经过的边不就是割边,考虑缩点后建出边双树(姑且这么叫吧),那么这颗树上的边就是必须经过的边。要找树上一条最长的路径,不就是树的直径(挺简单的,不会去搜搜看,网上挺多讲解的)。于是答案就是边双缩点之后形成的树的直径。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=3e5+5;
int n,m,num,tot,root,maxx;
int dfn[M],low[M],col[M];
stack<int> s;
vector<int> e[M];
int cnt=0;
struct N{
int to,next,val;
}; N p[M<<1];
int head[M],deep[M];
inline void add(int a,int b,int c)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b,p[cnt].val=c;
}
inline void push(int pos)
{
int x;++tot;
while(1)
{
col[(x=s.top())]=tot;
s.pop();
if(x==pos) break;
}
}
inline void dfs(int u,int pre)
{
dfn[u]=low[u]=++num;
s.push(u);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(p[i].val==pre) continue;
if(!dfn[v])
{
dfs(v,p[i].val);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) push(v);
}
else low[u]=min(low[u],dfn[v]);
}
}
inline void dfs2(int u,int f,int d)
{
deep[u]=d;
for(auto v:e[u])
{
if(v==f) continue;
dfs2(v,u,d+1);
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1,a,b;i<=m;i++) cin>>a>>b,add(a,b,i),add(b,a,i);
dfs(1,0),push(1);//保证了是连通图
for(int u=1;u<=n;u++)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(col[u]==col[v]||v>=u) continue;
e[col[u]].push_back(col[v]),e[col[v]].push_back(col[u]);
}
}
//以下都是在找直径
dfs2(1,0,0);
root=1,maxx=0;
for(int i=2;i<=n;i++)
{
if(deep[i]>maxx) maxx=deep[i],root=i;
}
dfs2(root,0,0),maxx=0;
for(int i=1;i<=n;i++) maxx=max(maxx,deep[i]);
cout<<maxx<<"\n";
return 0;
}
P2860 [USACO06JAN] Redundant Paths G
神秘推性质题,简化题意:给定一张无向图,问最少添加多少条边可以使得原图变成一个边双连通图。
首先都问你边双了,那自然是考虑边双求解。缩点建树后,思考对于树上不直接连通的两个点
问题问的就是使得树化为一个点需要的最少操作。考虑下面的这颗树,怎么操作最优?肯定是从叶子出发到达另外一个叶子时最优,这个时候能够消除最多的点化作一个点(
最后答案就是(叶子数量+1)/2,因为向上取整。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e4+5;
int n,m,num,tot,ans;
int dfn[M],low[M],col[M],in[M];
stack<int> s;
int cnt=0;
struct N{
int to,next,val;
}; N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b,p[cnt].val=c;
}
inline void push(int pos)
{
int x;++tot;
while(1)
{
col[(x=s.top())]=tot;
s.pop();
if(x==pos) break;
}
}
inline void dfs(int u,int pre)
{
dfn[u]=low[u]=++num,s.push(u);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(p[i].val==pre) continue;
if(!dfn[v])
{
dfs(v,p[i].val);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) push(v);
}
else low[u]=min(low[u],dfn[v]);
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1,a,b;i<=m;++i)
{
cin>>a>>b;
add(a,b,i),add(b,a,i);
}
dfs(1,0),push(1);//保证连通,只用做一遍边双缩点
for(int u=1;u<=n;++u)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v<u&&col[v]!=col[u]) ++in[col[u]],++in[col[v]];
}
}
for(int i=1;i<=tot;++i) ans+=(in[i]==1);//叶子就是度数为1的点嘛
cout<<(ans+1)/2<<"\n";
return 0;
}
P3469 [POI2008] BLO-Blockade
和 CF1986F Non-academic Problem 很像啊,只不过那道题删边,而这道题是删点。与点相关,考虑使用点双&割点的性质求解。
对于每个点
对于非割点,删去之后不会影响其他点的连通性,只是其他点到达不了自己,除开自己,其他点共有
对于割点,删去之后连通块数量增加(注意不一定只增加 1,如果这个割点在多个连通块的中间的话,类似菊花图,可能会多出相当多的连通块)。考虑一个割点
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5;
int n,m,num;
int dfn[M],low[M],siz[M],ans[M];
bool vis[M];
int cnt=0;
struct N{
int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
inline void dfs(int u,int f)
{
dfn[u]=low[u]=++num,siz[u]=1;
int son=0,sum=0;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
if(!dfn[v])
{
++son,dfs(v,u);
low[u]=min(low[u],low[v]),siz[u]+=siz[v];
if(dfn[u]<=low[v])
{
ans[u]+=siz[v]*(n-siz[v]),sum+=siz[v];
if(u!=1) vis[u]=1;
//如果当前点删去之后 v 子树就成一个连通块了,就要加上贡献,同时统计 sum
}
}
else low[u]=min(low[u],dfn[v]);
}
if(son>1&&u==1) vis[u]=1;
if(!vis[u]) ans[u]=2*(n-1);//不是割点答案就是 2*(n-1)
else ans[u]+=(n-sum-1)*(sum+1)+(n-1);//是的话还要加上剩下的那一个连通块与 u 自身的贡献
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
//freopen("P3469_1.in","r",stdin);
cin>>n>>m;
for(int i=1,a,b;i<=m;++i)
{
cin>>a>>b;
add(a,b),add(b,a);
}
dfs(1,0);
for(int i=1;i<=n;i++) cout<<ans[i]<<"\n";
return 0;
}
P5058 [ZJOI2004] 嗅探器
考察了 dfn 数组的一些性质。对于一张无向图,给出两个点
乍一看去似乎不是很可做,因为点双和边双性质上有不同,两点之间任意一条路径上的割点,不一定是两点之间的必经点(如下图中
那么为了判断某个点是两点之间的必经点,现在唯一已知的是这个点肯定是个割点。那么我们就枚举每一个割点,看删去这个割点之后两点是否连通,不连通那肯定就是必经点了。为了方便判断两点是个否连通,可以利用一下 dfn 数组的性质。
首先以两点中的一点为根 ,这里以
这样做的依据?由于在 dfs 生成树中,子树内的点的 dfn 值一定比根的 dfn 值大。对于由边
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5,inf=2e9;
int n,a,b,num,ans=inf;
int dfn[M],low[M];
int cnt=0;
struct N{
int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
inline void dfs(int u,int f)
{
dfn[u]=low[u]=++num;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
if(!dfn[v])
{
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
//u是一个割点
if(u!=a&&dfn[b]>=dfn[v]) ans=min(ans,u);//b在v子树内,u删去之后a,b必不连通,则u是必经点
}
}
else low[u]=min(low[u],dfn[v]);
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
while(1)
{
cin>>a>>b;
if(!a&&!b) break;
add(a,b),add(b,a);
}
cin>>a>>b;
dfs(a,0);
if(ans==inf) cout<<"No solution\n";
else cout<<ans<<"\n";
return 0;
}
P3225 [HNOI2012] 矿场搭建
非常妙的一道题。对于一张无向图,可以指定一些点为逃生点,删去任意一个点,都可以保证剩下的点可以与至少一个逃生点相连通,询问至少需要指定多少个逃生点,以及指定最少逃生点的方案数。
由于是和点相关,考虑点双&割点相关的性质。由于需要删去一个点,那么我们先考虑朴素情况,如果原图是一张点双连通图,那么删哪个点都没有影响,那么我们只需要随意指定两个点作为逃生点(只指定一个的话,可能删的就刚好是逃生点,然后剩下的点就没法和逃生点相连了),方案数显然为
那么对于原图中存在割点的时候,怎么分配逃生点才能使得分配的数量最少?考虑原图是如下一张图,存在 4 个点双
实际上,对于这些只存在一个割点的点双实际上就类似于树上的叶子,将这些叶子做上标记,那么随便断一个点,每个点还是能保证到达一个叶子节点(对于链这种极端情况,链的两端都会被看作叶子节点)。
于是答案是这些被看作叶子节点的节点数,也就是只含有一个割点的点双,方案数也比较简单,由于每一个叶子点双子需要任选一个不是割点的点作为逃生点,所以答案就是
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e3+5;
int n,m,a,b,num,tot,root,ans,sum;
int dfn[M],low[M];
bool vis[M];
stack<int> s;
vector<int> t[M];
int cnt=0;
struct N{
int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt,n=max(n,max(a,b));
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
inline void push(int u,int f)
{
++tot;int x;
while(1)
{
t[tot].push_back((x=s.top()));//统计每个点双内的点
s.pop();
if(x==u) break;
}
t[tot].push_back(f);
}
inline void dfs(int u,int f)
{
dfn[u]=low[u]=++num;
s.push(u);
int siz=0;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
if(!dfn[v])
{
++siz;
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
push(v,u);
if(u!=root) vis[u]=1;
}
}
else low[u]=min(low[u],dfn[v]);
}
if(u==root&&siz>1) vis[u]=1;
}
inline void clear()
{
cnt=n=num=tot=ans=0,sum=1;
memset(dfn,0,sizeof(dfn)),memset(low,0,sizeof(low));
memset(head,0,sizeof(head)),memset(vis,0,sizeof(vis));
for(int i=1;i<M;++i) t[i].clear();
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
for(int T=1;T<=10000;++T)
{
cin>>m,clear();
if(!m) break;
for(int i=1;i<=m;++i) cin>>a>>b,add(a,b),add(b,a);
for(int i=1;i<=n;++i)
{
if(!dfn[i])
{
while(!s.empty()) s.pop();
root=i,dfs(i,0);
}//点双
}
for(int i=1,res;i<=tot;++i)
{
res=0;
for(int it:t[i]) res+=vis[it];//看当前点双有几个割点
if(res==1) ++ans,sum*=(t[i].size()-1);//如果只有一个割点就统计答案
}
if(tot==1)
{
cout<<"Case "<<T<<": "<<2<<" "<<n*(n-1)/2<<"\n";
}
else cout<<"Case "<<T<<": "<<ans<<" "<<sum<<"\n";
}
return 0;
}
AT_abc334_g [ABC334G] Christmas Color Grid 2
均匀随机一个绿色块换成红色,求绿色四连通块的期望数量。题意非常清新啊,那么我们就可以枚举每个位置绿颜色块转化成红色之后形成的连通块数量,用每个位置变化后得到的绿色四连通块个数和除去绿色颜色个数就是期望答案了。
那么如何快速判定一个绿点转化红点之后连通块的数量,和点相关,所以考虑点双,那么为了跑点双,肯定需要把图建出来,那么每个绿色块就像自己的上下左右的绿色块连边。于是我们就可以用点双的性质来解题了,首先对于一个孤独的单点,删去之后自然连通块数量 -1,对于一个非割点,删去之后不影响连通性,所以连通块的数量不变。着重考虑的是割点,对于一个同时在
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e3+5,N=4e6+5,mod=998244353;
int n,m,num,res,sum,root;
char opt;
int a[M][M],dfn[N],low[N],c[N];
bool vis[N];
stack<int> s;
int cnt=0;
struct edge{
int to,next;
};edge p[N<<1];
int head[N];
inline int pos(int i,int j){return (i-1)*m+j;}
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
inline int quick(int a,int n)
{
int res=1;
while(n)
{
if(n&1) res=res*a%mod;
n>>=1,a=a*a%mod;
}
return res;
}
inline int inv(int x) {return quick(x,mod-2);}
inline void push(int u,int f)
{
int x;
while(1)
{
++c[(x=s.top())];
s.pop();
if(u==x) break;
}
++c[f];
}
inline void dfs(int u,int f)
{
dfn[u]=low[u]=++num;
s.push(u);int siz=0;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
if(!dfn[v])
{
++siz,dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]) vis[u]=1,push(v,u);
}
else low[u]=min(low[u],dfn[v]);
}
if(!f&&!siz) vis[u]=1;//孤独单点
}//点双
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
//freopen("2.in","r",stdin);
cin>>n>>m;
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j) cin>>opt,a[i][j]=(opt=='#');
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
if(!a[i][j]) continue;
++sum;
if(a[i+1][j]) add(pos(i,j),pos(i+1,j)),add(pos(i+1,j),pos(i,j));
if(a[i][j+1]) add(pos(i,j),pos(i,j+1)),add(pos(i,j+1),pos(i,j));//先连边,每个绿点只用向自己的右下方连边即可
}
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
if(!a[i][j]||dfn[pos(i,j)]) continue;
while(!s.empty()) s.pop();
++res,root=pos(i,j),dfs(pos(i,j),0);
}
}
res*=sum;
for(int i=1;i<=n*m;++i)
{
if(vis[i]) res+=c[i]-1;//每一个割点,就是其所在的点双个数-1,把孤独一个单点 vis 也记成1,那么刚好贡献也为 -1
}
cout<<res%mod*inv(sum)%mod<<"\n";//费马小定理求逆元
return 0;
}
P2783 有机化学之神偶尔会做作弊
给定一张无向图,所有的环缩成一个点,每次询问两个点之间有多少个点。一张无向图,不存在环,那不就是一颗树了,但是题目没有保证图连通,所以处理之后的图应该是一个森林。
处理环,一般使用边双处理,那么边双缩完点之后的树就是题目要求的操作,那么我们就在这些树上进行询问。求任意两个点之间的点个数,实际上就是求树上两点的路径长度+1,有
注意:由于要求两个点不成环,所以两点之间不能存在重边,需要用 map 判下重。输出也有点鬼畜。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#include<stack>
#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e4+5;
int n,m,q,num,tot;
int dfn[M],low[M],col[M],pre[25];
stack<int> s;
map<pii,int> mapp;
vector<int> e[M];
int cnt=0;
struct N{
int to,next,val;
}; N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b,p[cnt].val=c;
}
inline void push(int u)
{
int x;++tot;
while(1)
{
col[(x=s.top())]=tot;
s.pop();
if(x==u) break;
}
}//处理边双
inline void dfs(int u,int pre)
{
dfn[u]=low[u]=++num;
s.push(u);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(p[i].val==pre) continue;
if(!dfn[v])
{
dfs(v,p[i].val);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) push(v);
}
else low[u]=min(low[u],dfn[v]);
}
}
int deep[M],siz[M],fa[M],son[M],top[M];
inline void dfs1(int u,int f,int d)
{
deep[u]=d,siz[u]=1,fa[u]=f;
for(int v:e[u])
{
if(v==f) continue;
dfs1(v,u,d+1);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void dfs2(int u,int topp)
{
top[u]=topp;
if(!son[u]) return ;
dfs2(son[u],topp);
for(int v:e[u])
{
if(!top[v]) dfs2(v,v);
}
}
inline int LCA(int x,int y)
{
while(top[x]!=top[y])
{
if(deep[top[x]]<deep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(deep[x]>deep[y]) return y;
return x;
}
inline int length(int x,int y){return deep[x]+deep[y]-2*deep[LCA(x,y)];}
inline void print(int x)
{
bool flag=0;
for(int i=19;i>=0;--i)
{
if(x>=pre[i]) flag|=1,cout<<1,x-=pre[i];
else if(flag) cout<<0;
}
cout<<"\n";
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m,pre[0]=1;
for(int i=1;i<=19;++i) pre[i]=pre[i-1]*2;
for(int i=1,a,b;i<=m;++i)
{
cin>>a>>b;
if(mapp[mk(a,b)]) continue;//判重边
mapp[mk(a,b)]=mapp[mk(b,a)]=1;
add(a,b,i),add(b,a,i);
}
mapp.clear();
for(int i=1;i<=n;++i)
{
if(!dfn[i]) dfs(i,0),push(i);//边双处理
}
for(int u=1;u<=n;++u)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(col[u]==col[v]||mapp[mk(col[u],col[v])]) continue;//这里也别有重边了,因为有siz数组,有重边siz数组会算多次
e[col[u]].push_back(col[v]),e[col[v]].push_back(col[u]);
mapp[mk(col[u],col[v])]=mapp[mk(col[v],col[u])]=1;
}
}
dfs1(1,0,1),dfs2(1,1);//经典树剖
cin>>q;int x,y,len;
while(q--)
{
cin>>x>>y,x=col[x],y=col[y],len=length(x,y)+1;//答案是路径+1
print(len);
}
return 0;
}
P3854 [TJOI2008] 通讯网破坏
实际上是一道圆方树板子题,但是我们就按着点双的思路来。对于一张无向图,多次询问
于是我们就需要建出一种特殊的树来处理点双之间的连通性,这就是圆方树,这里简单叙述一下。有两种建发(具体我不是很清楚,有一种是所谓的广义圆方树)。
1.对于每一个割点新建一个点,所有点双化为一个点,每个割点建出来的新点向其所在的点双化作的点连一条边。
2.对于每一个点双连一条边,点双内的所有点向这个新建点连一条边,同时每个点双内原有的边都废弃。(广义)
建出来的圆方树,两点之间路径上的所有点才能被称之为必经点。于是树剖之后单点修改
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#include<map>
#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e5+5;
int n,m,q,num,tot,edge_num;
int dfn[M],low[M],col[M];
bool vis[M];
stack<int> s;
map<pii,int> mapp;
vector<int> e[M],res;
vector<vector<int>> ans;
int cnt=0;
struct N{
int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
inline void push(int u,int f)
{
++tot,res.clear();int x;
while(1)
{
res.push_back((x=s.top()));
s.pop();
if(x==u) break;
}
res.push_back(f);
ans.push_back(res);
}
inline void dfs(int u,int f)
{
dfn[u]=low[u]=++num;
s.push(u);
int siz=0;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
if(!dfn[v])
{
++siz;
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]) push(v,u),vis[u]=1;
}
else low[u]=min(low[u],dfn[v]);
}
}
int deep[M],siz[M],fa[M],son[M],top[M],id[M];
inline void dfs1(int u,int f,int d)
{
deep[u]=d,siz[u]=1,fa[u]=f;
for(int v:e[u])
{
if(v==f) continue;
dfs1(v,u,d+1);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void dfs2(int u,int topp)
{
top[u]=topp,id[u]=++num;
if(!son[u]) return ;
dfs2(son[u],topp);
for(int v:e[u]) if(!top[v]) dfs2(v,v);
}
struct SGT{
int tree[M<<2];
inline void update(int u,int ll,int rr,int x,int k)
{
if(ll==rr) {tree[u]+=k;return ;}
int mid=(ll+rr)>>1;
if(mid>=x) update(u<<1,ll,mid,x,k);
else update(u<<1|1,mid+1,rr,x,k);
tree[u]=tree[u<<1]+tree[u<<1|1];
}
inline int query(int u,int ll,int rr,int L,int R)
{
if(L<=ll&&rr<=R) return tree[u];
int mid=(ll+rr)>>1,res=0;
if(mid>=L) res+=query(u<<1,ll,mid,L,R);
if(R>mid) res+=query(u<<1|1,mid+1,rr,L,R);
return res;
}
inline int ask(int x,int y)
{
int ans=0;
while(top[x]!=top[y])
{
if(deep[top[x]]<deep[top[y]]) swap(x,y);
ans+=query(1,1,tot,id[top[x]],id[x]);
x=fa[top[x]];
}
if(deep[x]>deep[y]) swap(x,y);
ans+=query(1,1,tot,id[x],id[y]);
return ans;
}
};SGT t;//树剖+线段树
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1,a,b;i<=m;i++) cin>>a>>b,add(a,b),add(b,a);
dfs(1,0);
for(int i=1;i<=n;i++) if(vis[i]) col[i]=++tot;//对于每一个割点新建一个点
int pos=0;
for(auto res:ans)
{
++pos;
for(int it:res)//建边1所说的
{
if(vis[it])
{
e[col[it]].push_back(pos),e[pos].push_back(col[it]);
}
else col[it]=pos;
}
}
num=0;
dfs1(1,0,1),dfs2(1,1);//数据结构部分了
cin>>q;int k,x,y;
while(q--)
{
cin>>x>>y>>k;
if(!vis[k]) {cout<<"no\n";continue;}
x=col[x],y=col[y],k=col[k];
if(x==k||y==k) {cout<<"no\n";continue;}
t.update(1,1,tot,id[k],1);
if(t.ask(x,y)) cout<<"yes\n";
else cout<<"no\n";
t.update(1,1,tot,id[k],-1);
}
return 0;
}
P7687 [CEOI2005] Critical Network Lines
一张无向图连通上,有
询问边,考虑采用边双求解,由于边双缩点建树后树的性质较为优美,那么我们就建树,在缩点的过程中统计一下这个边双含有的
一棵树,删去一条边,一定只会分成两个连通块。那么只要其中一个连通块内的
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#include<map>
#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e5+5,N=1e6+5;
int n,m,k,l,num,tot,tota,totb,res;
int dfn[M],low[M],siza[M],sizb[M],col[M];
bool visa[M],visb[M];
stack<int> s;
vector<int> e[M];
pii a[M];
map<pii,pii> mapp;
int cnt=0;
struct edge{
int to,next,val;
};edge p[N<<1];
int head[M];
inline void add(int a,int b,int c)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b,p[cnt].val=c;
}
inline void push(int u)
{
++tot;int x;
while(1)
{
col[(x=s.top())]=tot;
siza[tot]+=visa[x],sizb[tot]+=visb[x];
s.pop();
if(x==u) break;
}
}
inline void dfs(int u,int pre)
{
dfn[u]=low[u]=++num;
s.push(u);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(p[i].val==pre) continue;
if(!dfn[v])
{
dfs(v,p[i].val);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) push(v);
}
else low[u]=min(low[u],dfn[v]);
}
}
inline void dfs1(int u,int f)
{
for(int v:e[u])
{
if(v==f) continue;
dfs1(v,u);
siza[u]+=siza[v],sizb[u]+=sizb[v];
}
}
inline void dfs2(int u,int f)
{
for(int v:e[u])
{
if(v==f) continue;
//如果v子树内没有A类型的点 或 v子树内没有B类型的点 或 v子树包含所有A类型的点 或 v子树包含所有B类型的点
if(!siza[v]||!sizb[v]||!(tota-siza[v])||!(totb-sizb[v])) a[++res]=mapp[mk(u,v)];
dfs2(v,u);
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m>>k>>l;
for(int i=1,x;i<=k;++i) cin>>x,visa[x]=1;
for(int i=1,x;i<=l;++i) cin>>x,visb[x]=1;
for(int i=1,a,b;i<=m;++i) cin>>a>>b,add(a,b,i),add(b,a,i);
dfs(1,0),push(1);
//边双缩点
for(int u=1;u<=n;++u)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(col[u]==col[v]||v<=u) continue;
e[col[u]].push_back(col[v]),e[col[v]].push_back(col[u]);
mapp[mk(col[u],col[v])]=mapp[mk(col[v],col[u])]=mk(u,v);
}
}//建树
dfs1(1,0),tota=siza[1],totb=sizb[1];//第一遍预处理以每个点为根时子树的信息
dfs2(1,0);
cout<<res<<"\n";
for(int i=1;i<=res;++i) cout<<a[i].first<<" "<<a[i].second<<"\n";
return 0;
}
P7924 「EVOI-RD2」旅行家
卡常题,被卡惨了(所以不保证代码卡的过去,实在不行看一下其他解法)。在一张无向图上给定多组
由于要走所有的路径,所以考虑使用边双,使用边双缩点建树之后,记录一下每个边双含有点的权值和,对于一组
问题就转化为对于一棵树,多次给定两点,将两点间路径上所有点+1,然后统计出所有权值不为 0 的点,这不树上差分板子题,对于
最后如果
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#include<stack>
#define pii pair<int,int>
#define mk make_pair
using namespace std;
namespace Fread
{
const int SIZE=1<<21;
char buf[SIZE],*S,*T;
inline char getchar()
{
if (S==T)
{
T=(S=buf)+fread(buf,1,SIZE,stdin);
if (S==T)
{
return '\n';
}
}
return *S++;
}
}
namespace Fwrite
{
const int SIZE=1<<21;
char buf[SIZE],*S=buf,*T=buf+SIZE;
inline void flush()
{
fwrite(buf,1,S-buf,stdout);
S=buf;
}
inline void putchar(char c)
{
*S++=c;
if(S==T)
{
flush();
}
}
struct NTR
{
~NTR()
{
flush();
}
}ztr;
}
#ifdef ONLINE_JUDGE
#define getchar Fread :: getchar
#define putchar Fwrite :: putchar
#endif
namespace Fastio
{
struct Reader
{
template<typename T>
Reader&operator>>(T&x)
{
char c=getchar();
T f=1;
while(c<'0'||c>'9')
{
if (c=='-') f=-1;
c=getchar();
}
x=0;
while(c>='0'&&c<='9')
{
x=x*10+(c-'0');
c=getchar();
}
x*=f;
return *this;
}
Reader&operator>>(char&c)
{
c=getchar();
while(c==' ' || c=='\n')
{
c=getchar();
}
return *this;
}
Reader&operator>>(char* str)
{
int len=0;
char c=getchar();
while (c==' '||c=='\n')
{
c=getchar();
}
while (c!=' '&&c!='\n'&&c!='\r')
{
str[len++]=c;
c=getchar();
}
str[len]='\0';
return *this;
}
Reader(){}
}cin;
const char endl='\n';
struct Writer
{
template<typename T>
Writer&operator<<(T x)
{
if(x==0)
{
putchar('0');
return *this;
}
if(x<0)
{
putchar('-');
x=-x;
}
static int sta[45];
int top=0;
while(x)
{
sta[++top]=x%10;
x/=10;
}
while(top)
{
putchar(sta[top]+'0');
--top;
}
return *this;
}
Writer&operator<<(char c)
{
putchar(c);
return *this;
}
Writer&operator<<(char* str)
{
int cur=0;
while(str[cur])
{
putchar(str[cur++]);
}
return *this;
}
Writer&operator<<(const char* str)
{
int cur=0;
while(str[cur])
{
putchar(str[cur++]);
}
return *this;
}
Writer(){}
}cout;
}
#define cin Fastio :: cin
#define cout Fastio :: cout
#define endl Fastio :: endl
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
inline void swap(int &x,int &y){x^=y^=x^=y;}
const int M=5e5+5,N=2e6+5;
int n,m,q,num,tot,num_edge;
int c[M],dfn[M],low[M],col[M],w[M],sum[M];
map<pii,int> mapp;
stack<int> s;
pii e[M];
int cnt=0;
struct edge{
int to,next,val;
};edge p[N<<1];
int head[M];
inline void add(int a,int b,int c=0)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b,p[cnt].val=c;
}
inline void push(int u)
{
++tot;int x;
while(1)
{
col[(x=s.top())]=tot,sum[tot]+=c[x];
s.pop();
if(x==u) break;
}
}
inline void dfs(int u,int pre)
{
dfn[u]=low[u]=++num;
s.push(u);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(p[i].val==pre) continue;
if(!dfn[v])
{
dfs(v,p[i].val);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) push(v);
}
else low[u]=min(low[u],dfn[v]);
}
}
int deep[M],fa[M],siz[M],son[M],top[M];
inline void dfs1(int u,int f,int d)
{
fa[u]=f,siz[u]=1,deep[u]=d;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
dfs1(v,u,d+1);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void dfs2(int u,int topp)
{
top[u]=topp;
if(!son[u]) return ;
dfs2(son[u],topp);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(!top[v]) dfs2(v,v);
}
}
inline int LCA(int x,int y)
{
while(top[x]!=top[y])
{
if(deep[top[x]]<deep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(deep[x]>deep[y]) return y;
return x;
}
inline void dfs3(int u,int f)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
dfs3(v,u);
w[u]+=w[v];
}
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;++i) cin>>c[i];
for(int i=1,a,b;i<=m;++i) cin>>a>>b,add(a,b,i),add(b,a,i);
for(int i=1;i<=n;++i)
{
if(!dfn[i]) dfs(i,0),push(i);
}
for(int u=1;u<=n;++u)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(col[u]==col[v]||mapp[mk(col[u],col[v])]) continue;
e[++num_edge]=mk(col[u],col[v]);
mapp[mk(col[u],col[v])]=mapp[mk(col[v],col[u])]=1;
}
}//边双缩点建树+去重边
cnt=0;for(int i=1;i<=tot;++i) head[i]=0;//md,卡死了,链式前向星快一点
for(int i=1;i<=num_edge;++i) add(e[i].first,e[i].second),add(e[i].second,e[i].first);
dfs1(1,0,1),dfs2(1,1);
cin>>q;int x,y,lca;
while(q--)
{
cin>>x>>y,x=col[x],y=col[y],lca=LCA(x,y);
++w[x],++w[y],--w[lca],--w[fa[lca]];//树上差分
}
dfs3(1,0);
int ans=0;
for(int i=1;i<=tot;++i) if(w[i]) ans+=sum[i];
cout<<ans<<"\n";
return 0;
}
P10875 [COTS 2022] 游戏 M
纯数据结构题,沾了一点边双的性质。
问题是有
首先考虑先连通,我们考虑就按照给定的边的顺序来,一条条加进去,用并查集维护一下,看当前边是否连通的是两个原本不连通的连通块,如果是,说明当前边肯定被当作树边,并查集 merge 一下,如果不是,就有可能被当成多余的边添加进图中,离线下来。
然后考虑这些多余的边有什么作用。因为询问时至少需要添加
那么对于不是树边一条边
那么每次询问两个点
上路径的最大值,如果最大值等于
使用树剖ing。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=3e5+5,inf=1e9;
int n,m,q,tot,root;
int F[M];
int cnt=0;
struct N{
int to,next;
}; N p[M<<1];
struct node{
int x,y,t;
};node a[M];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
inline int find(int x)
{
if(x!=F[x]) F[x]=find(F[x]);
return F[x];
}
inline bool merge(int x,int y)
{
x=find(x),y=find(y);
if(x==y) return 0;
F[x]=y;return 1;
}//并查集的操作
int deep[M],siz[M],fa[M],son[M],col[M];
int top[M],id[M],num;
inline void dfs1(int u,int f,int d)
{
fa[u]=f,siz[u]=1,deep[u]=d,col[u]=root;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
dfs1(v,u,d+1);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void dfs2(int u,int topp)
{
top[u]=topp,id[u]=++num;
if(!son[u]) return ;
dfs2(son[u],topp);
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(!top[v]) dfs2(v,v);
}
}
struct SGT{
int tree[M<<2],lazy[M<<2];
inline void build(int u,int ll,int rr)
{
tree[u]=lazy[u]=inf;
if(ll==rr) return ;
int mid=(ll+rr)>>1;
build(u<<1,ll,mid),build(u<<1|1,mid+1,rr);
}
inline void pushdown(int u)
{
tree[u<<1]=min(tree[u<<1],lazy[u]),tree[u<<1|1]=min(tree[u<<1|1],lazy[u]);
lazy[u<<1]=min(lazy[u<<1],lazy[u]),lazy[u<<1|1]=min(lazy[u<<1|1],lazy[u]);
lazy[u]=inf;
}
inline void update(int u,int ll,int rr,int L,int R,int k)
{
if(L<=ll&&rr<=R)
{
tree[u]=min(tree[u],k),lazy[u]=min(lazy[u],k);
return ;
}
if(lazy[u]!=inf) pushdown(u);
int mid=(ll+rr)>>1;
if(mid>=L) update(u<<1,ll,mid,L,R,k);
if(R>mid) update(u<<1|1,mid+1,rr,L,R,k);
tree[u]=max(tree[u<<1],tree[u<<1|1]);
}
inline int query(int u,int ll,int rr,int L,int R)
{
if(L<=ll&&rr<=R) return tree[u];
if(lazy[u]!=inf) pushdown(u);
int mid=(ll+rr)>>1,res=0;
if(mid>=L) res=query(u<<1,ll,mid,L,R);
if(R>mid) res=max(res,query(u<<1|1,mid+1,rr,L,R));
return res;
}
inline int ask(int x,int y)
{
int res=0;
while(top[x]!=top[y])
{
if(deep[top[x]]<deep[top[y]]) swap(x,y);
res=max(res,query(1,1,n,id[top[x]],id[x]));
x=fa[top[x]];
}
if(deep[x]>deep[y]) swap(x,y);
res=max(res,query(1,1,n,id[x]+1,id[y]));
return res;
}
inline void change(int x,int y,int k)
{
while(top[x]!=top[y])
{
if(deep[top[x]]<deep[top[y]]) swap(x,y);
update(1,1,n,id[top[x]],id[x],k);
x=fa[top[x]];
}
if(deep[x]>deep[y]) swap(x,y);
update(1,1,n,id[x]+1,id[y],k);
}
};SGT t;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
//freopen("P10875_21.in","r",stdin);
cin>>n>>m;
for(int i=1;i<=n;++i) F[i]=i;
for(int i=1,x,y;i<=m;++i)
{
cin>>x>>y;
if(!merge(x,y)) a[++tot]=(node){x,y,i};
else add(x,y),add(y,x);
}//先添加树边,多余的边离线下来
for(int i=1;i<=n;++i)
{
if(!siz[i])
{
root=i;
dfs1(i,0,1),dfs2(i,i);
}//先把得到的树树剖(注意可能是森林)
}
t.build(1,1,n);//初始化线段树
for(int i=1;i<=tot;++i) t.change(a[i].x,a[i].y,a[i].t);//加入多余的边,对树边取min
cin>>q;int x,y,res;
while(q--)
{
cin>>x>>y;
if(col[x]!=col[y]) {cout<<"-1\n";continue;}//不连通
res=t.ask(x,y);
if(res==inf) cout<<"-1\n";//最后都不边双连通
else cout<<res<<"\n";
}
return 0;
}
后面的题先咕咕咕。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效