题目地址:
https://www.acwing.com/problem/content/1177/
一个有向图 G = ( V , E ) G=(V,E) G=(V,E)称为半连通的(Semi-Connected),如果满足: ∀ u , v ∈ V ∀u,v∈V ∀u,v∈V,满足 u → v u→v u→v或 v → u v→u v→u,即对于图中任意两点 u , v u,v u,v,存在一条 u u u到 v v v的有向路径或者从 v v v到 u u u的有向路径。若 G ′ = ( V ′ , E ′ ) G'=(V',E') G′=(V′,E′)满足, E ′ E' E′是 E E E中所有和 V ′ V' V′有关的边,则称 G ′ G′ G′是 G G G的一个导出子图。若 G ′ G′ G′是 G G G的导出子图,且 G ′ G' G′半连通,则称 G ′ G' G′为 G G G的半连通子图。若 G ′ G' G′是 G G G所有半连通子图中包含节点数最多的,则称 G ′ G' G′是 G G G的最大半连通子图。给定一个有向图 G G G,请求出 G G G的最大半连通子图拥有的节点数 K K K,以及不同的最大半连通子图的数目 C C C。由于 C C C可能比较大,仅要求输出 C C C对 X X X的余数。
输入格式:
第一行包含三个整数
N
,
M
,
X
N,M,X
N,M,X。
N
,
M
N,M
N,M分别表示图
G
G
G的点数与边数,
X
X
X的意义如上文所述;接下来
M
M
M行,每行两个正整数
a
,
b
a,b
a,b,表示一条有向边
(
a
,
b
)
(a,b)
(a,b)。图中的每个点将编号为
1
1
1到
N
N
N,保证输入中同一个
(
a
,
b
)
(a,b)
(a,b)不会出现两次。
输出格式:
应包含两行。第一行包含一个整数
K
K
K,第二行包含整数
C
m
o
d
X
C \mod X
CmodX。
数据范围:
1
≤
N
≤
1
0
5
1≤N≤10^5
1≤N≤105
1
≤
M
≤
1
0
6
1≤M≤10^6
1≤M≤106
1
≤
X
≤
1
0
8
1≤X≤10^8
1≤X≤108
注意这里的半连通子图的定义,这里的定义是取一些点,和与这些点关联的所有边。所以不同的半连通子图只会在点上有区别,而不会在边上有区别。对于拓扑图来说,最大的半连通子图实际上就是某个从入度为 0 0 0的点出发的最长链(最长链一定走到了某个出度为 0 0 0的点)。所以容易想到先求强连通分量,然后缩点化为拓扑图来做。化为拓扑图之后,按拓扑序递推即可。这里还需要注意,在缩点之后的新图里,是不能有平行边的(严格来说可以有,但是在递推的时候需要注意判重,所以索性就每条边建一次就好),这样递推的时候就不会因为平行边而累加多次。设 f [ i ] f[i] f[i]是新图里走到 i i i的最长链的点总数(这里的“最长链”并不代表边数最多,而是代表总点数最多,因为缩点之后每个点实际上是由多个点捏成一个得到的),那么 f [ j ] = s [ j ] + max i → j f [ i ] f[j]=s[j]+\max_{i\to j} f[i] f[j]=s[j]+i→jmaxf[i]即以点 j j j结尾的最长链应当是其前驱为节点的最长链加上 j j j自己的点数。代码如下:
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 1e5 + 10, M = 2e6 + 10;
int n, m, mod;
int h[N], hs[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, sz[N];
// f[i]存的是以i结尾的最长链总点数,g[i]存的是以i结尾的最长链的条数
int f[N], g[N];
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] = ++timestamp;
stk[top++] = u, in_stk[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (in_stk[v]) low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
scc_cnt++;
int y;
do {
y = stk[--top];
in_stk[y] = false;
id[y] = scc_cnt;
sz[scc_cnt]++;
} while (y != u);
}
}
int main() {
memset(h, -1, sizeof h);
memset(hs, -1, sizeof hs);
scanf("%d%d%d", &n, &m, &mod);
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
add(h, a, b);
}
for (int i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i);
// 建新图。开一个哈希表对边判重
unordered_set<long> S;
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];
long hash = a * 1e6 + b;
if (a != b && !S.count(hash)) {
add(hs, a, b);
S.insert(hash);
}
}
// tarjan算法结束后,所有的强连通分量实际上是按照编号逆序成拓扑序的,
// 所以逆序遍历所有的强连通分量就可以做递推了。
for (int i = scc_cnt; i; i--) {
if (!f[i]) {
f[i] = sz[i];
g[i] = 1;
}
for (int j = hs[i]; ~j; j = ne[j]) {
int k = e[j];
if (f[k] < f[i] + sz[k]) {
f[k] = f[i] + sz[k];
g[k] = g[i];
} else if (f[k] == f[i] + sz[k]) g[k] = (g[k] + g[i]) % mod;
}
}
int maxf = 0, sum = 0;
for (int i = 1; i <= scc_cnt; i++)
if (f[i] > maxf) {
maxf = f[i];
sum = g[i];
} else if (f[i] == maxf) sum = (sum + g[i]) % mod;
printf("%d\n%d\n", maxf, sum);
return 0;
}
时空复杂度 O ( V + E ) O(V+E) O(V+E)。