连通性问题大杂烩
前言
连通性问题确实时一大比较难啃得蛋糕,每次都要先学习一遍,还不如一次学到通
无向图的连通性问题
求割点
连通图:连通图内的所有点都可以互相到达
割点:将割点删掉后整张图不连通
定理1:
一个点s是割点,当且仅当s作为该连通图的根时,会把连通图分为不相连的几部分
定理2:
一个非根节点u是割点,当且仅当u存在一个子节点v不存在一条边连向u的祖先
一开始我认为只要存在一条边连向祖先就不算割点,但这里举出反例:
实现
考虑dfs序,就是将节点编号为dfs遍历到的顺序
我们设一个dfn表示dfs序,设low表示当前节点能回到的最大祖先的编号,只要存在子节点v的\(low[v]>=dfn[u]\) 就可以了
代码
#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N=2e5+5;
int n,m,u,v,tot,ans;
int dfn[N],low[N],vis[N],dot[N];
vector<pii>b[N];
void dfs(int x,int nxt){
dfn[x]=low[x]=++tot;
vis[x]=1;
bool cnt=0;
int num=0;
for(auto i:b[x]){
if(nxt==i.second) continue;//这里大抵是没有用的
int k=i.first;//要开局部变量,不要像我一样手懒开全局变量寄掉
if(!vis[k]){
num++;
dfs(k,i.second);
low[x]=min(low[x],low[k]);
if(low[k]>=dfn[x]) cnt=1;
}
else{
low[x]=min(low[x],dfn[k]);//无向图中也可以写low
}
}
if(!nxt){
if(num>1) dot[++ans]=x;//注意特判根节点的情况
}
else if(cnt){
dot[++ans]=x;
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&u,&v);
b[u].push_back({v,i});
b[v].push_back({u,i});
}
for(int i=1;i<=n;i++){
if(!vis[i]){
dfs(i,0);
}
}
sort(dot+1,dot+ans+1);
printf("%d\n",ans);
for(int i=1;i<=ans;i++){
printf("%d ",dot[i]);
}
}
割边
只需要把 \(low[v]>=dfn[u]\) 改成 \(low[v]>dfn[u]\) 就行了,因为u的所有后代都到达不了v,从u->v的边一定是割边
边双连通分量
在边双连通分量中,两点间存在至少两条边不重复的路径
类似于割点的办法,但有所不同
将遍历到的点放入栈,若一个点满足 \(low[v]>=num[u]\),则将这个点作为这个边双连通分量中的头,将栈中在它上面的点依次弹出,这之中的点就构成了点双连通分量
点双连通分量
同理两点间存在至少两条点不重复的路径
把边双改一下栈中存的是边就可以了
因为点双中一个点可能在多个点双之中
不信,你画个8试试
有向图的连通性问题
tajan
其实和边双写法一样,我记得以前有人说回溯边只能用dfn不能用low,但是我证实了是可以的!不过也不保准
为什么要用vis数组记录是否在栈中呢?
因为不在栈中的点代表已经单独构成强连通分量了,不可能再与这个点构成强连通分量了,若有横叉边,就是从这个强连通分量到另一个强连通分量,但是这条边并不会到达它的祖先不符合low的定义,反而一定会影响其的low值,所以只能用栈中的点更新
可以用此图直观表示一下:
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int dfn[N],low[N],vis[N],s[N],col[N],en[N],a[N],num[N],vi[N],dp[N];
int cnt,colnum,n,m,u,v,top,ans;
vector<int>b[N];
vector<int>newb[N];
void dfs1(int x){
dfn[x]=low[x]=++cnt;
s[++top]=x;
vis[x]=1;
for(auto i:b[x]){
if(!dfn[i]) dfs1(i);
if(vis[i]) low[x]=min(low[x],low[i]);
}
if(low[x]==dfn[x]){
colnum++;
while(s[top]!=x){
col[s[top]]=colnum;
num[colnum]+=a[s[top]];
vis[s[top]]=0;
top--;
}
col[x]=colnum;
num[colnum]+=a[x];
vis[x]=0;
top--;
}
}
void dfs2(int x){
vi[x]=1;
for(auto i:newb[x]){
// printf("神金%d %d\n",x,i);
if(!vi[i]) dfs2(i);
// printf("他妈%d %d\n",x,i);
dp[x]=max(dp[x],dp[i]+num[x]);
ans=max(dp[x],ans);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=1;i<=m;i++){
scanf("%d%d",&u,&v);
b[u].push_back(v);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) dfs1(i);
}
for(int i=1;i<=colnum;i++){
dp[i]=num[i];
ans=max(dp[i],ans);
}
for(int i=1;i<=n;i++){
for(auto j:b[i]){
if(col[i]!=col[j]){
newb[col[i]].push_back(col[j]);
// printf("草%d %d\n",col[i],col[j]);
en[col[j]]++;
}
}
// printf("傻逼%d %d\n",i,col[i]);
}
for(int i=1;i<=colnum;i++){
if(!en[i]){
// printf("dfs=%d\n",i);
dfs2(i);
}
}
printf("%d",ans);
}
ybtoj练习题
3.4.2
一个强连通分量里的牛都是互相喜欢的,所以我们先缩点,然后判断那个点出度为0,因为他不喜欢任何的牛,说明他被所有奶牛喜欢,但如果有两个出度为0的点,就说明这两头牛都不互相喜欢,就没有解
3.4.3
口胡了
缩完点后,跑一遍拓扑排序,然后考虑dp转移,若u的答案可以更新v,则可以将v的答案更新为 \(dp[u]+num[v]\),然后v的答案个数为 $ans[u]
若u的答案等于v,则可以将v答案的个数加上u答案个数,就做完了
3.4.4
首先考虑将所有限制转为同一限制,发现所有限制都可以表示成 \(a+k\leq b\),因为 \(a<b => a+1\leq b\),然后所以就将每一条限制转化为a向b连上一条边权为k的边,然后缩点,若一个强连通分量中有边权为1的边则一定不合法,否则从入度为0的点值为1开始递推即可
3.4.5
缩点后跑最短路即可,注意不用判重边
3.4.6
某个傻逼缩完点后跑了一遍最小生成树,然后发现样例过不了,手模了一下,开始怀疑最小生成树的正确性,忽然想到它是有向图。。。。。。
缩完点后,它是一张0号点可以到达任意点的图,因为它本身就满足1号点可以到任意点,考虑删去不优的边,最终成一个类似于树的图,且要求边权相加最短
考虑树(和树不同,是一张由父节点指向子节点的图)的性质,就是每个节点除了根节点,入度都为1,所以每个节点贪心的保留入度的边边权最小的就行
3.4.7
考虑缩点,然后找到一条最长链即可
3.4.8
我们把有约束关系的连一条有向边,如果出现环,就说明这环内的所有点必须选或不选,然后树上背包即可
这里留一个问题:为什么不能在缩点之前建虚拟根呢?
3.4.9
口胡了一半,但是没有发现只有n个点有宝藏这个题目的要求
首先我们只需要对有宝藏的点互相连边即可
其次我口胡了出来,对于横竖连边的点,我们只需要针对一行或一列建一个虚拟点,这个点和这一行/列所有有宝藏的点连边即可
然后因为我们只针对有宝藏的点连边,所以我们按一下方式重新建图,map存一下宫室位置即可