题解:AcWing 1175 最大半连通子图
【题目来源】
AcWing:1175. 最大半连通子图 - AcWing题库
【题目描述】
一个有向图 \(G=(V,E)\) 称为半连通的(Semi-Connected),如果满足:\(∀u,v∈V\),满足 \(u→v\) 或 \(v→u\),即对于图中任意两点 \(u,v\),存在一条 \(u\) 到 \(v\) 的有向路径或者从 \(v\) 到 \(u\) 的有向路径。
若 \(G'=(V',E')\) 满足,\(E'\) 是 \(E\) 中所有和 \(V′\) 有关的边,则称 \(G'\) 是 \(G\) 的一个导出子图。
若 \(G'\) 是 \(G\) 的导出子图,且 \(G'\) 半连通,则称 \(G'\) 为 \(G\) 的半连通子图。
若 \(G'\) 是 \(G\) 所有半连通子图中包含节点数最多的,则称 \(G'\) 是 \(G\) 的最大半连通子图。
给定一个有向图 \(G\),请求出 \(G\) 的最大半连通子图拥有的节点数 \(K\),以及不同的最大半连通子图的数目 \(C\)。
由于 \(C\) 可能比较大,仅要求输出 \(C\) 对 \(X\) 的余数。
【输入】
第一行包含三个整数 \(N,M,X\)。\(N,M\) 分别表示图 \(G\) 的点数与边数,\(X\) 的意义如上文所述;
接下来 \(M\) 行,每行两个正整数 \(a,b\),表示一条有向边 \((a,b)\)。
图中的每个点将编号为 \(1\) 到 \(N\),保证输入中同一个 \((a,b)\) 不会出现两次。
【输出】
应包含两行。
第一行包含一个整数 \(K\),第二行包含整数 \(C\ mod\ X\)。
【输入样例】
6 6 20070603
1 2
2 1
1 3
2 4
5 6
6 4
【输出样例】
3
3
【核心思想】
-
问题分析:给定有向图 \(G=(V,E)\),需要找到最大的半连通子图。半连通图的定义是:对于任意两点 \(u,v\),存在 \(u \to v\) 或 \(v \to u\) 的有向路径。关键在于理解强连通分量(SCC)内部的任意两点互相可达,天然满足半连通性;而SCC之间需要形成链式结构(即缩点后的DAG是一条链)才能保持半连通性。
-
算法选择:
- Tarjan算法:用于求强连通分量,时间复杂度 \(O(N+M)\)
- 缩点(Condensation):将每个SCC缩成一个点,构建DAG
- DAG上动态规划:在缩点后的DAG上求最长链(按节点数加权)及方案数
-
关键步骤:
-
Step 1 - Tarjan求SCC:
- 使用DFS遍历图,维护 \(dfn[x]\)(时间戳)和 \(low[x]\)(能追溯到的最早栈中节点)
- 当 \(dfn[x] = low[x]\) 时,找到SCC的根,弹出栈中节点构成一个SCC
- 记录每个SCC的编号 \(scc[i]\) 和大小 \(siz[cnt]\)
-
Step 2 - 缩点建DAG:
- 遍历原图所有边 \((u,v)\),若 \(scc[u] \neq scc[v]\),则在DAG中添加边
- 使用哈希集合去重,避免重边影响DP
-
Step 3 - DAG上DP求最长链:
- 状态定义:\(f[i]\) 表示以SCC \(i\) 结尾的最大半连通子图节点数;\(g[i]\) 表示达到 \(f[i]\) 的方案数
- 初始化:对于入度为0的SCC,\(f[i] = siz[i]\),\(g[i] = 1\)
- 状态转移:对于边 \(i \to j\),\(f[j] = \max(f[j], f[i] + siz[j])\)
- 方案数更新:若 \(f[j]\) 更新,则 \(g[j] = g[i]\);若相等,则 \(g[j] = (g[j] + g[i]) \mod X\)
- 按SCC编号逆序处理(拓扑序)
-
Step 4 - 统计答案:
- 遍历所有SCC,找到最大的 \(f[i]\) 作为 \(K\)
- 累加所有达到 \(K\) 的 \(g[i]\),对 \(X\) 取模得到 \(C\)
-
-
时间/空间复杂度:
- 时间复杂度:\(O(N + M)\),Tarjan算法 \(O(N+M)\),缩点和DP均为 \(O(N+M)\)
- 空间复杂度:\(O(N + M)\),存储原图、DAG、以及各种辅助数组
-
强连通分量与缩点DP的核心思想:
- SCC的半连通性:强连通分量内部任意两点互相可达,天然满足半连通条件
- 缩点转化:将复杂的有向图转化为DAG,把问题转化为求DAG上的最长链
- 半连通子图的结构:缩点后的半连通子图必须形成一条链(无分支),因为DAG中若存在分支则无法保证分支间的可达性
- DAG上最长链:按拓扑序进行DP,利用DAG的无环性质保证状态转移的正确性
- 方案数统计:注意去重边,避免重复计算方案;使用逆序处理确保拓扑序
- 适用于有向图压缩、可达性分析、复杂图论问题的简化
【算法标签】
强连通分量
【代码详解】
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// ================= 常量与全局变量 =================
const int N = 100005;
int n, m, mod; // n: 点数, m: 边数, mod: 取模值
vector<int> e[N]; // 原图邻接表
vector<int> e2[N]; // 缩点后的 DAG 邻接表
// ---------- Tarjan 相关 ----------
int dfn[N]; // 时间戳:节点首次被访问的顺序
int low[N]; // 能追溯到的最早栈中节点时间戳
int tot; // 时间戳计数器
int stk[N]; // 手写栈,存储当前 DFS 路径上的节点
int instk[N]; // 标记节点是否在栈中
int top; // 栈顶指针
// ---------- 强连通分量(SCC)相关 ----------
int scc[N]; // 每个节点所属的强连通分量编号
int cnt; // 强连通分量的总数
int siz[N]; // 每个强连通分量包含的节点数
// ---------- DAG 上 DP 相关 ----------
int f[N]; // f[i]: 从某个入度为0的点到达SCC i的最大节点数
int g[N]; // g[i]: 达到最大节点数f[i]的方案数
// ================= Tarjan 算法 =================
void tarjan(int x)
{
dfn[x] = low[x] = ++tot; // 初始化时间戳
stk[++top] = x; // 当前节点入栈
instk[x] = 1; // 标记为在栈中
// 遍历 x 的所有出边
for (int y : e[x])
{
if (!dfn[y]) // y 尚未访问
{
tarjan(y); // 递归处理 y
low[x] = min(low[x], low[y]); // 用子树更新 low
}
else if (instk[y]) // y 仍在栈中(发现返祖边)
{
low[x] = min(low[x], dfn[y]); // 用回边更新 low
}
}
// 若 x 是强连通分量的根节点
if (dfn[x] == low[x])
{
int y;
++cnt; // 新建一个强连通分量
do
{
y = stk[top--]; // 弹出栈顶节点
instk[y] = 0; // 取消栈中标记
scc[y] = cnt; // 将节点归属到当前 SCC
siz[cnt]++; // 当前 SCC 节点数加1
}
while (y != x); // 直到弹出 x 为止
}
}
// ================= 主函数 =================
int main()
{
// 读取点数、边数和取模值
cin >> n >> m >> mod;
// 读入 m 条有向边
while (m--)
{
int u, v;
cin >> u >> v;
e[u].push_back(v);
}
// 对每个未访问的节点执行 Tarjan 算法
for (int i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i);
// ========= 缩点建 DAG(去重边) =========
unordered_set<ll> S; // 哈希集合,用于判断边是否重复
for (int x = 1; x <= n; x++)
{
for (int y : e[x])
{
int a = scc[x]; // x 所属 SCC
int b = scc[y]; // y 所属 SCC
ll hash = 1LL * a * 1000000 + b; // 构造哈希值避免重复边
// 如果 a 和 b 属于不同 SCC,且该边尚未添加
if (a != b && !S.count(hash))
{
e2[a].push_back(b); // 在 DAG 中添加一条有向边
S.insert(hash); // 标记该边已添加
}
}
}
// ========= DAG 上 DP(逆序处理) =========
for (int i = cnt; i >= 0; i--) // 从大到小遍历 SCC 编号
{
// 如果 f[i] 仍为 0,说明 i 是入度为 0 的起点
if (!f[i])
{
f[i] = siz[i]; // 从自身开始,节点数为当前 SCC 的大小
g[i] = 1; // 方案数为 1
}
// 从 i 转移到后继节点 j
for (int j : e2[i])
{
// 如果能获得更大的节点数
if (f[j] < f[i] + siz[j])
{
f[j] = f[i] + siz[j]; // 更新最大节点数
g[j] = g[i]; // 方案数继承自 i
}
// 如果节点数相等,累加方案数
else if (f[j] == f[i] + siz[j])
{
g[j] = (g[j] + g[i]) % mod; // 方案数相加并取模
}
}
}
// ========= 统计最终答案 =========
int maxf = 0; // 最大节点数
int sum = 0; // 达到最大节点数的方案数之和
for (int i = 1; i <= cnt; i++)
{
if (f[i] > maxf) // 找到更大的节点数
{
maxf = f[i];
sum = g[i]; // 重置方案数
}
else if (f[i] == maxf) // 节点数相等,累加方案数
{
sum = (sum + g[i]) % mod;
}
}
// 输出最大节点数和对应方案数
cout << maxf << endl;
cout << sum << endl;
return 0;
}
【运行结果】
6 6 20070603
1 2
2 1
1 3
2 4
5 6
6 4
3
3
浙公网安备 33010602011771号