tarjan求强联通分量

DAG定理

  • 有向无环图中唯一出度为 0 的点,任意点都可以到达
  • 有点无环图中任何入度不为0的点,一定可以从某个入度为 0 的点到达

tarjan求强联通分量

算法流程

  • d f n [ u ] dfn[u] dfn[u]: 深度优先搜索遍历时结点 u u u 被搜索的次序
  • l o w [ u ] low[u] low[u]:以 u u u 为根的子树中所有结点的 d f n dfn dfn 的最小值

参考链接

练习题

P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G

链接:https://www.luogu.com.cn/problem/P2341

题意:给定一个有向图,问所以点都可以到达的点有多少个。

思路:先将有向图变成DAG,然后根据定理 1 ,统计出度为 0 的点是否唯一,如果唯一,那么答案就是这个点代表的scc的大小。否则答案为0。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e4+10,maxm=5e4+10;

vector<int> e[maxn];
int dfn[maxn],times,low[maxn];
stack<int> sta;
bool insta[maxn];
int scc[maxn],scnt,num[maxn];

int deg[maxn];//出度 

void dfs(int u)
{
	dfn[u]=low[u]=++times;
	sta.push(u);insta[u]=1;
	for(auto v: e[u])
	{
		if(!dfn[v])
		{
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
		else if(insta[v]) low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u])
	{
		scnt++;
		while(1)
		{
			int v=sta.top();sta.pop();insta[v]=0;
			scc[v]=scnt;num[scnt]++;
			if(v==u) break;
		}
	}
}
int n,m;
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		e[u].push_back(v);
	}
	for(int i=1;i<=n;++i)
		if(!dfn[i]) dfs(i);
	for(int u=1;u<=n;++u)
	{
		for(auto v: e[u])
		{
			if(scc[v]!=scc[u])
				deg[scc[u]]++;
		}
	}
	int cnt=0,ans=0;
	for(int i=1;i<=scnt;++i)
		if(deg[i]==0) cnt++,ans=num[i];
	if(cnt==1) printf("%d\n",ans);
	else puts("0");
	return 0;
}

367. 学校网络

链接:https://www.acwing.com/problem/content/description/369/

题意:将有向图转化为DAG后,其实就是求加几条边才能使得整个图变成一个scc

思路

  • 如果DAG只有一个强联通分量(一个点),那么不需要加边,答案为 0
  • 否则,求DAG入度为 0 的点有多少个,出度为 0 的点有多少个,两者取最大值就是答案。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=100+10,maxm=5e4+10;

vector<int> e[maxn];
int dfn[maxn],times,low[maxn];
stack<int> sta;
bool insta[maxn];
int scc[maxn],scnt,num[maxn];

int in[maxn],out[maxn];

void dfs(int u)
{
	dfn[u]=low[u]=++times;
	sta.push(u);insta[u]=1;
	for(auto v: e[u])
	{
		if(!dfn[v])
		{
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
		else if(insta[v]) low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u])
	{
		scnt++;
		while(1)
		{
			int v=sta.top();sta.pop();insta[v]=0;
			scc[v]=scnt;num[scnt]++;
			if(v==u) break;
		}
	}
}
int n,m;

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
	{
		int v;
		while(scanf("%d",&v)&&v)
			e[i].push_back(v);
	}
	for(int i=1;i<=n;++i)
		if(!dfn[i]) dfs(i);
	for(int u=1;u<=n;++u)
	{
		for(auto v: e[u])
		{
			if(scc[v]!=scc[u])
				in[scc[v]]++,out[scc[u]]++;
		}
	}
	int cnt1=0,cnt2=0;
	for(int i=1;i<=scnt;++i)
	{
		if(in[i]==0) cnt1++;
		if(out[i]==0) cnt2++;
	}
	int ans2;
	if(scnt==1) ans2=0;
	else ans2=max(cnt1,cnt2);
	printf("%d\n%d\n",cnt1,ans2);
	return 0;
}

P3387 【模板】缩点 (tarjan缩点 + DP)

链接:https://www.luogu.com.cn/problem/P3387

题意:给定一个n个点m条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

思路:缩点之后求DAG上最长链

  • 设 dp[u] = 从u出发路径的最大权值和
  • dp[u] = max{dp[v]} + sz[u], u->v有向边, sz[u] = scc权值和
  • Tarjan算法求出scc的顺序就是拓扑序倒序(拓扑序靠后的scc先出栈) 找到一个新的scc,就对其出边做DP即可。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e4+10,maxm=5e4+10;

