强连通分量那些事
tarjan
先定义一下强连通分量,就是在一个图中的一个子集,子集中的每一个点都可以相互到达,并且再加入一个点都不满足这个性质,我们称它二这个图的极大强连通子集,也叫强连通分量。
我们发现,其实一个不在环内的单点,or一个环就可以构成强连通分量,那么我们怎么去求呢,一个叫tarjan的牛人给出了答案,也就是dfs生成树的方式。
首先我们定义low数组是这个点可以到达的点中dfs顺序最小的点,dfn就是dfs的顺序,之后我们每一次dfs的时候都把找到的点放入一个栈s中,最后当low[x]==dfn[x]的时候我们就开始从s里一个一个弹出点,直到栈顶的点是x为止,这些弹出的点就构成了一个强联通分量,来一波图片解释一下
弹出6
返回到5 弹出5
返回到3,找到4,之后4又找到1,但是1在栈里面所以不管他(这也就是为什么找的答一定是极大强连通子集)
找到2,最后又回溯到1,发现low[1]==dfs[1]之后开始弹出
那么正确性显然 ,所有不是这个强连通分量里的点早就弹出了,并且加入像上图一样环套环,我们也因为有vis(记录点在不在栈里面),所以保证了极大性。
模板如下
bool via[10000];
int s[10000];//用来存栈的
int tt=0,dfs_cnt=0;
int dfn[10000];//用来存dfs顺序的
int low[10000];//用来存可以到达最小的点
int qlt[100000];//用来存每一个分量的
void taijian(int x,int faget){
vis[x]=true;
dfn[x]=low[x]=++dfs_cnt;//每一个点最先可以到达自己
s[++tt]=x;
for(int i=head[x];i>0;i=nex[i]){
if(!dfn[to[i]]){
taijian(to[i],x);
low[x]=min(low[x],low[to[i]]);
}
else if(vis[to[i]]){
low[x]=min(low[x],dfn[to[i]]);
}
}
if(low[x]==dfn[x]){
group_cnt++;//用来记录强连通
int cp;
do{
cp=s[tt--];
qlt[cp]=group_cnt;
vis[cp]=false;
} while(cp!=x)
}
}
那么这个算法除了求环以外还可以干什么,那就是缩点,我们把一个强连通分量里面的点看成一个点,在有些图中就可以发现题目不一样的性质,至于这个操作就是把一个分量中的点染成一样的颜色就OK了
tarjan缩点的性质
首先对于一个有向图缩完点之后一定是变为一个DAG(有向无环图)。
之后就是对于每一个强连通分量的编号都是反拓扑序。
之后我们利用这个性质可以做一道题,就是这个P3387
首先我们知道要求的一条路最长,之后对于一个强连通里的点肯定可以一起选的,之后又就是缩完点之后的操作,显然要找拓扑排序路径上的最大值,u边拓扑边更新,之后因为之前的编号是反拓扑,显然我们从后往前去更新会更加方便(也就是说我们不用去找拓扑序了),最后就是统计最大值了。
割边(无向图中)
这个割边的定义就是删去这条边之后,整个图的连通块数量增加的边,就叫做割边。
那么我们怎么去求这个割边呢,其实也是tarjan算法,我们发现在dfs搜索树上有可能有些点会有返祖边,就会根据这个去更新low,但是假如一个点的low在更新完之后都比 dfn[fa] 大,那么也就是说这个点永远不会回到上面的scc中,所以链接这两个点的边就会是割边。
代码之后和边双的放在一起
边双连通分量(无向图中)
定义就是不走割边的连通块,所以我们就用这个性质去找边双连通分量。
也就是说去遍历每一个点,假如说这个点还没有被染色,那么我们就去遍历这个点所有可以到达的点,并且不走割边,之后这些点都会在同一个边双连通分量中。
边双的性质
在缩完点之后会形成一个树,之后就可以在这个树上操作
代码实现
#include<bits/stdc++.h>
using namespace std;
const int maxn=10005;
int n,m;
struct road{
int head[2*maxn], to[maxn*2],nex[2*maxn],num=1;
void add(int x,int y){
to[++num]=y;
nex[num]=head[x];
head[x]=num;
}
int col[maxn],low[maxn],dfn[maxn];
int bj[2*maxn],tot=0,nm;//bj是标记割边
void dfs(int x,int fa){
dfn[x]=low[x]=++tot;
for(int i=head[x];i;i=nex[i]){
if(i == (fa^1))continue;
if(dfn[to[i]] == 0) {
dfs(to[i],i);
if(low[to[i]] > dfn[x]) bj[i] = bj[i ^ 1] = 1;//求割边
low[x]=min(low[x],low[to[i]]);
}
else low[x]=min(low[x],dfn[to[i]]);
}
}
void dfs2(int x){
col[x]=nm;
for(int i=head[x];i;i=nex[i]){
if(bj[i]) continue;
if(col[to[i]]) continue;
dfs2(to[i]);
}
}
int rb[maxn];
void tarjan(){
dfs(1,0);
for(int i=1;i<=n;i++) if(!col[i])++nm,dfs2(i);//找双连通分量
for(int k=1;k<=n;k++){
for(int i=head[k];i;i=nex[i]){
if(!bj[i]) continue;
rb[col[to[i]]] ++;
}
}
int as=0;
for(int i=1;i<=nm;i++) {
if(rb[i] == 1) as++;
}
cout<<(as+1)/2<<"\n";
}
}G;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++) {
int a,b;
scanf("%d%d",&a,&b);
G.add(a,b);
G.add(b,a);
}
G.tarjan();
return 0;
}
//边双连通分量——>不走割边的连通块,缩完点之后变为一棵树 ,这是在无向图中的,之后就是缩完点的树的边都是割边
//判断割边的方法就是 low[son] > dfn[fa] 也就是说这个儿子永远不会访问到比父亲根高的点,所以这条边就是割边
//割边的的定义是 -> 删除这条边之后连通块的数量增加
//割点割边边双
//tarjan缩点所给的每一个scc的编号就是反拓扑序
割点(无向图中)
这个和刚才割边的定义差不多,也就是我们再删掉这个点及其联通的边之后,整个图的连通块的数量增加了,这个点就是割点
判断其实就比刚才多了一个等于号,也就是假如说low[son] >= dfn[x] 那么x就是割点
若这个点是根节点,就必须有两个及以上的子搜索树上的点满足上面的限制
代码如下
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e4+4;
const int maxm=1e5+5;
int n,m;
struct group{
int head[2*maxm],to[2*maxm],nex[2*maxm],num=0;
void add(int x,int y){
to[++num]=y;
nex[num]=head[x];
head[x]=num;
}
int bj[maxn],root[maxn],dfn[maxn],low[maxn];
int tt=0,cnt=0;
void dfs(int x){
int p=0;
dfn[x] = low[x] = ++tt;
for(int i=head[x];i;i=nex[i]){
if(!dfn[to[i]]){
dfs(to[i]);
if(low[to[i]] == dfn[x] )p++; //看看有几个儿子满足性质
low[x] = min(low[x],low[to[i]]);
}
else low[x] = min(low[x],dfn[to[i]]);
}
if(root[x] == 1 && p >= 2) bj[x] = 1,++cnt;
else if(root[x] == 0 && p >= 1) bj[x] = 1,++cnt;
}
void tarjan(){
for(int i=1;i<=n;i++) if(dfn[i] == 0) root[i]=1,dfs(i);
printf("%d\n",cnt);
for(int i=1;i<=n;i++){
if(bj[i] == 1) printf("%d ",i);
}
}
}G;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i){
int a,b;
scanf("%d%d",&a,&b);
G.add(a,b);
G.add(b,a);
}
G.tarjan();
return 0;
}
拓扑排序
不知道为什么设个会出现在这里哈哈哈
这个主要是去处理图上的dp,这个可以保证无后效性,之后具体的实现也比较简单,就是模拟就ok了,之后反拓扑排序也是可以使这个dp无后效性的
题目
来一个强连通分量加缩点的题
luoguP2341
这道题我们发现每一个强连通分量里面的点的成为明星的机会是相同的,所以可以当成一个点考虑,之后我们就会发现这成了一个DAG,由于DAG的性质,(不知道的可以看看我另外的博客),加入只有一个点的出度为0,那么所有点都可以到达这个点,加入有其他出度为0的点,那么就不会有奶牛被所有奶牛所喜欢,所以这道题就做完了。
可以在这里看一看代码https://www.luogu.com.cn/paste/zvkkpph4
https://www.luogu.com.cn/problem/P2812
这是一道很典型的缩点伪问题,其实而后上面的差不多,我们发现每一个强连通分量都可以看成一体去讨论,而要选多少的点加入计算机其实就是统计入度为0的分量,这个比较好理解。
至于加多少条边其实就是让整个图变为一个强连通图,那么怎么加呢,肯定还是在入度和出度上下文章,我们知道一个性质,假如说一个图缩点变为dag图之后,如果每一个分量的入度和出度都不为0,那么必定可以再缩点or有自环,那么我们就考虑到两两匹配,那么显然最优的解就是min入度0的点,出度0的点,证明有一个大佬证明了(我实在懒得看)可以在洛谷上看看,最后这就是本题的思路