割点(割顶):割点即割顶,去掉割点后,原图的连通分量变多;
割边(桥):割边即桥,去掉割边后,原图的连通分量变多;
点双连通分量:任意两点之间存在至少两条顶点不同的路径/去掉任意一个点后,图的连通性不变
边双连通通分量:任意两点之间存在至少两条边不同的路径/去掉任意一条边后,图的连通性不变
点双连通和边双连通的区别与联系:
- 两者都是基于无向图而言的
- 点双连通分量一定是边双连通分量(除了两点一线,两点一线是点双,但不是边双),反之不一定
- 点双连通分量之间可以有公共点,但也只能有一个(该点是割点);因此在求点双连通的时候,可以找割点,但是入栈的要是边,因为一个点可能属于多个点双连通分量,若存的是点,那割点出栈后还要入栈。如果存的是边,则不用,但出边的时候要判断边的两个端点是否和之间出去的有重复
- 边双连通分量之间不能有公共点或公共边,连接两个边双连通分量的是割边;因此边双连通就方便了,一个点只能属于一个边双连通分量
二分图:下面针对的是无向图
判定:先对任意一个顶点染色,然后找它相邻点
*若为染色,则染色(递归此点)
*已染色,且颜色和当前结点相同(失败,该图不是二分图)
*已染色,且颜色和当前结点不同,跳过
一些定理:
*如果一个双连通分量内的某些顶点在一个奇圈中(即双连通分量含有奇圈),那么这个双连通分量的其他顶点也在某个奇圈中
证明:假设有一个奇圈,因为是点双,没有割点,必然有紧挨着的圈,设公共点有k个(至少有两个公共点),假设这个圈是偶圈,则这个偶圈必然能和原来的奇圈组成新的奇圈(新的圈=(奇圈-k)+(偶圈-k)= 奇数);这里k是两个圈的公共边上的点数(不包含端点);如果这个圈是奇圈,则也得证
*如果一个双连通分量含有奇圈,则它一定不是二分图;反之,如果一个图是二分图,它一定没有奇圈
对于定理一,如下图,若有一个奇圈(1,2,3,4,5),和它相邻的有一个偶圈(4,5,3,6);则它们的k是1(即公共边<4,5> <5,3>这两条边上的点5,端点3,4不算);新的圈=(5-1)+(4-1)-2 =7,即(1,2,3,6,4).组成一个新的奇圈;即若一个双连通分量内含有一个奇圈,则其他点必定在某个奇圈中(一个点可能属于多个奇圈)
//代码:
/**
求解方法:不断把某个点的树边和方向边入栈,更新当前点的Low,如果某个点是割点,就把栈里存的边弹出
注意:子节点的low可以用来更新父节点的low,而方向边只能通过dfn来更新当前点的low。不能拿方向边的点(祖先结点)的Low来更新当前low,因为祖先结点的low不一定根当前结点有关系
*/
void tarjan(int u,int fa)
{
int child = 0;
low[u] = dfn[u] = ++dfn_cnt;
for(int i = 0; i < G[u].size(); i++)
{
int v = G[u][i];
pii p = MP(u,v); //取得当前这条边
if(!dfn[v]) //如果还没被访问,说明是一条树边
{
S.push(p); //必须在递归前推入栈中
child++;
tarjan(v,u);
low[u] = min(low[u],low[v]);
if(low[v] >= dfn[u]) ///***当前子节点会不会连向u的祖先结点,如果不会,则u去掉后,v子分支
//肯定就断开了。这个判断要在tarjan完这个子节点就判断,因为一个
//结点u可以连着多个点双,不会相互影响,相反,如果等遍历完所有子节点
//才判断就不行了。这里不能换成low[v] >= low[u],应该是和u的dfs序
//比较,看是不是会连到u的祖先结点上
{
iscut[u] = true;
bcc_cnt++;
bcc[bcc_cnt].clear();
for(;;){
pii tmp = S.top(); S.pop();
//判重
if(bccno[tmp.X] != bcc_cnt) bcc[bcc_cnt].PB(tmp.X),bccno[tmp.X] = bcc_cnt;
if(bccno[tmp.Y] != bcc_cnt) bcc[bcc_cnt].PB(tmp.Y),bccno[tmp.Y] = bcc_cnt;
if(tmp.X == u && tmp.Y == v) break;
}
}
} else if(dfn[v] < dfn[u] && v != fa)//该点已被访问过,看看是不是在u的上边,若是,则是一条
//回边,入栈
//这里v!=fa也很重要,因为双连通是针对无向图,我们建图的时候是有正反向边的。
{
S.push(p);
low[u] = min(low[u],dfn[v]);//这里不能用min(low[u],low[v]),v是u的祖先结点,祖先结点
//的low值对其孩子结点来说是不适用的
}
}
if(fa < 0 && child > 1) iscut[u] = true;
}
对于下述情况,经过dfn[a1] < dfn[u],注意这里比较的是结点的先后次序,不是Low值;主要是看该结点是不是u的回向边,所以是根据dfn来比较的;然后low[u] = min(low[u],dfn[v]);这里不能用low[v],不然u的箭头就指向a0了,这样整条线构成点双,但实际情况并不是
边双连通分量
对于边双连通,第一遍dfs找出所有的桥,第二遍dfs遍历整个图,遇到桥或已经遍历过的结点则停止。即可找出每个边双连通分量。此种做法可以判重边和环,如果1--2(1,2连边)则有两个边双,如果是1--2,2--1则有一个边双。具体过程可以参考代码模拟一遍即可。
void tarjan(int u,int fa)
{
low[u] = dfn[u] = ++dfn_cnt;
for(int i = head[u]; ~i; i = edge[i].next)
{
int v = edge[i].v;
//dbg(u,v,"*");
if(!dfn[v])
{
tarjan(v,u);
low[u] = min(low[u],low[v]);
if(low[v] > dfn[u])
{
isbridge[i] = isbridge[i^1] = true;
//dbg("****");
}
}else if(dfn[v] < dfn[u] && v != fa)
low[u] = min(low[u],dfn[v]);
}
}
void dfs_bcc(int u,int id)
{
bccno[u] = id;
for(int i = head[u]; ~i; i = edge[i].next)
{
if(!isbridge[i])
{
int v = edge[i].v;
if(!bccno[v]) dfs_bcc(v,id);
}
}
}
注意点:
双连通(包括边和点)分量都是针对无向图,因此建边的时候要建立正反边。并且dfs的时候记得判断某点的子边是不是它的父节点。任意两个边双连通分量之间都没有公共边,公共点(因此可以缩点??),这点和强连通分量一样。缩点建图后的图一定没有环