vector<int> e[maxn];
int dfn[maxn],times,low[maxn];
stack<int> sta;
bool insta[maxn];
int scc[maxn],scnt,num[maxn];

int n,m,w[maxn],dp[maxn],ans;

void dfs(int u)
{
	dfn[u]=low[u]=++times;
	sta.push(u);insta[u]=1;
	for(auto v: e[u])
	{
		if(!dfn[v])
		{
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
		else if(insta[v]) low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u])
	{
		scnt++;
		int res=0;
		while(1)
		{
			int v=sta.top();sta.pop();insta[v]=0;
			scc[v]=scnt;num[scnt]+=w[v];
			for(auto v1: e[v])
				if(scc[v1]&&scc[v1]!=scnt) res=max(res,dp[scc[v1]]);
			if(v==u) break;
		}
		dp[scnt]=res+num[scnt];
		ans=max(ans,dp[scnt]);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%d",&w[i]);
	for(int i=1;i<=m;++i)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		e[u].push_back(v);
	}
	for(int i=1;i<=n;++i)
		if(!dfn[i]) dfs(i);
	printf("%d\n",ans);
	return 0;
}

也可以遍历所有的边之后重新建图,正向拓扑排序然后dp。这里需要给 dp 赋初值

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e4+10;

vector<int> e[maxn],e2[maxn];
int n,m,w[maxn];

int dfn[maxn],low[maxn],times;
int sta[maxn],insta[maxn],top;
int scc[maxn],scnt,num[maxn];

int dp[maxn],ans,in[maxn];

void dfs(int u)
{
	dfn[u]=low[u]=++times;
	sta[++top]=u;insta[u]=1;
	
	for(auto v: e[u])
	{
		if(!dfn[v])
		{
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
		else if(insta[v]) low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u])
	{
		++scnt;
		int res=0;
		while(1)
		{
			int v=sta[top--];insta[v]=0;
			scc[v]=scnt;num[scnt]+=w[v];
			if(v==u) break;
		}
	}
}

void topo()
{
	queue<int> q;
	for(int i=1;i<=scnt;++i)
		if(in[i]==0) q.push(i),dp[i]=num[i];
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(auto v: e2[u])
		{
			dp[v]=max(dp[v],dp[u]+num[v]);
			in[v]--;
			if(in[v]==0) q.push(v);
		}
	}
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%d",&w[i]);
	for(int i=1;i<=m;++i)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		e[u].push_back(v);
	} 
	ans=top=scnt=times=0;
	for(int i=1;i<=n;++i)
		if(!dfn[i]) dfs(i);
	for(int u=1;u<=n;++u)
		for(auto v: e[u])
			if(scc[v]!=scc[u]) 
				e2[scc[u]].push_back(scc[v]),in[scc[v]]++;
	topo();
	for(int i=1;i<=scnt;++i) ans=max(ans,dp[i]);
	printf("%d\n",ans);
	return 0;
}

P1073 最优贸易

题意:给定 n 个城市的水晶球价格,有 m 条道路,问从 1 到 n 的的路径中,买入一次和卖出一次能获取的最大利益是多少?同一个城市可以经过多次,不需要经过所有城市。

思路:缩点+ DP。

  • d p [ s c c [ u ] ] dp[scc[u]] dp[scc[u]] 表示从 s c c [ u ] scc[u] scc[u] 到达 s c c [ n ] scc[n] scc[n] 中最大的价格。
  • 用一个 f [ s c c [ u ] ] f[scc[u]] f[scc[u]] 数组来表示 s c c [ u ] scc[u] scc[u] 是否能够到达 s c c [ n ] scc[n] scc[n],如果不连通那么 d p [ s c c [ u ] ] = 0 dp[scc[u]] = 0 dp[scc[u]]=0,这样做的目的是,使得这个 scc 不能更新答案。
  • 同时枚举每个城市作为最小值。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+10;

int n,m,w[maxn],f[maxn],dp[maxn],ans=0;
vector<int> e[maxn];

int dfn[maxn],low[maxn],times=0;
int sta[maxn],insta[maxn],top=0;
int scc[maxn],scnt=0,sz[maxn];

