强连通分量
一、强连通分量
1.DFS森林和强连通分
(1)DFS Forest
- Tree Edge指树边
- Back Edge指连向祖先的边(返祖边)
- Forward Edge指连向子孙的边(前向边,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。)
- Cross Edge指连向树其他分支的边(横插边,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。)
- 在无向图只存在Tree Edge和Back Edge
(2)强连通分量SCC(Strongly Connected Component)
强连通:u存在到达v的路径,v存在到达u的路径,我们称u、v是强连通的(如图中绿框里面的)
推论:\(\begin{cases} u,v强连通\\ v,w强连通\end{cases} \tag{1}\) ==> \(u,w\)强连通
2.SCC的Tarjan和Kosaraju算法
(1)Tarjan 算法求强连通分量
我们考虑把它切成一块一块的强连通分类,能切就切
- 不能连上去
- 不能连向前面未切的点
对于每个结点\(u\)我们需要维护:
-
\(dfn\):\(dfs\)遍历时候结点u被搜到的次序
-
\(low\):在\(u\)的子树中能回溯到的最早的已经在栈中的结点。设以\(u\)为根的子树为\(subtree_u\)。\(low_u\) 定义为以下结点的\(dfn\)的最小值:\(subtree_u\)中的结点;从\(subtree_u\)通过一条不在搜索树上的边能到达的结点。
一个结点的子树内结点的 \(dfn\) 都大于该结点的 \(dfn\)。
每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。
在搜索过程中,对于结点\(u\)和与其相邻的结点(\(v\)不是\(u\)的父节点)考虑 3 种情况:
- \(v\)未被访问:继续对\(v\)进行深度搜索。在回溯过程中,用\(low_v\)更新\(low_u\)。因为存在从\(u\)到 \(v\)的直接路径,所以\(v\)能够回溯到的已经在栈中的结点,\(u\)也一定能够回溯到。
- \(v\)被访问过,已经在栈中:根据 low 值的定义,用\(dfn_v\)更新\(low_v\)。
- \(v\)被访问过,已不在栈中:说明\(v\)已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 101000;
vector<int>edge[N];
int n,m;
int dfn[N],low[N],ins[N],idx,bel[N],cnt;
// low记这个子树里面能跳到的dfn最小的,且未被切掉的(ins[u] = true)
stack<int>stk;
vector<vector<int>>scc;
void dfs(int u)
{
dfn[u] = low[u] = ++idx;
ins[u] = true;
stk.push(u);//还没被切掉的点
for(auto v:edge[u])
{
// if(!dfn[v]){
// dfs(v);
// low[u] = min(low[u],low[v]);
// }
// else{
// if(ins[v])low[u] = min(low[u],dfn[v]);
// }
if(!dfn[v])dfs(v);
if(ins[v])low[u] = min(low[u],low[v]);
}
if(dfn[u] == low[u]){
vector<int>c;
++cnt;
while(1)
{
int v = stk.top();
c.push_back(v);
ins[v] = false;
bel[v] = cnt;
stk.pop();
if(u==v)break;
}
sort(c.begin(), c.end());
scc.push_back(c);
}
}
int main()
{
cin>>n>>m;
for(int i = 1;i<=m;i++)
{
int u,v;
cin>>u>>v;
edge[u].push_back(v);
}
for(int i = 1;i<=n;i++)
{
if(!dfn[i])dfs(i);
}
sort(scc.begin(),scc.end());
for(auto c:scc)
{
for(auto u:c)
{
cout<<u<<" ";
}
cout<<"\n";
}
}
(2)Kosaraju 算法
依靠2次DFS实现:
先DFS一遍:在回溯时候给出点编号,得到出栈顺序(即后序遍历)。
第二次DFS:对反图dfs,这样遍历到的所有点的集合就是一个强连通分量。
❗重要的几个结论
- DAG(有向无环图)出栈顺序是反图的拓扑序
- 有向图 SCC缩点(不考虑强连通分量内部的连边,只考虑强连通分量与强连通分量之间的连边)之后就是一个DAG
- 最后一个出栈的一定是源点(入度为0的点)
- Tarjan的SCC的点的编号是反向的拓扑序
反着搜:6 2 3 9 7 4 14 12 13 10 15 11 5 1 0 8
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 101000;
vector<int>edge[N],erev[N];
vector<int>c,out;
int n,m;
bool vis[N];
vector<vector<int>>scc;
void dfs(int u)
{
vis[u] = true;
for(auto v:edge[u])
{
if(!vis[v])dfs(v);
}
out.push_back(u);
}
void dfs2(int u)
{
vis[u] = true;
for(auto v:erev[u])
{
if(!vis[v])dfs2(v);
}
c.push_back(u);
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i = 1;i<=m;i++)
{
int u,v;
cin>>u>>v;
edge[u].push_back(v);
erev[v].push_back(u);
}
for(int i = 1;i<=n;i++)
{
if(!vis[i])dfs(i);
}
reverse(out.begin(),out.end());
memset(vis,false,sizeof(vis));
for(auto u: out)
{
if(!vis[u])
{
c.clear();
dfs2(u);
sort(c.begin(),c.end());
scc.push_back(c);
}
}
sort(scc.begin(),scc.end());
for(auto c:scc)
{
for(auto u:c)
{
cout<<u<<" ";
}
cout<<endl;
}
}
(3)例题:HAOI2006, 受欢迎的牛
看有多少点,所有点都可达
- 对于DAG来说,唯一汇点是所有点都可达
- 对于一般图来说,它SCC缩点之后是一个DAG,那么它要所有点都可达它,它必须是缩完点之后的唯一汇点。之后再去看SCC里面有多少个点。
//有多少个点,所有点都可达
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 101000;
vector<int>edge[N];
int n,m;
int dfn[N],low[N],ins[N],idx,bel[N],cnt,sz[N];//sz:每个强连通分量的size
// low记这个子树里面能跳到的dfn最小的,且未被切掉的(ins[u] = true)
int outd[N];//每个强连通分量它的出度
stack<int>stk;
void dfs(int u)
{
dfn[u] = low[u] = ++idx;
ins[u] = true;
stk.push(u);//还没被切掉的点
for(auto v:edge[u])
{
if(!dfn[v])dfs(v);
if(ins[v])low[u] = min(low[u],low[v]);
}
if(dfn[u] == low[u]){
++cnt;
while(1)
{
int v = stk.top();
ins[v] = false;
bel[v] = cnt;
sz[cnt]++;
stk.pop();
if(u==v)break;
}
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i = 1;i<=m;i++)
{
int u,v;
cin>>u>>v;
edge[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 : edge[u])
{
if(bel[u] != bel[v])
outd[bel[u]]++;
}
}
int cnts = 0,cntv = 0;
for(int i = 1;i<=cnt;i++)
{
if(outd[i]==0)
{
cnts++;
cntv += sz[i];
}
}
if(cnts>=2)//有两个汇点,这两个汇点之间是不可达的
cout<<0<<"\n";
else
cout<<cntv<<"\n";
}
3.SCC缩点和DP
例题1:ZJOI2007, 最大半连通子图
思路:
对于DAG来说,我们找最大半连通子图就是找一条最长路。
那么对于一般图,我们考虑缩点。缩点之后转化为带权的求最长路问题。
步骤:
- SCC缩点
- DP
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 101000;
vector<int>edge[N];
int n,m,cnt,mod;;
int dfn[N],low[N],ins[N],idx,bel[N];
// low记这个子树里面能跳到的dfn最小的,且未被切掉的(ins[u] = true)
stack<int>stk;
vector<int>vec[N];
int dp[N],way[N];
bool vis[N];
void dfs(int u)
{
dfn[u] = low[u] = ++idx;
ins[u] = true;
stk.push(u);//还没被切掉的点
for(auto v:edge[u])
{
if(!dfn[v])dfs(v);
if(ins[v])low[u] = min(low[u],low[v]);
}
if(dfn[u] == low[u]){
++cnt;
while(1)
{
int v = stk.top();
vec[cnt].push_back(v);//记录每个强连通分量有哪些点
ins[v] = false;
bel[v] = cnt;
stk.pop();
if(u==v)break;
}
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>mod;
for(int i = 1;i<=m;i++)
{
int u,v;
cin>>u>>v;
edge[u].push_back(v);
}
for(int i = 1;i<=n;i++)
if(!dfn[i])dfs(i);
int ans = 0,w = 0;
for(int i = 1;i<=cnt;i++)
{
way[i] = 1;
dp[i] = 0;
for(int u : vec[i])
{
for(int v :edge[u])
{
if(!vis[bel[v]]&&bel[v]!=i) //注意缩完点之后连边注意重边不要重复计算
{
vis[bel[v]] = true;
if(dp[bel[v]]>dp[i])
dp[i] = dp[bel[v]],way[i] = 0;
if(dp[bel[v]]==dp[i])
way[i] = (way[i]+way[bel[v]])%mod;
}
}
}
dp[i] += vec[i].size();
if(dp[i]>ans)
ans = dp[i],w = 0;
if(dp[i]==ans)
w = (w+way[i])%mod;
for(int u : vec[i])
for(int v :edge[u])
vis[bel[v]] = false;
}
cout<<ans<<"\n";
cout<<w<<"\n";
}
写法2:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 101000;
vector<int>edge[N];
int n,m,mod;
int cnt,idx;
int dfn[N],low[N],ins[N],bel[N];
int dp[N],vis[N],way[N];
int ans,w,T;
stack<int>stk;
void dfs(int u)
{
dfn[u] = low[u] = ++idx;
ins[u] = true;
stk.push(u);
for(auto v:edge[u])
{
if(!dfn[v])dfs(v);
if(ins[v])low[u] = min(low[u],low[v]);
}
if(dfn[u]==low[u])
{
++cnt;
int sz = 0;
dp[cnt] = 0;
way[cnt] = 1;
++T;
vis[cnt] = T;
while(1)
{
int v = stk.top();
stk.pop();
ins[v] = false;
bel[v] = cnt;
sz++;
for(auto w:edge[v])
{
if(vis[bel[w]]!=T&&bel[w]!=0)
{
vis[bel[w]] = T;
if(dp[bel[w]]>dp[cnt])
dp[cnt] = dp[bel[w]],way[cnt] = way[bel[w]];
else if(dp[bel[w]]==dp[cnt])
way[cnt] = (way[cnt]+way[bel[w]])%mod;
}
}
if(u==v)break;
}
dp[cnt] += sz;
if(dp[cnt]>ans)
ans = dp[cnt],w = way[cnt];
else if(dp[cnt]==ans)
w = (w+way[cnt])%mod;
}
}
void tarjan()
{
for(int i = 1;i<=n;i++)
if(!dfn[i])dfs(i);
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>mod;
for(int i = 1;i<=m;i++)
{
int u,v;
cin>>u>>v;
edge[u].push_back(v);
}
tarjan();
cout<<ans<<"\n";
cout<<w<<"\n";
return 0;
}
例题2:APIO2009, ATM
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 501000;
vector<int>edge[N];
int n,m;
int dfn[N],low[N],ins[N],idx,bel[N],cnt;
// low记这个子树里面能跳到的dfn最小的,且未被切掉的(ins[u] = true)
stack<int>stk;
int val[N],bar[N];
ll dp[N];
void dfs(int u)
{
dfn[u] = low[u] = ++idx;
ins[u] = true;
stk.push(u);//还没被切掉的点
for(auto v:edge[u])
{
if(!dfn[v])dfs(v);
if(ins[v])low[u] = min(low[u],low[v]);
}
if(dfn[u] == low[u]){
++cnt;
ll sval = 0;
bool sbar = false;
dp[cnt] = -(1ll<<60);
while(1)
{
int v = stk.top();
stk.pop();
ins[v] = false;
bel[v] = cnt;
sval += val[v];
sbar |= bar[v];
for(auto w:edge[v])
{
if(bel[w]!= cnt && bel[w] != 0)
{
dp[cnt] = max(dp[cnt],dp[bel[w]]);
}
}
if(u==v)break;
}
if(sbar)dp[cnt] = max(dp[cnt],0ll);
dp[cnt] += sval;
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i = 1;i<=m;i++)
{
int u,v;
cin>>u>>v;
edge[u].push_back(v);
}
for(int i = 1;i<=n;i++)
cin>>val[i];
int s,p;
cin>>s>>p;
for(int i = 1;i<=p;i++)
{
int x;
cin>>x;
bar[x] = 1;
}
dfs(s);
cout<<dp[bel[s]]<<"\n";
}
例题3:SDOI2010, 所驼门王的宝藏
思路:首先思考一下怎么去建图?
一共有\(N^2\)个点,如果所有的可达信息都建出来,这复杂度太高了。
那怎么办呢?考虑建立辅助结点。
比如有一个横向的传送门,它可以到这一行所有的点。那么我们考虑先建立一个辅助点,这个辅助点向这一行所有点连边,再把这个有横向传送门的点连向辅助点。这样就可以实现,该点先到辅助点,辅助点再到所有点。
注意点:
- 统计size的时候注意不要把辅助点加进去了。
- 记录编号数组稍微开大一点(至少开两倍),因为我们还要记录辅助点。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 301000;
vector<int>edge[N];
map<int,vector<int>>r,c;
map<int,int>rid,cid;
map<pair<int,int>,int>id;
int dfn[N],low[N],ins[N],idx,bel[N],cnt;
stack<int>stk;
int n,R,C,dp[N],ans,x[N],y[N],ty[N];
int tot;
void dfs(int u)
{
dfn[u] = low[u] = ++idx;
ins[u] = true;
stk.push(u);//还没被切掉的点
for(auto v:edge[u])
{
if(!dfn[v])dfs(v);
if(ins[v])low[u] = min(low[u],low[v]);
}
if(dfn[u] == low[u]){
++cnt;
int sz = 0;
dp[cnt] = 0;
while(1)
{
int v = stk.top();
ins[v] = false;
bel[v] = cnt;
sz+=(v<=n);//注意不要把辅助点加进去了!
stk.pop();
for(auto w:edge[v])
{
if(bel[w]!=cnt&&bel[w]!=0)
{
dp[cnt] = max(dp[cnt],dp[bel[w]]);
}
}
if(u==v)break;
}
dp[cnt] += sz;
ans = max(ans,dp[cnt]);
}
}
void tarjan()
{
for(int i = 1;i<=tot;i++)
{
if(!dfn[i])dfs(i);
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>R>>C;
for(int i = 1;i<=n;i++)
{
cin>>x[i]>>y[i]>>ty[i];
id[{x[i],y[i]}] = i;
r[x[i]].push_back(i);
c[y[i]].push_back(i);
}
tot = n;
for(int i = 1;i<=n;i++)
{
if(ty[i]==1)
{
if(!rid.count(x[i]))
{
rid[x[i]] = ++tot;
for(auto id:r[x[i]])edge[tot].push_back(id);
}
edge[i].push_back(rid[x[i]]);
}
else if(ty[i]==2)
{
if(!cid.count(y[i]))
{
cid[y[i]] = ++tot;
for(auto id:c[y[i]])edge[tot].push_back(id);
}
edge[i].push_back(cid[y[i]]);
}
else if(ty[i]==3)
{
for(int dx = -1;dx<=1;dx++)
{
for(int dy = -1;dy<=1;dy++)
{
if(dx==0&&dy==0)continue;
if(!id.count({x[i]+dx,y[i]+dy}))continue;
edge[i].push_back(id[{x[i]+dx,y[i]+dy}]);
}
}
}
}
tarjan();
cout<<ans<<"\n";
}
标签:cnt,连通,int,图论,ins,dfn,low,dp,分量
From: https://www.cnblogs.com/nannandbk/p/17585080.html