图论--连通性--tarjan
tarjan打法博大精深,有向图强连通,无向图点双连通、边双连通、割点、割边~~学得寡人头大,今天整理整理,明天继续干 ~(*^*)~
有向图求强连通:
//强连通是对于有向图而言的,无需考虑某个边的重复访问
//单个点也是一个强连通分量,要根据题目的意思对结果进行特判
//dfs自上而下,而强连通的分离自下而上,故用栈这个数据结构来存储
int mystack[maxv], dfn[maxv], low[maxv], indx, top;
int belong[maxv], scc_cnt; //所属的强连通的编号
bool in_stack[maxv];
void tarjan(const int& q)
{
dfn[q] = low[q] = ++indx; //时间戳
mystack[++top] = q;
in_stack[q] = true;
for(int i = head[q]; i; i = e[i].next){
if(!dfn[e[i].to]){
tarjan(e[i].to);
low[q] = min(low[q], low[e[i].to]);
}else if(in_stack[e[i].to]){
low[q] = min(low[q], dfn[e[i].to]);
}else
continue;
}
int v = 0;
if(dfn[q] == low[q]){
scc_cnt += 1;
do{
v = mystack[top--];
in_stack[v] = false;
belong[v] = scc_cnt;
}while(q != v);
}
return;
}
接下来的操作都是对于无向图,由于无向图的双向性,以下的方法都要避免一条无向边的多次访问,标记点或是标记边。
无向图求割点:
- 标记点 因为重边对割点无影响
割点是对于无向图而言的,note that 重边对于割点的判断没有影响,故重边都看作一条边!
由于无向图的边双向,为了防止某条边的重复访问,引入一个root
root的作用:
1)root是第一个调用tarjan()函数的点的编号,此时root是dfs搜索树的根。对于根是否是割点--至少有两个子树的根是割点,所以对于根类型,我们要统计它的子树个数。有许多人都是直接统计子树最后再判断是否是根,我觉得那样逻辑不太清楚~也是因为这个他人的代码我理解了大半天~
2)在搜索过程中root用于标记当前访问点的上一个节点,所以在访问dfn[nv]!=0的点时,即之前访问过的点时,若root == nv说明这条边之前访问过,只是边是双向仍然可以通回,所以对于这个nv点就没必要进行访问。所以仅当(root != nv)我们才进行low[u] = min(low[u], dfn[nv])的判断。但这种因用法人而异,有的人不这么写,而是直接将每次root固定函数内递归时仍然为tarjan(nv, root),这样也没问题,因为root == u的情况只出现 在根节点,所以递归传入的"root"是root或是u问题不大,只是既然都写多了个参数还是好好利用利用吧~
割点的判断:
1)u是根节点:当u有>=2个子树时,u是割点
2)u不是根节点:当u存在下一个子节点nv无论如何最多只能回溯到u或者u的子节点时,也可以说是nv无法回溯到u的严格祖先时,u是割点
稍微解释下,nv要访问u的祖先们只能通过u,说明u为nv回溯的必经之点。注意这里已经考虑过重边的问题,同样建议纸上模拟模拟,理解更深哦~
*我们求的是割点,所以不需要栈什么之类的
尤其注意区分时间戳dfn【】,于最早回溯编号low【】
int dfn[mav], low[maxv], indx;
bool is_cut[maxv];
void tarjan_v(int u, int root){ //u root 相当于一条边的两端
dfn[u] = low[u] = ++indx;
int subtree = 0;
for(int i = head[u]; i; i = e[i].next)
{
int nv = e[i].to;
if(!dfn[nv])
{
tarjan(nv, u);
low[u] = min(low[u], low[nv]); //最小能够到达的节点
if(u != root && low[nv] >= dfn[u])//非根节点且其节点为根节点的子树中任意节点最多回溯到其本身,只要存在一个即可!
{ //注意是nv的最早能回溯到的编号low【】与u的时间戳dfn【】(dfs第一次遍历到的次序)相比较!
is_cut[u] = true;
}
if(u == root) //是根节点则统计子树
subtree +=1;
}else if(nv != root)
low[u] = min(low[u], dfn[nv]);
}
if(u == root && subtree >= 2)//是根节点,且有两个子树
is_cut[u] = true;
return;
}
无向图求边双连通
边双连通类似有向图的“强连通”:标记边 因为重边会影响边双连通的判断
- 某个极大子图中去掉任意一条边都不会改变此图的连通性,则该极大子图为边双连通分量。
- 若某极大子图为边双连通分量 <==> 该图上任意两点至少有两条所有边完全不同的路径相连(例如一个环上两点,既可以顺时针来,也可以逆时针来)
- 简而言之--不存在桥(割边)的极大子图为边双连通分量
求法也几乎与有向图中的强连通类似:只是要考虑双向边重复访问的,即两点间有多条双向边的问题。由于两点间可能有多条边,相比单向图的强连通球法我们引入多一个参数--eid,这代表到达u这个点刚经过的边,着重讲讲防止重复遍历一条边:
以链式向前星的存图方式为例。首先了解一下与1异或这个小技巧,对于偶数与1异或相当于+1,对于奇数与1异或相当于-1,存双向边本质上是存了两个反向的单向边,所以恰好是成对存储的(如果你用其他神仙操作不成对存,那在下就告辞聊)。为了方便我们将边的编号从2开始编号,这样一条双向边的编号就是一对<偶数,奇数>,这样我们就恰好可以利用与1异或的性质来根据当前的编号判断当前边是否是访问过的eid边的成对的另一条边。所以这就是为何有那句 if(i == (1^eid)) continue;
对于其他存图方式则依据这种原理利用其他方法判断即可
注意图中的每个点只可能属于一个边双连通分量,等会讲点双连通分量,注意区分!!
int dfn[maxv], low[maxv], indx;
int belong[maxv], bcc_cnt;
int mystack[maxv], top;
void tarjan( int u, int eid){ //经过eid这条边到达的u点
dfn[u] = low[u] = ++indx;
mystack[++top] = u;
in_stack[u] = true;
for(int i = head[u]; i; i = e[i].next)
{
if(i == (1^eid)) continue; //i边是到达u访问过的,是与eid成对的双向边的单向边之一
int nv = e[i].to;
if(!dfn[nv])
{
tarjan(nv, i);
low[u] = Min(low[u], low[nv]);
}else if(in_stack[nv]){
low[u] = Min(low[u], dfn[nv]);
}else
continue;
}
if(low[u] == dfn[u]){
++bcc_cnt;
int tmp = 0;
do{
tmp = mystack[top--];
in_stack[tmp] = false;
belong[tmp] = bcc_cnt; //缩点(将该连通分量上的点同一用一个编号表示)
}while(u != tmp);
}
return;
}
无向图求点双连通
参考自:https://www.cnblogs.com/LiHaozhe/p/9527136.html
点双连通:标记点 因为重边不影响割点或是树根的判断,故再多重边都当作一边处理,弄清这里的逻辑哦~
- 对于无向图中,任意两点至少存在两条点完全不同的路径相连,则该图点双连通。
- 简而言之,无割点的图是点双连通图。
三个重要的性质:
1)bcc中无割点
2)若两个bcc有公共点,则该公共点为两个bcc组成的图的割点
3)每一个割点至少属于两个bcc,非割点只属于一个bcc。(注意由于割点的定义,形如 a--b 也称作以个点双连通,做题时注意题目要求特判。
由于割点会同时属于多个bcc,所以有很多其他博主都用栈存边的方式来查找,这里介绍一种仍然存点的方式。这里的u和root与求割点中的意义相同。给一个结论每个bcc都在其最先发现的点(必是割点或dfs树根)的子树中。所以,关键是找到割点或树根,找到割点、树根就找到了点连通分量,所以主要操作与找割点很像,唯一不同在于无需将树根(root == u)的情况另外考虑,只需要满足(low[nv] >= dfn[u])就可以,因为此时要么是割点,要么是树根。之后将该子树与当前节点加入到bcc中。
*要注意得到点双连通分量后出栈时,出到根/割点之前就停止,这是用存点的方法求点双连通的一个关键点,因为割点必然属于多个点双连通分量,若一下子全部出栈会造成其他点连通点的缺失,这里建议在纸上自己模拟一次,理解了才不容易犯错!
int dfn[maxv], low[maxv], indx;
int mystack[maxv], top;
int bcc[maxv], bcc_cnt;
void tarjan_bcc_v(int u, int root) //root u 相当于一条边的两端点
{
dfn[u] = low[u] == ++indx;
for(int i = head[u]; i; i = e[i].next)
{
int nv = e[i].to;
if(!dfn[nv])
{
mystack[++top] = nv; //搜索到的点入栈
tarjan(nv, u);
low[u] = min[low[u], low[nv]];
if(low[nv] >= dfn[u]){ //是割点或根
bcc_cnt += 1;
int tmp = mystack[top--];
while(tmp != nv)
{
bcc[bcc_cnt].push_back(tmp);
tmp = mystack[top--];
}//一直出栈到割点之前
bcc[bcc_cnt].push_back(tmp);
bcc[bcc_cnt].push_back(u); //虽然加入到了一个bcc中,当前割点仍然在栈中
}
}else if(nv != root)
low[u] = min(low[u], dfn[nv]);
}
return;
}
无向图求割边(桥)
嗯,对的,割边最后讲,因为有两种求法:
1)先将无向图用边双连通的方法缩点处理,得到一个新图,此时所有新点间的边都是桥
2)在tarjan中若low[nv] > dfn[u],则u与v间相连的边是桥,因为nv最早能够回溯到的点在u之后,所以u与nv间的边不可或缺。
法一:在进行边双连通操作时查找
void tarjan(int u, int eid){ dfn[u] = low[u] = ++indx;for(int i = head[u]; i; i = e[i].next)
{ if(i == (1^eid)) continue; int nv = e[i].to; if(!dfn[nv]) { tarjan(nv, i); low[u] = Min(low[u], low[nv]); if(low[nv] > dfn[u]){ // 只要存在一个则是桥 **编号为i这条边是桥(怎么保存,看题目而定吧)**
} }else if(in_stack[nv]) low[u] = Min(low[u], dfn[nv]); else continue; }return; }
法二:缩点后在新图上查找
void tarjan(int u, int eid){ dfn[u] = low[u] = ++indx; mystack[++top] = u; in_stack[u] = true; for(int i = head[u]; i; i = e[i].next){ if(i == (1^eid)) continue; int nv = e[i].to; if(!dfn[nv]) { tarjan(nv, i); low[u] = Min(low[u], low[nv]); }else if(in_stack[nv]) low[u] = Min(low[u], dfn[nv]); else continue; } if(low[u] == dfn[u]){ bcc_cnt += 1; int tmp = 0; do{ tmp = mystack[top--]; in_stack[tmp] = false; belong[tmp] = bcc_cnt; //缩点 }while(tmp != u); } return; } void find_minBridge(){ for(int u = 1; u<=n; ++u){ for(int t = head[u]; t; t = e[t].next){ int nv = e[t].to; if(belong[u] != belong[nv]){//两点间的边是桥 u与nv之间的边t是桥
} } } return; }
emm, last but not least,点双连通一定是边双连通,边双连通不一定是点双连通。