void dfs(int u)
{
    dfn[u]=low[u]=++times;
    sta[++top]=u;
    insta[u]=1;
    for(auto v: e[u])
    {
        if(!dfn[v])
        {
            dfs(v);
            low[u]=min(low[u],low[v]);
        }
        else if(insta[v]) low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u])
    {
        scnt++;
        int minn=2e9,maxx=0;
        while(1)
        {
            int v=sta[top--];
            insta[v]=0;
            scc[v]=scnt;
            sz[scnt]+=w[v];
            if(v==n) f[scnt]=1;

            minn=min(minn,w[v]);
            dp[scnt]=max(dp[scnt],w[v]);
            for(auto v1: e[v])
            {
                if(scc[v1]&&scc[v1]!=scc[v])
                {
                    f[scc[v]]|=f[scc[v1]];
                    if(f[scc[v1]]) maxx=max(maxx,dp[scc[v1]]);
                }
            }
            if(v==u) break;
        }
        if(f[scnt]) dp[scnt]=max(dp[scnt],maxx);
        else dp[scnt]=0;//没有路到n
        ans=max(ans,dp[scnt]-minn);
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; ++i) scanf("%d",&w[i]);
    for(int i=1; i<=m; ++i)
    {
        int u,v,x;
        scanf("%d%d%d",&u,&v,&x);
        e[u].push_back(v);
        if(x==2) e[v].push_back(u);
    }
    dfs(1);
    printf("%d\n",ans);
    return 0;
}

P3275 [SCOI2011]糖果 (求最小值,解最长路)

链接:https://www.luogu.com.cn/problem/P3275

题意:给一些关于 x i x_i xi 的不等式,求 ∑ x i \sum x_i xi 的最小值

思路

  • 求最小值,需要反向建图跑最长路,出现正环时无解。将条件 x i − x j ≤ d x_i- x_j\le d xixjd 变为 x i − d ≤ x j x_i-d\le x_j xidxj,即从 i i i j j j 连一条权值为 − d -d d 的边

  • 隐含条件: x i ≥ 1 x_i \ge 1 xi1,可以设一个超级源点 s,设 d i s [ s ] = 1 dis[s]=1 dis[s]=1,然后 s s s 向每个 x i x_i xi 连一条权值为 0 0 0 的边

  • 差分约束并不是正解,这份代码会 T

  • 正解是缩点成DAG之后,正向建图并在topo 排序时 dp。设 d i s [ i ] dis[i] dis[i] 表示从 0 到 i 的最长路
    d i s [ v ] = m a x ( d i s [ v ] , d i s [ u ] + w ( u , v ) ) , ( u 为 v 的 入 边 ) dis[v]= max(dis[v],dis[u]+w(u,v)) ,(u为v的入边) dis[v]=max(dis[v],dis[u]+w(u,v)),(uv)

  • 最后统计答案时,每一个 scc 都是一个 0 环,也就是上面的每一个孩子分得的糖果是相同的。因此: a n s = ∑ i = 1 s c n t d i s [ i ] × s z [ i ] ans=\sum_{i=1}^{scnt} dis[i]\times sz[i] ans=i=1scntdis[i]×sz[i]

#include <bits/stdc++.h>
#define fi first
#define se second
#define ll long long
using namespace std;
const int maxn=1e5+10;

int n,m;
vector<pair<int,int> > e[maxn],e2[maxn];

int dfn[maxn],low[maxn],times=0;
int sta[maxn],insta[maxn],top=0;
int scc[maxn],sz[maxn],scnt=0;

void dfs(int u)
{
	dfn[u]=low[u]=++times;
	sta[++top]=u;insta[u]=1;
	for(auto x: e[u])
	{
		int v=x.fi,w=x.se;
		if(!dfn[v])
		{
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
		else if(insta[v]) low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u])
	{
		scnt++;
		while(1)
		{
			int v=sta[top--];insta[v]=0;
			scc[v]=scnt;sz[scnt]++;
			if(v==u) break;
		}
	}
}

int in[maxn],dis[maxn];

