【算法日记】Tarjan

图论

Tarjan

Tarjan 求强连通分量

在图中找到一个最大的图,使这个图中的每个两点能够互相到达。
用DFS搜,将每一个强连通分量作为搜索树上的子树。

模板

输出图中的每个强连通分量

//DFN[]作为这个点搜索的次序编号(时间戳
//LOW[]作为每个点在这颗树中的,最小的子树的根,每次保证最小,like它的父亲结点的时间戳这种感觉。如果它自己的LOW[]最小,那这个点就应该从新分配,变成这个强连通分量子树的根节点。
//每次找到一个新点,这个点LOW[]=DFN[]。
struct node {
    int v,next;
}edge[1001];
int DFN[1001],LOW[1001];
int sstack[1001],heads[1001],visit[1001],cnt,tot,index;
void add(int x,int y)
{
    edge[++cnt].next=heads[x];
    edge[cnt].v = y;
    heads[x]=cnt;
    return ;
}
void tarjan(int x)//代表第几个点在处理。递归的是点。
{
    DFN[x]=LOW[x]=++tot;// 新进点的初始化。
    sstack[++index]=x;//进站
    visit[x]=1;//表示在栈里
    for(int i=heads[x];i!=-1;i=edge[i].next)
    {
        if(!DFN[edge[i].v]) {//如果没访问过
            tarjan(edge[i].v);//往下进行延伸,开始递归
            LOW[x]=min(LOW[x],LOW[edge[i].v]);//递归出来,比较谁是谁的儿子/父亲,就是树的对应关系,涉及到强连通分量子树最小根的事情。
        }
        else if(visit[edge[i].v ]){  //如果访问过,并且还在栈里。
            LOW[x]=min(LOW[x],DFN[edge[i].v]);//比较谁是谁的儿子/父亲。就是链接对应关系
        }
    }
    if(LOW[x]==DFN[x]) //发现是整个强连通分量子树里的最小根。
    {
        do{
            printf("%d ",sstack[index]);
            visit[sstack[index]]=0;
            index--;
        }while(x!=sstack[index+1]);//出栈,并且输出。
        printf("
");
    }
    return ;
}
int main()
{
    memset(heads,-1,sizeof(heads));
    int n,m;
    scanf("%d%d",&n,&m);
    int x,y;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for(int i=1;i<=n;i++)
         if(!DFN[i])  tarjan(1);//当这个点没有访问过,就从此点开始。防止图没走完
    return 0;
}

//vector 实现邻接表
const int N=100005;
vector <ll> g[N]; 
stack <ll> s;
ll color[N],vis[N],dfn[N],low[N],cnt[N];// dfn[i] i出现的次序 low[i] 点i在这棵树中最小子树的根 
ll deep,top,sum,res=0;//deep 节点编号  top 栈中元素数量 sum 强连通分量的数目
void tarjan(int v){
	dfn[v] = low[v] = ++deep;
	vis[v]=1;
	s.push(v);
	for(unsigned i=0;i<g[v].size();i++){
		ll id = g[v][i];
		if(!dfn[id]){
			tarjan(id);
			low[v]=min(low[v],dfn[id]);
		} 
		else{
			if(vis[id]){
				low[v]=min(low[v],dfn[id]);
			}
		}
	}
	if(low[v] == dfn[v]){//自己和子节点形成了强连通分量或者自己只有一个人 
		color[v]= ++sum;
		vis[v]=0;
		while(s.top()!=v){
			color[s.top()]=sum;
			vis[s.top()]=1;
			s.pop();
		} 
		cout<<endl;
	}	
}
int main(){
	int n,m;
	n=read();m=read();
	for(int i=1;i<=m;i++){
		g[read()].push_back(read());
	}
	return 0;
}

Tarjan缩点

原理:强连通分量对于一些贡献有传导性
对于一个有向环,可以缩成一个点。

引入问题

一个有向图,有n个点以及m条边,我们至少应该添加几条边才能使整个图变成强连通图。或者是一个无向图至少添加几条边变成连通图。

思路

我们计算入度为零的点数为a,出度为零的点数为b,那么我们至少需要添加的边数为max(a,b),如果只有一个点的话,我们不需要添加任何边。
那么我们怎么把一个图转换为DAG呢,因为上面给出的图可能存在环,那么我们就会想到把已经组成全连通的子图转换成一个点来看,那么我们最终的图就不会包含环了。
我们使用Tarjan算法求解出强连通分量之后,我们上面使用了一个color数组将同一个连通分量的点分配相同的数值,然后我们再次遍历一遍所有的边,如果边的两侧u->v不属于同一颜色,那么u对应颜色将会有一条边连向v对应的颜色。在此基础上我们可以计算缩点之后的出入度,得到max(a,b)或者其他一些信息。

实现

#define MAX 10005
#define ll long long

vector<ll> g[MAX];
ll color[MAX], vis[MAX], stack[MAX], dfn[MAX], low[MAX], cnt[MAX], num[MAX];
ll ind[MAX], outd[MAX];//每个点的出度入度
//deep:节点编号 top:栈顶  sum:强连通分量数目
ll deep, top, sum, res = 0;

void tanjan(ll v) {
	dfn[v] = ++deep;
	low[v] = deep;   //(1)初始化dfn数组,同时将low设置为相同值
	vis[v] = 1;
	stack[++top] = v;//(2)入栈,作为栈顶元素,同时更新vis数组

	for (unsigned i = 0; i < g[v].size(); i++) {//(3)遍历所有可能到达的点
		ll id = g[v][i];
		if (!dfn[id]) {//如果这个点从没访问过,则先放问它,再用它更新low[v]的值
			tanjan(id);
			low[v] = min(low[v], low[id]); //他的儿子如果能连到更小的祖先节点,显然他也可以
		}
		else {
			if (vis[id]) {//不在栈中的点,要么没有访问,要么不能到达id,所以只需要判断栈中的
				low[v] = min(low[v], low[id]);
			}
		}
	}

	if (low[v] == dfn[v]) {//(4)自己和子节点形成了强连通分量,或者只有自己孤身一人
		color[v] = ++sum; num[sum]++; //num统计该颜色有多少点
		vis[v] = 0;
		while (stack[top] != v) {//将从v开始所有的点取出
			color[stack[top]] = sum;//给予同一颜色
			vis[stack[top--]] = 0;//出栈要顺便修改vis
			num[sum]++;
		}
		top--;
	}
}
int main(){
	for (int i = 1; i <= N; i++) {
		for (unsigned k = 0; k < g[i].size(); k++) {
			ll v = g[i][k];
			if (color[v] != color[i]) {//二者分属于不同的联通集
				outd[color[i]] += 1; //以颜色作为点,更新相应点的出度
			}
		}
	}
}

Tarjan割点

割点定义:对于一个无向图,如果这个顶点集合以及集合中相关联的边,图的联通分量增多,这个点为割点。
对于根节点,如果子树数量>=2 就是割点(子树互不相连,而且不连向祖先)
对于非根借点 维护数组dfn和low,对于边(u,v),如果low[v]>=dfn[u] u就是割点

解释

low[v]>=dfn[u] 意味着v不能回到u前面 u的儿子可以不通过u访问到u的祖先,那么去掉u后,不影响连通性,那么u就不是割点。反之如果u如果存在儿子节点,必须要通过u链接到u的祖先,那么去掉u,连通性增加,u就是割点。

注意事项!!!

对于tarjan 可以将更新语句改为 low[u]=min(low[u],dfn[v]);
对于割点 这一句就不可以
在求强连通分量中,如果v已经在栈里,那么u,v一定在同一个强连通给分量中,所有Low[u]=low[v]是必然,提前更新也不会有问题。
但是,在求割点过程中,low不能追溯到最早祖先,因为这是一个无向图。应该是最早能绕到的割点。如果把dfn[v]改掉就会翻过头。


void tarjan(ll u, ll r) {// v:当前点  r:本次搜索树的root
	dfn[u] = low[u] = ++deep;
	ll child = 0;
	for (unsigned i = 0; i < g[u].size(); i++) {
		ll v = g[u][i];
		if (!dfn[v]) {
			tarjan(v, r);
			low[u] = min(low[u], low[v]);
			if (low[v] >= dfn[u] && u != r)cut[u] = 1;//不是根而且他的孩子无法跨越他回到祖先
			if (r == u)child++; //如果是搜索树的根,统计孩子数目
		}
		low[u] = min(low[u], dfn[v]);//已经搜索过了
	}
	if (child >= 2 && u == r)cut[r] = 1; //是根节点且子树>=2
}

学习博客来源:https://blog.csdn.net/csyifanZhang/article/details/105370924?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160488705219724836759948%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=160488705219724836759948&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-1-105370924.pc_first_rank_v2_rank_v28p&utm_term=Tarjan&spm=1018.2118.3001.4449

原文地址:https://www.cnblogs.com/Shayndel/p/13951305.html