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 xi−xj≤d 变为 x i − d ≤ x j x_i-d\le x_j xi−d≤xj,即从 i i i 到 j j j 连一条权值为 − d -d −d 的边
-
隐含条件: x i ≥ 1 x_i \ge 1 xi≥1,可以设一个超级源点 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)),(u为v的入边) -
最后统计答案时,每一个 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;
}