洛谷 - P2812 校园网络【[USACO]Network of Schools加强版】
这个题其实是洛谷 - P2746的加强版,只是改了数据范围。
详细分析思路见这里
#include <cstdio> // 用于输入输出函数,如scanf、printf
#include <cstring> // 用于内存操作函数,如memset
#include <algorithm> // 用于算法函数,如min
using namespace std; // 引入标准命名空间,避免每次使用std::前缀
// 常量定义
const int N = 1e4 + 10; // 最大学校数量(节点数),1e2=100,+10是为了防止数组越界
const int M = N * N; // 最大边数,每个节点最多连接到其他所有节点,故为N*N
int n; // 实际学校数量(节点数)
// 邻接表存储图
int e[M]; // 存储每条边的终点
int ne[M]; // 存储下一条边的索引,用于链表结构
int h[N]; // 存储每个节点的第一条边的索引,初始化为-1表示无第一条边
int idx; // 边的计数器,用于给新边分配索引
// Tarjan算法相关变量
int dfn[N]; // 存储每个节点的深度优先搜索次序(时间戳)
int low[N]; // 存储每个节点能回溯到的最早时间戳
int timestamp; // 时间戳计数器,用于记录访问顺序
// 用于强连通分量(SCC)的栈
int stk[N]; // 存储当前路径上的节点
int top; // 栈顶指针
bool instk[N]; // 标记节点是否在栈中
// 强连通分量相关变量
int scc[N]; // 存储每个节点所属的强连通分量编号
int size[N]; // 存储每个强连通分量的大小(包含的节点数)
int cnt; // 强连通分量的计数器
// 缩点后的有向图的入度和出度
int indegree[N]; // 每个强连通分量的入度
int outdegree[N]; // 每个强连通分量的出度
// 向邻接表中添加一条从a到b的有向边
void add(int a, int b) {
e[idx] = b; // 第idx条边的终点是b
ne[idx] = h[a]; // 第idx条边的下一条边是a当前的第一条边
h[a] = idx++; // 更新a的第一条边为第idx条边,然后idx自增
}
// Tarjan算法,用于寻找强连通分量
void tarjan(int u) {
dfn[u] = low[u] = ++timestamp; // 初始化当前节点的dfn和low为当前时间戳,并递增时间戳
stk[++top] = u; // 将当前节点入栈
instk[u] = true; // 标记当前节点在栈中
// 遍历当前节点的所有邻接边
for (int i = h[u]; ~i; i = ne[i]) { // ~i 等价于 i != -1,遍历链表直到结束
int v = e[i]; // 获取边的终点v
if (!dfn[v]) { // 如果v还未被访问过
tarjan(v); // 递归访问v
// 更新当前节点的low值为自身low和v的low中的较小值
low[u] = min(low[u], low[v]);
}
// 如果v已被访问且在栈中(说明在当前强连通分量中)
else if (instk[v]) {
// 更新当前节点的low值为自身low和v的dfn中的较小值
low[u] = min(low[u], dfn[v]);
}
}
// 如果当前节点是其所在强连通分量的根节点(dfn等于low)
if (dfn[u] == low[u]) {
cnt++; // 强连通分量数量加1
int v; // 用于临时存储出栈的节点
// 将栈中从当前节点开始的所有节点弹出,这些节点构成一个强连通分量
do {
v = stk[top--]; // 栈顶节点出栈
instk[v] = false; // 标记节点不在栈中
scc[v] = cnt; // 记录节点v所属的强连通分量编号
size[cnt]++; // 该强连通分量的大小加1
} while (v != u); // 直到弹出的节点是当前节点u为止
}
}
int main() {
memset(h, -1, sizeof h); // 初始化邻接表头,所有节点初始无第一条边
scanf("%d", &n); // 读取学校数量n
// 读取每个学校的连接关系
for (int a = 1; a <= n; a++) { // 遍历每个学校a(1-based索引)
while (true) {
scanf("%d", &b); // 读取a指向的学校b
if (b == 0) break; // 若b为0,结束当前学校的输入
add(a, b); // 添加一条从a到b的有向边
}
}
// 对所有未访问的节点执行Tarjan算法,寻找强连通分量
for (int i = 1; i <= n; i++) {
if (!dfn[i]) { // 如果节点i未被访问过
tarjan(i); // 对i执行Tarjan算法
}
}
// 计算缩点后每个强连通分量的入度和出度
for (int u = 1; u <= n; u++) { // 遍历每个节点u
// 遍历u的所有邻接边
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i]; // 边的终点v
// 如果u和v属于不同的强连通分量,则说明两个分量之间有边
if (scc[u] != scc[v]) {
indegree[scc[v]]++; // v所在分量的入度加1
outdegree[scc[u]]++; // u所在分量的出度加1
}
}
}
// 统计入度为0和出度为0的强连通分量数量
int in = 0, out = 0;
for (int i = 1; i <= cnt; i++) {
if (indegree[i] == 0) in++; // 入度为0的分量数
if (outdegree[i] == 0) out++; // 出度为0的分量数
}
// 输出第一个答案:至少需要添加的边数,使所有节点能被到达(入度为0的分量数)
printf("%d\n", in);
// 输出第二个答案:至少需要添加的边数,使整个图成为强连通图
if (cnt == 1) puts("0"); // 如果只有一个强连通分量,不需要添加边
else printf("%d\n", max(in, out)); // 否则为入度0和出度0的分量数的最大值
return 0;
}