强连通分量
1|0有向图的强连通分量
1|1基本概念
1|0连通分量:
对于分量内任意两点u和v , 必然可以找到从 u 走到 v 且可以从 v 走到 u.
1|0强连通分量:
极大连通分量(包含点数最多)
强连通分量常用于缩点
1|2Tarjan算法:
基于 DFS :
1|0Tarjan算法几个重要概念:
在已经DFS的树中:
- 后前边: (x, y) x是y的一个祖先, 但存在一条由y->x的边.
- 横插边: (x, y) x和y不属于同一条分支, 但存在一条y->x的边
- 前向边: (x, y) y是x的祖先, 存在一条x->y的边
1|0几个数组和变量:
- 时间戳: 记录搜索到每个点的时间.即对每个点根据搜索顺序进行标号排序.
- dfn数组: 记录每个点的时间戳.(同时具有判重数组作用)
- low数组: 记录每个点向上走所能达到的最高点(即时间戳最小点).
- stk栈: 记录当前强连通分量内的点.
- id数组: 记录每个点所在的连通分量.
- scc_cnt: 强连通分量个数
1|0模板:
int timecnt; //时间戳
int dfn[N]; //每个点的时间戳
int low[N]; //low[u] : u所在的子树中所有点中所能向上走到的时间戳最小的点
int scc_cnt; //强连通分量的数量
int id[N]; //id[i] : 表示i号点所在的强连通分量的编号
stack<int> stk; //存储当前强连通分量里的所有点
int in_stk[N]; //记录该点是否在栈中
void tarjan(int u)
{
low[u] = dfn[u] = ++ timecnt;
stk.push(u);
in_stk[u] = 1;
for (int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if (!dfn[j]){ //j点没有被遍历过,j点一定是在子树中
tarjan(j); //遍历j
low[u] = min(low[u], low[j]); //遍历过后的j的low可能已经找到一个更高的结点,所以要去更新u
}
else if (in_stk[j]) //j在栈中,则j和u之间一定是一条横叉边或向前边,即j的时间戳一定比u小
low[u] = min(low[u], dfn[j]);
}
if (low[u] == dfn[u]){ //到此处,u的所有边已经遍历完,如果low[u] = dfn[u] : 得到了一个强连通分量
scc_cnt ++;
int y; //此时该强连通分量里的点全在栈中,全部取出
do{
y = stk.top();
stk.pop();
id[y] = scc_cnt;
sizes[scc_cnt] ++;
}while (y != u);
}
}
2|0例题
2|1AcWing 1174. 受欢迎的牛
1|0算法思路 :
强连通分量的典型应用:缩点. 将整个图缩点后,得到一张拓扑图, 求出强连通分量后, 寻找出度为0的节点, 该节点内的牛的数量为答案(注意出度为0的点只能有一个,否则结果为0)
#include <iostream>
#include <cstring>
#include <queue>
#include <stack>
using namespace std;
const int N = 1e5 + 10, M = 5e4 + 10;
int h[N], e[M], ne[M], idx;
int timecnt; //时间戳
int dfn[N]; //每个点的时间戳
int low[N]; //low[u] : u所在的子树中所有点中所能向上走到的时间戳最小的点
int scc_cnt; //强连通分量的数量
int id[N]; //id[i] : 表示i号点所在的强连通分量的编号
stack<int> stk; //存储当前强连通分量里的所有点
int in_stk[N]; //记录该点是否在栈中
int sizes[N]; //强连通分量的结点数量
int dout[N]; //每个强连通分量的出度
int n, m;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void tarjan(int u)
{
low[u] = dfn[u] = ++ timecnt;
stk.push(u);
in_stk[u] = 1;
for (int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if (!dfn[j]){ //j点没有被遍历过,j点一定是在子树中
tarjan(j); //遍历j
low[u] = min(low[u], low[j]); //遍历过后的j的low可能已经找到一个更高的结点,所以要去更新u
}
else if (in_stk[j]) //j在栈中,则j和u之间一定是一条横叉边或向前边,即j的时间戳一定比u小
low[u] = min(low[u], dfn[j]);
}
if (low[u] == dfn[u]){ //到此处,u的所有边已经遍历完,如果low[u] = dfn[u] : 得到了一个强连通分量
scc_cnt ++;
int y; //此时该强连通分量里的点全在栈中,全部取出
do{
y = stk.top();
stk.pop();
id[y] = scc_cnt;
sizes[scc_cnt] ++;
}while (y != u);
}
}
int main()
{
cin >> n >> m;
memset (h, -1, sizeof h);
for (int i = 1; i <= m; i ++ ){
int a, b;
cin >> a >> b;
add(a, b);
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
for (int i = 1; i <= n; i ++ )
for (int j = h[i]; j != -1; j = ne[j]){
int k = e[j];
int a = id[i], b = id[k];
if (a != b){ //二者不在同一个强连通分量内,则由i -> k的边是缩点后点一条边,对于连通分量: a -> b
dout[a] ++;
}
}
int cnt = 0;
int sum = 0;
for (int i = 1; i <= scc_cnt; i ++ )
if (!dout[i]){
cnt ++;
sum = sizes[i];
if (cnt > 1){
sum = 0;
break;
}
}
cout << sum << endl;
return 0;
}
2|2AcWing 367. 学校网络
1|0算法思路:
强连通分量缩点, 将原图转化为拓扑图, 第一问求入度为0的点的个数, 第二问结论: max(cntin,cntout)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
const int N = 110, M = N * N / 2;
int h[N], e[M], ne[M], idx;
int timecnt;
int low[N], dfn[N];
stack<int> stk;
int scc_cnt;
int id[N];
int in_stk[N];
int dout[N];
int din[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++timecnt;
stk.push(u);
in_stk[u] = 1;
for (int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if (!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}else if (in_stk[j])
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u]){
scc_cnt ++;
int y;
do{
y = stk.top();
stk.pop();
in_stk[y] = 0;
id[y] = scc_cnt;
}while (y != u);
}
}
int main()
{
int n;
cin >> n;
memset (h, -1, sizeof h);
for (int i = 1; i <= n; i ++ ){
int b;
while (cin >> b && b){
add(i, b);
}
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
for (int i = 1; i <= n; i ++ )
for (int j = h[i]; j != -1; j = ne[j]){
int k = e[j];
int a = id[i], b = id[k];
if (a != b){
dout[a] ++;
din[b] ++;
}
}
int in_cnt = 0;
int out_cnt = 0;
for (int i = 1; i <= scc_cnt; i ++ ){
if (!dout[i]){
out_cnt ++;
}
if (!din[i]){
in_cnt ++;
}
}
if (scc_cnt == 1)
cout << 1 << endl << 0 << endl;
else
cout << in_cnt << endl << max(in_cnt, out_cnt) << endl;
return 0;
}
2|3AcWing 1175. 最大半连通子图
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_set>
#include <stack>
using namespace std;
const int N = 1e5 + 10, M = 2 * 1e6 + 10;
typedef long long LL;
int h[N], hs[N], e[M], ne[M], idx;
int timecnt;
int dfn[N], low[N];
stack<int> stk;
int scc_cnt;
int in_stk[N];
int sizes[N];
int id[N];
int f[N], g[N];
unordered_set<LL> used;
int n, m, mod;
void add(int h[], int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++timecnt;
stk.push(u);
in_stk[u] = 1;
for (int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if (!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}else if (in_stk[j])
low[u] = min(low[u], dfn[j]);
}
if (low[u] == dfn[u]){
scc_cnt ++;
int y;
do{
y = stk.top();
stk.pop();
in_stk[y] = 0;
id[y] = scc_cnt;
sizes[scc_cnt] ++;
}while (y != u);
}
}
int main()
{
cin >> n >> m >> mod;
memset (h, -1, sizeof h);
memset (hs, -1, sizeof hs);
for (int i = 1; i <= m; i ++ ){
int a, b;
scanf("%d%d",&a, &b);
add(h, a, b);
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
for (int i = 1; i <= n; i ++ )
for (int j = h[i]; j != -1; j = ne[j]){
int k = e[j];
int a = id[i], b = id[k];
if (a != b && !used.count((LL)a * 1e6 + b)){
add(hs, a, b);
used.insert((LL)a * 1e6 + b);
}
}
for (int i = scc_cnt; i ; i -- ){
if (!f[i]){
f[i] = sizes[i];
g[i] = 1;
}
for (int j = hs[i]; j != -1; j = ne[j]){
int k = e[j];
if (f[k] < f[i] + sizes[k]){
f[k] = f[i] + sizes[k];
g[k] = g[i];
}else if (f[k] == f[i] + sizes[k])
g[k] = (g[k] + g[i]) % mod;
}
}
int maxn = 0;
int sum = 0;
for (int i = 1; i <= scc_cnt; i ++ )
if (f[i] > maxn){
maxn = f[i];
sum = g[i];
}else if (f[i] == maxn)
sum = (g[i] + sum) % mod;
cout << maxn << endl << sum << endl;
return 0;
}
2|4AcWing 368. 银河
1|0算法思路:
强连通分量求解差分约束问题:
由强连通分量进行缩点, 求缩点后的拓扑图, 对拓扑图求最长路
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
const int N = 1e5 + 10, M = 5e5 + 10;
int h[N], e[M], w[M], idx, ne[M];
int hs[N]; //缩点后的表头
int low[N], dfn[N], timecnt;
int in_stk[N];
int scc_cnt;
int id[N];
int sizes[N];
stack<int> stk;
int dist[N];
int n, m;
void add(int a, int b, int v)
{
e[idx] = b, ne[idx] = h[a], w[idx] = v, h[a] = idx ++;
}
void add1(int a, int b, int v)
{
e[idx] = b, w[idx] = v, ne[idx] = hs[a], hs[a] = idx ++;
}
void tarjan(int u)
{
low[u] = dfn[u] = ++timecnt;
stk.push(u);
in_stk[u] = 1;
for (int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if (!dfn[j]){
tarjan(j);
low[u] = min(low[j], low[u]);
}else if (in_stk[j])
low[u] = min(low[u], dfn[j]);
}
if (low[u] == dfn[u]){
scc_cnt ++;
int y;
do{
y = stk.top();
stk.pop();
in_stk[y] = 0;
id[y] = scc_cnt;
sizes[scc_cnt] ++;
}while (y != u);
}
}
int main()
{
cin >> n >> m;
memset (h, -1, sizeof h);
memset (hs, -1, sizeof hs);
for (int i = 1; i <= m; i ++ ){
int a, b, t;
scanf("%d%d%d",&t, &a, &b);
if (t == 1)
add(a, b, 0), add(b, a, 0);
if (t == 2)
add(a, b, 1);
if (t == 3)
add(b, a, 0);
if (t == 4)
add(b, a, 1);
if (t == 5)
add(a, b, 0);
}
for (int i = 1; i <= n; i ++ )
add(0, i, 1);
tarjan(0);
int flag = 1;
for (int i = 0; i <= n; i ++ )
for (int j = h[i]; j != -1; j = ne[j]){
int k = e[j];
int a = id[i], b = id[k];
if (a == b){
if (w[j] > 0){
flag = 0;
}
}else add1(a, b, w[j]);
}
if (flag == 0)
puts("-1");
else{
for (int i = scc_cnt; i > 0; i -- ){
for (int j = hs[i]; j != -1; j = ne[j]){
int k = e[j];
dist[k] = max(dist[i] + w[j], dist[k]);
}
}
long long res = 0;
for (int i = 1; i <= scc_cnt; i ++ )
res += dist[i] * sizes[i];
cout << res << endl;
}
return 0;
}
__EOF__

本文作者:lhqwd
本文链接:https://www.cnblogs.com/lhqwd/p/14552950.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
本文链接:https://www.cnblogs.com/lhqwd/p/14552950.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