void topo()
{
	queue<int> q;
	for(int i=1;i<=scnt;++i)
		if(in[i]==0) q.push(i);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(auto x: e2[u])
		{
			int v=x.fi,w=x.se;
			dis[v]=max(dis[v],dis[u]+w);
			in[v]--;
			if(in[v]==0) q.push(v);
		}
	}
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i)
	{
		int x,u,v;
		scanf("%d%d%d",&x,&u,&v);
		if(x==1) e[u].push_back({v,0}),e[v].push_back({u,0});
		else if(x==2) e[u].push_back({v,1});
		else if(x==3) e[v].push_back({u,0});
		else if(x==4) e[v].push_back({u,1});
		else if(x==5) e[u].push_back({v,0});
	}
	for(int i=1;i<=n;++i) e[0].push_back({i,1});
	dfs(0);
	for(int i=0;i<=n;++i)
	{
		for(auto x: e[i])
		{
			int v=x.fi,w=x.se;
			if(scc[v]==scc[i]&&w==1)
			{
				puts("-1");
				return 0;
			}
			else if(scc[v]!=scc[i]) e2[scc[i]].push_back({scc[v],w}),in[scc[v]]++;
		}
	}
	topo();
	ll ans=0;
	for(int i=1;i<=scnt;++i)
		ans+=dis[i]*sz[i];
	printf("%lld\n",ans);
	return 0;
}

401. 从u到v还是从v到u?

链接:https://www.acwing.com/problem/content/description/403/

题意:给定一个 n 个点 m 条边的有向图,现在要求图中任意两点u和v,均可满足u能通往v或v能通往u,请你判断要求是否能够成立。

思路:答案为YES的充要条件是 拓扑序唯一
由于有重边,采用topo排序进行判断,看q.size()是否始终<=1即可。

  • 如果存在两种拓扑序:比如: a -> b ,b -> a 。意思是说,a、b 同时存在与队列中,即入度同时为 0 ,此时 a、b互相不可达。
#include <bits/stdc++.h>
#define fi first
#define se second
#define ll long long
using namespace std;
const int maxn=1000+10;

int t,n,m;
vector<int> e[maxn],e2[maxn];
int in[maxn];

int dfn[maxn],low[maxn],times=0;
int sta[maxn],insta[maxn],top=0;
int scc[maxn],scnt=0;

void dfs(int u)
{
	dfn[u]=low[u]=++times;
	sta[++top]=u;insta[u]=1;
	for(auto v: e[u])
	{
		if(!dfn[v])
		{
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
		else if(insta[v]) low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u])
	{
		scnt++;
		while(1)
		{
			int v=sta[top--];insta[v]=0;
			scc[v]=scnt;
			if(v==u) break;
		}
	}
}

int topo()
{
	queue<int> q;
	for(int i=1;i<=scnt;++i)
		if(in[i]==0) q.push(i);
	while(!q.empty())
	{
		if(q.size()>1) return 0; 
		int u=q.front();q.pop();
		for(auto v: e2[u])
		{
			in[v]--;
			if(in[v]==0) q.push(v);
		}
	}
	return 1;
}

int main()
{
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;++i)
		{
			dfn[i]=low[i]=in[i]=0;
			e[i].clear();
			e2[i].clear();
		}
		top=scnt=times=0;	
		for(int i=1;i<=m;++i)
		{
			int u,v;
			scanf("%d%d",&u,&v);
			e[u].push_back(v);
		}
		for(int i=1;i<=n;++i)
			if(!dfn[i]) dfs(i);
		for(int u=1;u<=n;++u)
			for(auto v: e[u])
				if(scc[v]!=scc[u]) 
					e2[scc[u]].push_back(scc[v]),in[scc[v]]++;
		puts(topo()?"Yes":"No");
	}
	return 0;
}

402. 杀人游戏

链接:https://www.acwing.com/problem/content/description/404/

题意: n 个人中存在一个杀手,询问一个人时,如果是平民,他会告诉你他知道的平民和杀手是谁,如果是杀手那么他会杀死警察。假设每个人是杀手的概率相同,问警察找到凶手的概率是多少?

思路:缩点后,找入度为 0 的点的数量。同时特判一种特殊情况,即可以直接排除的情况,其他所有点都不是杀手,可以推得最后一个人是杀手。

  • 这种情况:要么是孤立的一个点,或者是这个点的所有出边,都可以被其他的路径访问。即这个点的出边的入度大于等于2。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+10;

int n,m;
vector<int> e[maxn],e2[maxn];
int in[maxn],out[maxn];

