强连通分量的递归写法不解释,如有问题请右转传送门:
强连通分量–Tarjan个人理解+详解
优势:防止tarjan求强连通分量时爆栈
从递归式出发,程序是从原来的递归程序运行一半,递归运行下一层。直至下一层的递归调用完毕后,再回溯至源程序。而手工栈需要模拟系统栈的调用方式,先定义一个类似于栈的数组或数据结构,再通过while循环调用进行模拟运算。
但漏洞是很明显的,在系统栈的调用中,原递归程序的运行都只运行到一半,待所有更低级的调用结束后再完成下半部分。很明显,用while循环完成的手工栈无法使每一层的运算都恰好停留在向下调用时的位置。这时候我们需要根据原来的递归程序进行形式上的改写,尤其是for循环部分,需要将循环完整的部分进行拆分。
具体解释附在每行代码旁,未被注释部分是非递归式写法,注释部分是原递归写法:
struct stack
{
int ch,u,down;
}sp[maxn];
//非递归的栈
void tarjan(int u)
{
int tp=0;
tp=1;
sp[tp].u=u;
sp[tp].ch=head[u];
int v,t;
while(tp)
{
stack &cur=sp[tp];//所有对cur的操作,均视为对sp[tp]的操作
u=cur.u;
if(cur.ch==head[u])
{//第一次遍历
pre[u]=low[u]=++dfs_clock;
s[++top]=u;//同tarjan递归程序初始时的压栈操作
}
if(cur.down)//用所有tarjan过的子孩子更新自己的low值
low[u]=min(low[u],low[cur.down]);
if(cur.ch==-1)
{//所有子结点均已经遍历
if(pre[u]==low[u])
{
scc_cnt++;
while(t!=u)
{
t=s[top--];
belong[t]=scc_cnt;
}
}
tp--;
continue;//返回至上一层的tarjan
}
v=edge[cur.ch].v;//遍历u结点的孩子
cur.ch=edge[cur.ch].nxt;//链表存边
if(!pre[v])//如果未被遍历则进行tarjan操作
{
cur.down=v;//当前u的子结点v已经被tarjan过
tp++;
sp[tp].u=v;//下一层的父亲结点
sp[tp].ch=head[v];//下一层的子结点的第一个地址,等于for循环中的i=head[u]
sp[tp].down=0;//初始化子结点为0,只有在下一层的某个子结点被tarjan计算时,过才被赋值,以用于更新low值
}
else if(!belong[v])//如果被遍历过且目前不属于任何强连通分量,则用pre[v]更新low[u]的值
low[u]=min(low[u],pre[v]);
}
// low[u]=pre[u]=++dfs_clock;
// s[++top]=u;
// for(int i=head[u];i!=-1;i=edge[i].nxt)
// {
// if(!pre[edge[i].v])
// {
// tarjan(edge[i].v);
// low[u]=min(low[u],low[edge[i].v]);
// }
// else if(!belong[edge[i].v])
// {
// low[u]=min(low[u],pre[edge[i].v]);
// }
// }
// if(low[u]==pre[u])
// {
// scc_cnt++;
// while(1)
// {
// belong[s[top]]=scc_cnt;
// if(s[top]==u)
// {
// top--;
// break;
// }
// top--;
// }
// }
}