【笔记】强连通分量
一、强连通分量
强连通:有向图 G 强连通是指 G 中任意两个节点相通。
强连通分量(Strongly Connected Componets, SCC),是指一个有向图中的强连通子图。
用处:可以把 SCC 看作一个点,构造新图解决问题。
二、Tarjan
Tarjan 是一种用来求强连通分量的算法。
我们 DFS 一个有向图,维护两个数组和一个栈:
\(dfn_u\):\(u\) 节点被搜索的次序。
\(low_u\):\(u\) 节点能回溯到最早进栈的节点。设 \(u\) 的子树为 \(subtree_u\),那么 \(low_u\) 为 \(subtree_u\) 中的节点和 \(subtree_u\) 连到遍历过的节点的 \(dfn\) 最小值。
我们把每次遍历的节点 \(u\) 入栈,\(dfn_u\) 比上一个遍历的节点大 \(1\),然后遍历 \(subtree_u\),寻找 \(low_u\) 的最小值,\(v\) 为 \(u\) 相邻的节点(不是父亲节点),有三种情况:
- \(v\) 在栈中,拿 \(dfn_v\) 与 \(low_u\) 作比较,取最小值。
- \(v\) 没遍历过,遍历 \(v\),拿 \(low_v\) 与 \(low_u\) 作比较,取最小值。
- \(v\) 遍历过且已经出栈,证明不属于 \(u\) 所在的强连通分量,不进行操作。
当一个节点的 \(dfn=low\) 时,说明这个节点没有被其他节点的 \(dfn\) 影响,所以 \(dfn\) 和 \(low\) 都最小,那么这个节点必定是强连通分量中最开始遍历的那个节点。此时把栈中这个节点以上的所有节点都弹出,因为它们构成了一个强连通分量。
代码(取自 OI wiki):
int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc; // 结点 i 所在 SCC 的编号
int sz[N]; // 强连通 i 的大小
void tarjan(int u) {
low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
for (int i = h[u]; i; i = e[i].nex) {
const int &v = e[i].t;
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (in_stack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
++sc;
while (s[tp] != u) {
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
}
三、例题
Luogu P2863 [USACO06JAN] The Cow Prom S
简单题。Tarjan 求出每个 SCC 的节点个数,遍历统计大于 \(1\) 的 SCC 并输出即可。
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=1e4+10;
const int maxm=5e4+10;
struct Node{
int from,to,nxt;
}e[maxm];
int n,m,cnt;
int tot,h[maxn];
int low[maxn],dfn[maxn],sz[maxn],sc,st[maxn],top,dfncnt;
bool stvis[maxn];
void Add(int u,int v){
tot++;
e[tot].from=u;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
void tarjan(int u){
low[u]=dfn[u]=++dfncnt;
st[++top]=u;
stvis[u]=1;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(stvis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
sc++;
while(st[top]!=u){
sz[sc]++;
stvis[st[top]]=0;
top--;
}
sz[sc]++;
stvis[st[top]]=0;
top--;
}
}
int main(){
scanf("%d%d",&n,&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 i=1;i<=sc;i++) if(sz[i]>1) cnt++;
cout<<cnt;
return 0;
}
Luogu P3387【模板】缩点
解决这个问题我们可以把一个 SCC 缩成一个点,因为如果路径经过了这个 SCC 的任意一点那么就可以经过其他的点,而且不用担心重复经过一点/边。
使用 Tarjan 求出所有的 SCC,把所有 SCC 看成一个点,然后重新构造一个图,每一个点的点权都是这个 SCC 的所有节点的点权和。在原图中寻找连接两个 SCC 的边,然后再新图中连接。连接的时候计算每个新点的入度,方便跑拓扑排序。此题因为数据问题不需要判断重边。连完之后对新图进行拓扑排序,把所有入度为 \(0\) 的节点入栈然后往下遍历,每次遍历都把入度 \(-1\)(相当于忽视了已经遍历过的父节点,类似于递归),继续找入度为 \(0\) 的节点遍历。在拓扑排序的过程中动态规划,设与每次遍历的节点 \(u\) 相连的节点为 \(v\),新图的点权为 \(a\),那么 \(dp_v=\max(dp_v,dp_u+a_v)\)。最后找到最大的结果输出。
代码还是挺麻烦的,写了整整 \(100\) 行。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=1e4+10;
const int maxm=1e5+10;
struct Node{
int from,to,nxt;
}e[maxm],e2[maxm];
int n,m,ans;
int a[maxn];
int tot,h[maxn],h2[maxn];
int low[maxn],dfn[maxn],st[maxn],top,scc[maxn],dfncnt,sc;
int ind[maxn],dp[maxn],a2[maxn];
bool stvis[maxn];
void Add(int u,int v){
tot++;
e[tot].from=u;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
void Add2(int u,int v){
tot++;
e2[tot].from=u;
e2[tot].to=v;
e2[tot].nxt=h2[u];
ind[v]++;
h2[u]=tot;
}
void tarjan(int u){//求 SCC。
low[u]=dfn[u]=++dfncnt;
st[++top]=u;
stvis[u]=1;
for(int i=h[u];i!=-1;i=e[i].nxt){
int v=e[i].to;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(stvis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
++sc;
while(st[top]!=u){
scc[st[top]]=sc;
stvis[st[top]]=0;
a2[sc]+=a[st[top]];//缩点,集中点权。
top--;
}
scc[st[top]]=sc;
a2[sc]+=a[st[top]];
stvis[st[top]]=0;
top--;
}
}
int main(){
memset(h,-1,sizeof(h));
memset(h2,-1,sizeof(h2));
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
//Add(0,i);
}
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);//注意非连通图的情况。
tot=0;
for(int i=1;i<=m;i++){
int x=scc[e[i].from],y=scc[e[i].to];//构造新图。
if(x!=y) Add2(x,y);
}
top=0;
for(int i=1;i<=sc;i++){
if(!ind[i]){
st[++top]=i;
dp[i]=a2[i];
}
}
while(top){//拓扑排序。
int u=st[top--];
for(int i=h2[u];i!=-1;i=e2[i].nxt){
int v=e2[i].to;
dp[v]=max(dp[v],dp[u]+a2[v]);
ind[v]--;
if(!ind[v]){
st[++top]=v;
}
}
}
for(int i=1;i<=sc;i++){
ans=max(ans,dp[i]);
}
cout<<ans;
return 0;
}
时间复杂度:Tarjan 为 \(O(n+m)\),构造新图为 \(O(m)\),拓扑近似 \(O(n)\),总时间复杂度 \(O(n+m)\)。
Luogu P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G
在一个 SCC 里的牛是互相喜欢的。我们求出所有的 SCC 并缩点构造新图,新图中如果只有一个出度为 \(0\) 的节点那么这个 SCC 里的所有牛都是明星。如果不止一个出度为 \(0\) 的节点那么就都构不成明星关系,输出 \(0\)。如果新图只有一个节点那么输出这个节点里的牛数。
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=1e4+10;
const int maxm=5e4+10;
struct Node{
int from,to,nxt;
}e[maxm],e2[maxm];
int n,m,ans,anss;
int tot,h[maxn],h2[maxn],outd[maxn];
int low[maxn],dfn[maxn],st[maxn],scc[maxn],sz[maxn],sc,dfncnt,top;
bool stvis[maxn];
void Add(int u,int v){
tot++;
e[tot].from=u;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
void Add2(int u,int v){
tot++;
e2[tot].from=u;
e2[tot].to=v;
e2[tot].nxt=h[u];
outd[u]++;
h[u]=tot;
}
void tarjan(int u){
low[u]=dfn[u]=++dfncnt;
st[++top]=u;
stvis[u]=1;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(stvis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
sc++;
while(st[top]!=u){
scc[st[top]]=sc;
sz[sc]++;
stvis[st[top]]=0;
top--;
}
scc[st[top]]=sc;
sz[sc]++;
stvis[st[top]]=0;
top--;
}
}
int main(){
scanf("%d%d",&n,&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);
tot=0;
for(int i=1;i<=m;i++){//构造新图。
int x=scc[e[i].from],y=scc[e[i].to];
if(x!=y) Add2(x,y);
}
for(int i=1;i<=sc;i++){//求出度。
if(!outd[i]){
ans++;
anss=i;
}
}
if(ans==1) cout<<sz[anss];
else cout<<0;
return 0;
}
Luogu P2746 [USACO5.3] 校园网Network of Schools
子任务 A,Tarjan 求 SCC 并缩点之后输出入度为 \(0\) 的 SCC 的节点数,因为这些 SCC 中的学校无法接收到软件。
子任务 B,如果要把这些 SCC 合并成一个 SCC,它们必须再组成一个环。这时候把所有出度为 \(0\) 的节点和入度为 \(0\) 的节点相连,剩下的入度为 \(0\) 或出度为 \(0\) 的节点连在其他的节点上就能构成一个 SCC 了。设入度为 \(0\) 的节点数量为 \(in\),出度为 \(0\) 的节点数量为 \(out\),那么最少所需要增加的边数量为 \(\max(in,out)\)。注意在新图只有一个 SCC 的情况下答案为 \(0\)。
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=110;
const int maxm=1e4+10;
struct Node{
int from,to,nxt;
}e[maxm],e2[maxm];
int n,m,cnt1,cnt2;
int tot,h[maxn],h2[maxn],ind[maxn],outd[maxn];
int low[maxn],dfn[maxn],scc[maxn],sz[maxn],sc,st[maxn],top,dfncnt;
bool stvis[maxn];
void Add(int u,int v){
tot++;
e[tot].from=u;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
void Add2(int u,int v){
tot++;
e2[tot].from=u;
e2[tot].to=v;
e2[tot].nxt=h2[u];
ind[v]++;
outd[u]++;
h2[u]=tot;
}
void tarjan(int u){
low[u]=dfn[u]=++dfncnt;
st[++top]=u;
stvis[u]=1;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(stvis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
sc++;
while(st[top]!=u){
scc[st[top]]=sc;
sz[sc]++;
stvis[st[top]]=0;
top--;
}
scc[st[top]]=sc;
sz[sc]++;
stvis[st[top]]=0;
top--;
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int v;
while(1){
scanf("%d",&v);
if(v==0) break;
else{
Add(i,v);
m++;
}
}
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
tot=0;
for(int i=1;i<=m;i++){
int x=scc[e[i].from],y=scc[e[i].to];
if(x!=y){
Add2(x,y);
}
}
for(int i=1;i<=sc;i++){//统计入度为0和出度为0的节点。
if(!ind[i]) cnt1++;
if(!outd[i]) cnt2++;
}
cout<<cnt1<<endl;
if(sc==1) cout<<0;
else cout<<max(cnt1,cnt2);
return 0;
}
Luogu P1262 间谍网络
第一问判断是否能控制所有间谍,也就是所有间谍是否与受贿间谍连通,可以从所有受贿间谍进行 Tarjan,最终没被遍历到的,即没有 \(dfn\) 的间谍就是不会被控制的间谍。Tarjan 完之后从小到大遍历到第一个没有 \(dfn\) 的间谍就输出 NO 和这个间谍的编号。否则就输出 YES,进行第二问。
第二问我们把所有 SCC 缩成点并建新图,新图的每个点的点权就是原图中每个 SCC 中受贿间谍钱数的最小值。然后输出新图所有入度为 \(0\) 的节点的点权和即可。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=3e3+10;
const int maxr=8e3+10;
struct Node{
int from,to,nxt;
}e[maxr];
int n,r,p,ans;
int tot,h[maxn],a[maxn],ind[maxn],mon[maxn];
int low[maxn],dfn[maxn],st[maxn],scc[maxn],sz[maxn],sc,top,dfncnt;
bool stvis[maxn];
void Add(int u,int v){
tot++;
e[tot].from=u;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
void tarjan(int u){
low[u]=dfn[u]=++dfncnt;
st[++top]=u;
stvis[u]=1;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(stvis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(low[u]==dfn[u]){
sc++;
while(st[top]!=u){
scc[st[top]]=sc;
sz[sc]++;
stvis[st[top]]=0;
mon[sc]=min(a[st[top]],mon[sc]);
top--;
}
scc[st[top]]=sc;
sz[sc]++;
stvis[st[top]]=0;
mon[sc]=min(a[st[top]],mon[sc]);
top--;
}
}
int main(){
memset(a,0x3f,sizeof(a));
memset(mon,0x3f,sizeof(mon));
scanf("%d%d",&n,&p);
for(int i=1;i<=p;i++){
int b,c;
scanf("%d%d",&b,&c);
a[b]=c;
}
scanf("%d",&r);
for(int i=1;i<=r;i++){
int u,v;
scanf("%d%d",&u,&v);
Add(u,v);
}
for(int i=1;i<=n;i++) if(!dfn[i]&&a[i]!=0x3f3f3f3f) tarjan(i);
for(int i=1;i<=n;i++){
if(!dfn[i]){
cout<<"NO"<<endl<<i;
return 0;
}
}
for(int i=1;i<=r;i++){
int x=scc[e[i].from],y=scc[e[i].to];
if(x!=y) ind[y]++;
}
for(int i=1;i<=sc;i++){
if(!ind[i]){
ans+=mon[i];
}
}
cout<<"YES"<<endl<<ans;
return 0;
}
Luogu P3627 [APIO2009] 抢掠计划
这个题与【模板】缩点类似,但是有一个地方需要多加注意,就是在拓扑排序的时候不能只把起点 \(s\) 入栈,而是要把所有入度为零的节点入栈,但是只在 dp 中记录 \(s\) 所在 SCC 的点权,其余赋值为极小值以防影响 dp 的结果。这么做的原因是,与起点相连的节点不一定入度为 \(1\),而遍历所有入度为 \(0\) 的节点就可以把整个图遍历完,不会导致没有节点入度为 \(0\) 导致直接结束。
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=5e5+10;
const int INF=2147483647;
struct Node{
int from,to,nxt;
}e[maxn],e2[maxn];
int n,m,s,p,ans;
int h[maxn],h2[maxn],tot;
int a[maxn],a2[maxn],ind[maxn],dp[maxn];
int low[maxn],dfn[maxn],dfncnt,st[maxn],top,scc[maxn],sc,sz;
bool bar[maxn],stvis[maxn];
void Add(int u,int v){
tot++;
e[tot].from=u;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
void Add2(int u,int v){
tot++;
e2[tot].from=u;
e2[tot].to=v;
e2[tot].nxt=h2[u];
ind[v]++;
h2[u]=tot;
}
void Tarjan(int u){
low[u]=dfn[u]=++dfncnt;
st[++top]=u;
stvis[u]=1;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!dfn[v]){
Tarjan(v);
low[u]=min(low[u],low[v]);
}else if(stvis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(low[u]==dfn[u]){
sc++;
while(st[top]!=u){
scc[st[top]]=sc;
a2[sc]+=a[st[top]];
stvis[st[top]]=0;
top--;
}
scc[st[top]]=sc;
a2[sc]+=a[st[top]];
stvis[st[top]]=0;
top--;
}
}
int main(){
scanf("%d%d",&n,&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++){
scanf("%d",&a[i]);
}
scanf("%d%d",&s,&p);
for(int i=1;i<=p;i++){
int x;
scanf("%d",&x);
bar[x]=1;
}
for(int i=1;i<=n;i++){
if(!dfn[i]) Tarjan(i);
}
tot=0;
for(int i=1;i<=m;i++){
int x=scc[e[i].from],y=scc[e[i].to];
if(x!=y) Add2(x,y);
}
top=0;
for(int i=1;i<=sc;i++){
if(!ind[i]){
st[++top]=i;
}
}
for(int i=1;i<=sc;i++) dp[i]=-INF;
dp[scc[s]]=a2[scc[s]];
while(top){
int u=st[top--];
for(int i=h2[u];i;i=e2[i].nxt){
int v=e2[i].to;
dp[v]=max(dp[v],dp[u]+a2[v]);
ind[v]--;
if(!ind[v]) st[++top]=v;
}
}
for(int i=1;i<=n;i++){
if(bar[i]){
ans=max(ans,dp[scc[i]]);
}
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号