int dfn[maxn],low[maxn],times=0;
int sta[maxn],insta[maxn],top=0;
int scc[maxn],sz[maxn],scnt=0;

void dfs(int u)
{
	dfn[u]=low[u]=++times;
	sta[++top]=u;insta[u]=1;
	for(auto v: e[u])
	{
		if(!dfn[v])
		{
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
		else if(insta[v]) low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u])
	{
		scnt++;
		while(1)
		{
			int v=sta[top--];insta[v]=0;
			scc[v]=scnt;sz[scnt]++;
			if(v==u) break;
		}
	}
}

bool check(int u)
{
	if(sz[u]!=1) return 0;
	if(out[u]==0) return 1;
	for(auto v: e2[u])
		if(in[v]<=1) return 0; 
	return 1;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		e[u].push_back(v);
	}
	for(int i=1;i<=n;++i)
		if(!dfn[i]) dfs(i);
	for(int u=1;u<=n;++u)
	{
		for(auto v: e[u])
			if(scc[v]!=scc[u])
			{
				in[scc[v]]++;out[scc[u]]++;
				e2[scc[u]].push_back(scc[v]);
			}
	}
	int cnt=0,f=0;
	for(int i=1;i<=scnt;++i)
	{
		if(in[i]==0)
		{
			cnt++;
			if(f==0&&check(i)) f=1;
		}
	}
	if(f) cnt--;
	printf("%.6lf\n",1-cnt*1.0/n);
	return 0;
}

The Bottom of a Graph HDU - 2553 (tarjan 求强联通分量 + 缩点)

题意:求出度为0的强联通分量,从小到大输出点的 id
思路:对每一个强联通分量染色。统计每个强联通分量的出度。把出度为 0 的强联通分量放入 ans 中

#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstring>
#define ll long long
using namespace std;
const int maxn=50000+5,inf=0x3f3f3f3f;

int n,m;
int head[maxn],cnt;

struct Edge
{
    int nxt,to;
}edges[maxn<<1];

void add(int u,int v)
{
    edges[++cnt].to=v;
    edges[cnt].nxt=head[u];
    head[u]=cnt;
}

int low[maxn],dfn[maxn],inStack[maxn],id;
int sta[maxn],top;
int tot,color[maxn],out[maxn];

void init()
{
	cnt=0;
	memset(head,-1,sizeof(head));

	id=0;
	memset(low,0,sizeof(low));
	memset(dfn,0,sizeof(dfn));
	memset(inStack,0,sizeof(inStack));

	tot=0,top=0;
	memset(color,0,sizeof(color));
	memset(out,0,sizeof(out));
}

void tarjan(int u)
{
    low[u]=dfn[u]=++id;
    sta[++top]=u,inStack[u]=true;
    for(int i=head[u];i!=-1;i=edges[i].nxt)
    {
        int v=edges[i].to;
        if(!dfn[v])//没有访问过
            tarjan(v),low[u]=min(low[u],low[v]);
        else if(inStack[v])//已经访问过,但是还未包含在其他联通分量中
            low[u]=min(low[u],dfn[v]);
        //已经访问过,且已经包含在其他联通分量之中,所以不再栈内
    }
    if(low[u]==dfn[u])
    {
        tot++;
        do
        {
        	color[sta[top]]=tot;
        	inStack[sta[top]]=false;
        }while(sta[top--]!=u);
    }
}

int main()
{
    while(scanf("%d",&n)&&n)
    {
    	init();
        scanf("%d",&m);
        for(int i=1;i<=m;++i)
        {
            int u,v;
            scanf("%d%d",&u,&v);
            add(u,v);
        }
        for(int i=1;i<=n;++i)
            if(!dfn[i])
                tarjan(i);

        for(int u=1;u<=n;++u)
        {
            for(int i=head[u];i!=-1;i=edges[i].nxt)
            {
                int v=edges[i].to;
                if(color[u]!=color[v])
                    out[color[u]]++;
            }
        }
        vector<int> ans;
        for(int i=1;i<=tot;++i)
        {
            if(out[i]>0) continue;
            for(int j=1;j<=n;++j)
                if(color[j]==i)
                    ans.push_back(j);
        }
        sort(ans.begin(),ans.end());
        for(int i=0;i<ans.size();++i)
            printf("%d%c",ans[i],i==ans.size()-1?'\n':' ');
    }
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值