在学习tarjan算法之前,我们先来了解一下强连通分量的概念
强连通分量的概念
若在有向图中,任意两个顶点,Vi和Vj能够互相到达的话,那么这些顶点构成的图就是强连通分量,换句话说,在强连通分量内的任意两个顶点之间可以相互到达
tarjan算法缩点的应用
用于将有向有环图变为有向无环图
可以找到图中的强连通分量的个数,以及强联通分量内的元素,以及每个强联通分量的入度和出度等等
板子
void tarjan(int v)//v表示当前节点
{
dfn[v]=low[v]=op;
op++;
s.push(v);
vis[v]=1;
for(int u:e[v])
{
if(dfn[u]==0)
{
tarjan(u);
low[v]=min(low[v],low[u]);
}
else if(dfn[u]!=0&&vis[u]!=0)
{
low[v]=min(low[v],low[u]);
}
}
int u;
if(dfn[v]==low[v])
{
cnt++;
do{
u=s.top();
s.pop();
vis[u]=0;
scc[u]=cnt;//这个地方可以变得,这个只是为了统计每个点属于哪个强连通分量
}while(u!=v);
}
}
例题
P1726 上白泽慧音
思路:题目中要求的就是去求出所有的强连通分量,按照强连通分量的含有数量的多少进行排序,如果相同就按照第一个小的进行排序即可,tarjan板题
#include<bits/stdc++.h>
using namespace std;
#define long long
int dfn[5005];
int low[5005];
int op=1;//表示dfn序
stack<int> s;
int vis[5005];//表示当前点是否在栈中
int n,m;
int u,v,flag;
vector<int> e[5005];
int sz[5005];
int cnt;//scc编号
vector<int> scc[5005];
void tarjan(int v)//v表示当前节点
{
dfn[v]=low[v]=op;
op++;
s.push(v);
vis[v]=1;
for(int u:e[v])
{
if(dfn[u]==0)
{
tarjan(u);
low[v]=min(low[v],low[u]);
}
else if(dfn[u]!=0&&vis[u]!=0)
{
low[v]=min(low[v],low[u]);
}
}
int u;
if(dfn[v]==low[v])
{
cnt++;
do{
u=s.top();
s.pop();
vis[u]=0;
scc[cnt].push_back(u);
sz[cnt]++;
}while(u!=v);
}
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>u>>v>>flag;
if(flag==1)
{
e[u].push_back(v);
}
else
{
e[u].push_back(v);
e[v].push_back(u);
}
}
for (int i = 1; i <= n; i++) {
if (dfn[i] == 0) {
tarjan(i);
}
}
int maxn=0;
for(int i=1;i<=cnt;i++)
{
maxn=max(maxn,sz[i]);
sort(scc[i].begin(),scc[i].end());
}
cout<<maxn<<"\n";
int z=0;
int biao=0x3f3f3f3f;
for(int i=1;i<=cnt;i++)
{
if(sz[i]==maxn)
{
if(biao>scc[i][0])
{
biao=scc[i][0];
z=i;
}
}
}
for(int u:scc[z])
{
cout<<u<<" ";
}
return 0;
}
B3609 [图论与代数结构 701] 强连通分量
思路:我们将所有强联通分量求出来,然后按照包含i的强连通分量输出即可,输出过就不用再输出了
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int u,v;
int len;
int dfn[10005];
int low[10005];
stack<int> s;
int vis[10005];
vector<int> e[10005];
int cnt=0;
int record[10005];
int b[10005];
int sz[10005];
vector<int> scc[10005];
void tarjan(int v)
{
dfn[v]=low[v]=++len;
s.push(v);
vis[v]=1;
for(int u:e[v])
{
if(dfn[u]==0)
{
tarjan(u);
low[v]=min(low[v],low[u]);
}
else if(vis[u])
{
low[v]=min(low[v],dfn[u]);
}
}
int u;
if(dfn[v]==low[v])
{
cnt++;
do{
u=s.top();
s.pop();
vis[u]=0;
record[u]=cnt;
sz[cnt]++;
scc[cnt].push_back(u);
}while(u!=v);
}
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>u>>v;
e[u].push_back(v);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)
tarjan(i);
}
cout<<cnt<<"\n";
for(int i=1;i<=cnt;i++)
{
sort(scc[i].begin(),scc[i].end());
}
for(int i=1;i<=n;i++)
{
if(b[i]==0)
{
for(int u:scc[record[i]])
{
b[u]=1;
cout<<u<<" ";
}
cout<<"\n";
}
}
return 0;
}
P3387 【模板】缩点
思路: 首先我们看到题目中所说,任何一条边可以走无数多次,但是权值只能计算一次,因此一个强连通分量内的所有权值是一定可以跑完的,因此我们可以将强连通分量缩成一个点,然后权值累加和,由tarjan算法的过程分析可知,先被弹出栈的,也一定是强连通分量内的的末端值,因此我们从1~cnt去跑dp就可以找到最大的权值累加和了
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, m;
int a[10005];
int u, v;
vector<int> e[10005];
int dfn[10005];
int low[10005];
int len;
stack<int> s;
int vis[10005];
int cnt;
int scc[10005];
int ans[10005];
vector<int> g[10005];
int f[10005];
void tarjan(int v) {
dfn[v] = low[v] = ++len;
s.push(v);
vis[v] = 1;
for(int u : e[v]) {
if(dfn[u] == 0) {
tarjan(u);
low[v] = min(low[v], low[u]);
} else if(vis[u] == 1) {
low[v] = min(low[v], dfn[u]);
}
}
int u;
if(dfn[v] == low[v]) {
cnt++;
do {
u = s.top();
s.pop();
vis[u] = 0;
scc[u] = cnt;
ans[cnt] += a[u];
} while(u != v);
}
}
signed main() {
cin >> n >> m;
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
for(int i = 1; i <= m; i++) {
cin >> u >> v;
e[u].push_back(v);
}
for(int i = 1; i <= n; i++) {
if(dfn[i] == 0) {
tarjan(i);
}
}
for(int i = 1; i <= n; i++) {
for(int u : e[i]) {
if(scc[u] != scc[i]) {
g[scc[i]].push_back(scc[u]);
}
}
}
for(int i = 1; i <= cnt; i++) {
f[i]=ans[i];
for(int u : g[i]) {
f[i] = max(f[i], f[u] + ans[i]);
}
}
int maxn = 0;
for(int i = 1; i <= cnt; i++) {
maxn = max(maxn, f[i]);
}
cout << maxn;
return 0;
}
P2746 [USACO5.3] 校园网Network of Schools
下面的是这个题的数据加强版,tarjan代码都一样,双倍经验,嘿嘿!!!
P2812 校园网络【[USACO]Network of Schools加强版】
思路:我们发现当我们拿到强连通分量的一台机子的时候,那么整个强连通分量内的机子都可以用上这个软件,那么因此我们的第一个问题就是要去统计强连通分量的数目
我们来思考第二个问题,最少添加几条线路,我们假设,整个图只有一个强连通分量的时候,那么这个强连通分量就是极大连通分量,不需要添加任何一条线路都可以,因此是0
但是不为一条线路的时候,因为在强连通分量内,任何点的入度和出度都不为0,我们会发现我们要去找的就是入度或出度为0的最大值,那么我们去统计每个强连通分量的入度和出度,当入度为0,num1++,出度为0,num2++
最后选择出来max(num1,num2)即为最终结果
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
int x;
vector<int> e[500005];
int len;
stack<int> s;
int dfn[500005];
int low[500005];
int vis[500005];
int scc[500005];
int cnt;
int ru[500005];
int chu[500005];
void tarjan(int v)
{
dfn[v]=low[v]=++len;
s.push(v);
vis[v]=1;
for(int u:e[v])
{
if(dfn[u]==0)
{
tarjan(u);
low[v]=min(low[v],low[u]);
}
else if(vis[u]==1)
{
low[v]=min(low[v],dfn[u]);
}
}
int u;
if(low[v]==dfn[v])
{
cnt++;
do{
u=s.top();
s.pop();
vis[u]=0;
scc[u]=cnt;
}while(v!=u);
}
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
while(cin>>x)
{
if(x==0)
{
break;
}
else
{
e[i].push_back(x);
}
}
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)
{
tarjan(i);
}
}
for(int i=1;i<=n;i++)
{
for(int u:e[i])
{
if(scc[u]!=scc[i])
{
ru[scc[u]]++;
chu[scc[i]]++;
}
}
}
int num1=0,num2=0;
for(int i=1;i<=cnt;i++)
{
if(ru[i]==0)
num1++;
if(chu[i]==0)
num2++;
}
cout<<num1<<"\n";
if(cnt==1)
cout<<0;
else
cout<<max(num1,num2);
return 0;
}
P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G
思路:我们可以发现,强连通分量内的所有点都会爱上强连通分量下一个链接的点,因此我们可以将其缩点,将所有强连通分量去缩点,然后去找出度为0的点,如果只有一个,那么这个强联通分量内的点就是受欢迎的牛,如果有多个,那就说明没有受欢迎的牛
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int u,v;
vector<int> e[10005];
int dfn[10005];
int low[10005];
int len=0;
stack<int> s;
int vis[10005];
int scc[10005];
int cnt=0;
int ru[10005];
int chu[10005];
int sz[10005];
void tarjan(int v)
{
dfn[v]=low[v]=++len;
s.push(v);
vis[v]=1;
for(int u:e[v])
{
if(dfn[u]==0)
{
tarjan(u);
low[v]=min(low[v],low[u]);
}
else if(vis[u]==1)
{
low[v]=min(low[v],dfn[u]);
}
}
int u;
if(dfn[v]==low[v])
{
cnt++;
do{
u=s.top();
s.pop();
vis[u]=0;
scc[u]=cnt;
sz[cnt]++;
}while(v!=u);
}
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>u>>v;
e[u].push_back(v);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)
{
tarjan(i);
}
}
for(int i=1;i<=n;i++)
{
for(int u:e[i])
{
if(scc[u]!=scc[i])
{
ru[scc[u]]++;
chu[scc[i]]++;
}
}
}
int flag=0;
for(int i=1;i<=cnt;i++)
{
if(chu[i]==0&&flag==0)
{
flag=i;
}
else if(chu[i]==0&&flag!=0)
{
cout<<"0\n";
return 0;
}
}
cout<<sz[flag]<<"\n";
return 0;
}
P2863 [USACO06JAN] The Cow Prom S
思路:我们用一个sz数组去统计每个强连通分量内的点的个数即可,然后最后遍历一遍,去寻找个数大于1的强连通分量
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int u,v;
vector<int> e[10005];
int dfn[10005];
int low[10005];
int len;
stack<int> s;
int vis[10005];
int cnt=0;
int sz[10005];
void tarjan(int v)
{
dfn[v]=low[v]=++len;
s.push(v);
vis[v]=1;
for(int u:e[v])
{
if(dfn[u]==0)
{
tarjan(u);
low[v]=min(low[v],low[u]);
}
else if(vis[u]==1)
{
low[v]=min(low[v],dfn[u]);
}
}
int u;
if(dfn[v]==low[v])
{
cnt++;
do{
u=s.top();
s.pop();
vis[u]=0;
sz[cnt]++;
}while(v!=u);
}
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>u>>v;
e[u].push_back(v);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)
{
tarjan(i);
}
}
int ans=0;
for(int i=1;i<=cnt;i++)
{
if(sz[i]>1)
{
ans++;
}
}
cout<<ans;
return 0;
}
P1262 间谍网络
思路:我们对题目分析可知,掌握了一个强连通分量内的一个间谍就相当于掌握了整个强连通分量内的所有间谍,但是什么时候要输出no呢,就是说入度为0的强连通分量,且没有人可以贿赂,那就是NO,我们可以将所有能贿赂的间谍跑一遍tarjan,然后遍历一遍1~n,若有人的dfn序是0,就说明是最小的不能掌握的人,输出NO,否则去找到入度为0的强连通分量内的最小贿赂费用总和
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,p;
int u,v;
vector<int> e[3005];
int flag[3005];
int sum[3005];
int dfn[3005];
int low[3005];
stack<int>s;
int vis[3005];
int len;
int scc[3005];
int cnt;
int assum[3005];//统计每个强联通分量内的最小可贿赂的花销
int ru[3005];
int chu[3005];
void tarjan(int v)
{
dfn[v]=low[v]=++len;
s.push(v);
vis[v]=1;
for(int u:e[v])
{
if(dfn[u]==0)
{
tarjan(u);
low[v]=min(low[v],low[u]);
}
else if(vis[u]==1)
{
low[v]=min(low[v],dfn[u]);
}
}
int u;
if(dfn[v]==low[v])
{
cnt++;
do{
u=s.top();
s.pop();
vis[u]=0;
scc[u]=cnt;
if(flag[u]==1)
{
assum[cnt]=min(assum[cnt],sum[u]);
}
}while(u!=v);
}
}
signed main()
{
memset(assum,50000,sizeof(assum));
cin>>n;
cin>>p;
for(int i=1;i<=p;i++)
{
cin>>u>>v;
flag[u]=1;
sum[u]=v;
}
cin>>m;
for(int i=1;i<=m;i++)
{
cin>>u>>v;
e[u].push_back(v);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0&&flag[i]==1)
{
tarjan(i);
}
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)
{
cout<<"NO\n";
cout<<i<<"\n";
return 0;
}
}
for(int i=1;i<=n;i++)
{
for(int u:e[i])
{
if(scc[u]!=scc[i])
{
ru[scc[u]]++;
chu[scc[i]]++;
}
}
}
int flag=1;
int minn=0x3f3f3f3f;
int ans=0;
cout<<"YES\n";
for(int i=1;i<=cnt;i++)
{
if(ru[i]==0)
{
ans+=assum[i];
}
}
cout<<ans<<'\n';
return 0;
}
总结
我们要谨记,tarjan缩点只是为了将有向有环图变成有向无环图
缩点之后去处理我们想要的东西