$一.强连通分量$
Tarjan(缩点)将一个带环的图转化成一个有向无环图(DAG)
将一个DAG变成一个强连通分量最少需要加$min(cnt_{起点}, cnt_{终点})$条边($n=1$时特判为$0$)
模板
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 10010, M = 100010;
int n, m, w[N], Time;
int dfn[N], low[N];
int stack[N], top;
int col[N], cnt, s[N];
int d[N], q[N], f[N];
int ue[M], une[M], uh[N], uidx;
int e[M], ne[M], h[N], idx;
void uadd(int a, int b)
{
ue[uidx] = b, une[uidx] = uh[a], uh[a] = uidx ++;
}
void Tarjan(int u)
{
dfn[u] = low[u] = ++ Time; // 第一次搜索到
stack[++ top] = u; // 先让它入栈
for (int i = uh[u]; ~i; i = une[i])
{
int j = ue[i];
if (!dfn[j]) // 如果没有被搜过,那就继续深搜
{
Tarjan(j);
low[u] = min(low[u], low[j]); // 回溯的时候更新一下父节点的low值
}
else if (!col[j]) low[u] = min(low[u], low[j]);
// 如果到达的元素还在栈中,那么还可以更新(有可能这个元素就是这个强联通分量的代表元素)
}
if (dfn[u] == low[u])
{
col[u] = ++ cnt; // cnt是缩点以后的标号
s[cnt] += w[u]; // 缩点后的点权是这个强连通分量里所有点的点权
while (stack[top] != u) col[stack[top]] = cnt, s[cnt] += w[stack[top --]];
top --; // 最后记得把代表元素也删去
}
}
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
int main()
{
scanf ("%d %d", &n, &m);
for (int i = 1; i <= n; i ++) scanf ("%d", &w[i]);
memset(uh, -1, sizeof uh);
while (m --)
{
int a, b;
scanf ("%d %d", &a, &b);
uadd(a, b);
}
for (int i = 1; i <= n; i ++)
if (!dfn[i]) Tarjan(i);
memset(h, -1, sizeof h);
for (int u = 1; u <= n; u ++)
for (int i = uh[u]; ~i; i = une[i])
{
// 这里一定是建col[u] 和 col[j]的边,把图上点的标号都改成缩点后的标号
int j = ue[i];
if (col[u] != col[j]) add(col[u], col[j]), d[col[j]] ++;
}
// 因为你现在不知道哪个是起始元素(1吗?可万一 1 在一个强联通分量里,但不是这个强连通分量的代表元素呢?)
// 所以用拓扑排序再好不过了
int hh = 0, tt = -1;
for (int i = 1; i <= cnt; i ++)
if (!d[i]) q[++ tt] = i, f[i] = s[i];
while (hh <= tt)
{
int u = q[hh ++];
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
d[j] --;
f[j] = max(f[j], f[u] + s[j]);
if (!d[j])
q[++ tt] = j;
}
}
// 在任何一个点上结束后都可能是最大值
int res = 0;
for (int i = 1; i <= cnt; i ++) res = max(res, f[i]);
printf ("%d", res);
return 0;
}
例题
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110, M = N * N;
int n;
int e[M], ne[M], h[N], idx;
int dfn[N], low[N], timestamp;
int id[N], scc_cnt, stk[N], top;
bool in_stk[N];
int din[N], dout[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] = ++ timestamp;
stk[++ top] = u, in_stk[u] = true;
for (int i = h[u]; ~i; 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])
{
int x;
scc_cnt ++;
do {
x = stk[top --];
id[x] = scc_cnt;
in_stk[x] = false;
} while (x != u);
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++)
{
int a;
while (cin >> a, a) add(i, a);
}
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; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
if (a != b) din[b] ++, dout[a] ++;
}
int a = 0, b = 0;
for (int i = 1; i <= scc_cnt; i ++)
a += !din[i], b += !dout[i];
cout << a << endl << (scc_cnt == 1 ? 0 : max(a, b));
return 0;
}
$二.边双连通分量$
Tarjan(缩点)将一个带环无向图->树
加边将一棵树变成边双连通分量, 最小要加$ \lceil \frac{cnt_{叶子结点}}{2} \rceil$($n=1$时特判为$0$)
$三.点双连通分量$
判断一个点是不是割点: 若为根节点, 需要有两个不同的分支能回头; 若不为根节点, 只需有一个分支能回头
求割点: 电力
点双连通分量缩点: 矿场搭建