深入学习C++:什么是割点?
一. 基础概念
割点也称为关节点,是图论中的一个重要概念。
定义:在一个无向连通图中,如果删除某个顶点及其相关的边后,图被分成两个或多个不连通的子图,则称该顶点为割点。割点的存在意味着图的连通性依赖于这些特定的顶点,删除它们会导致图的连通性下降。
割点的优点:
1. 连通性评估:割点是图连通性的关键指标。确定割点可以帮助分析图的结构强度,了解哪些节点对整体连通性至关重要。
2. 块分解:割点将图分割成多个双连通分量(块),每个块内部不存在割点。这有助于将复杂图分解为更简单的子结构进行研究。
割点的实际运用价值:
1. 通信网络:在互联网、电信网络或传感器网络中,割点代表单点故障风险。识别割点可以帮助优化网络拓扑,增强可靠性(例如,通过冗余设计避免单点故障)。
2. 电力系统:电网中的割点可能导致大规模停电。通过分析割点,可以提高电网的抗灾能力。
二. 算法原理
求解图中割点的经典算法是 Tarjan 算法,该算法基于深度优先搜索(DFS),通过记录节点的访问时间和回溯值来判断割点。算法的核心思想如下:
时间戳:记录节点在 DFS 遍历中被访问的顺序。
回溯值:节点及其子树能够通过非父子边回溯到的最早节点的时间戳。
割点的判定条件:
1. 根节点:如果根节点有两个或更多的子树,则根节点是割点。
2. 非根节点:对于非根节点 u,如果存在子节点 v,使得low[v] ≥ dfn[u]
,则 u 是割点。
C++ 实现 Tarjan 算法求割点
下面是使用 C++ 实现 Tarjan 算法求无向图中割点的代码:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int MAXN = 10005; // 最大节点数
vector<int> graph[MAXN]; // 邻接表表示图
bool visited[MAXN]; // 记录节点是否被访问过
int dfn[MAXN]; // 节点的时间戳
int low[MAXN]; // 节点的回溯值
bool isCut[MAXN]; // 记录节点是否为割点
int n, m; // n为节点数,m为边数
int timestamp = 0; // 时间戳计数器
// Tarjan算法求割点
void tarjan(int u, int parent) {
int children = 0; // 记录子节点数量
visited[u] = true;
dfn[u] = low[u] = ++timestamp;
// 遍历所有邻接点
for (int v : graph[u]) {
if (!visited[v]) {
children++;
tarjan(v, u);
low[u] = min(low[u], low[v]);
// 割点判定条件1:非根节点,存在子节点v使得low[v] >= dfn[u]
if (parent != -1 && low[v] >= dfn[u]) {
isCut[u] = true;
}
} else if (v != parent) {
// 处理回边
low[u] = min(low[u], dfn[v]);
}
}
// 割点判定条件2:根节点,有两个或更多子节点
if (parent == -1 && children > 1) {
isCut[u] = true;
}
}
int main() {
cin >> n >> m;
// 读取图的边
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u); // 无向图
}
// 初始化
fill(visited, visited + n + 1, false);
fill(isCut, isCut + n + 1, false);
// 处理可能存在的多个连通分量
for (int i = 1; i <= n; i++) {
if (!visited[i]) {
tarjan(i, -1);
}
}
// 输出割点
cout << "割点集合:";
for (int i = 1; i <= n; i++) {
if (isCut[i]) {
cout << i << " ";
}
}
cout << endl;
return 0;
}
算法复杂度
时间复杂度:O (V+E),其中 V 是节点数,E 是边数,与 DFS 遍历的复杂度相同。
空间复杂度:O (V+E),主要用于存储图和算法过程中的辅助数组。
三. 示例(WZOI 1042嗅探器)
题目描述:
某军搞信息对抗实战演习。红军成功地侵入了蓝军的内部网络。蓝军共有两个信息中心。红军计划在某台中间服务器上安装一个嗅探器(Sniffer),从而能够侦听到这两个信息中心互相交换的所有信息。但是蓝军的网络相当庞大,数据包从一个信息中心到达另一个信息中心可以有不止一条的通路。现在需要你尽快地解决这个问题。应该把嗅探器安装在哪台中间服务器上才能保证所有的数据包都能被捕获?
输入格式:
第一行是一个整数n(1<=n<=100),表示蓝军网络中服务器的数目。接下来是若干行对蓝军网络拓扑结构的描述。每行两个整数i,j表示编号为i和编号为j的服务器之间存在直接连接。服务器的编号从1开始。描述以两个0结尾。再接下来一行是两个整数a,b分别表示两个信息中心服务器的编号。蓝军的整个网络保证是连通的。
输出格式:
输出要安装嗅探器的服务器编号。如果有多个解,输出编号最小的一个。如果找不到任何解,输出No solution
样例输入:
5 2 1 2 5 1 4 5 3 2 3 5 1 0 0 4 2
样例输出:
1
时间限制: 1000ms
空间限制: 256MB
解题思路
我们可以使用改进版的 Tarjan 算法,或者采用更简单的方法:对于每个节点 v (v≠a 且 v≠b),尝试从图中移除 v,然后检查 a 和 b 是否仍然连通。如果移除 v 后 a 和 b 不连通,则 v 是一个解。
AC代码如下:
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int MAXN = 105;
vector<int> graph[MAXN]; // 邻接表表示图
bool visited[MAXN]; // 访问标记
int n; // 服务器数量
// BFS检查a和b是否连通
bool isConnected(int a, int b, int exclude) {
fill(visited, visited + n + 1, false);
queue<int> q;
q.push(a);
visited[a] = true;
while (!q.empty()) {
int curr = q.front();
q.pop();
if (curr == b) return true;
for (int next : graph[curr]) {
if (next == exclude || visited[next]) continue;
visited[next] = true;
q.push(next);
}
}
return false;
}
int main() {
cin >> n;
// 读取网络拓扑
int i, j;
while (true) {
cin >> i >> j;
if (i == 0 && j == 0) break;
graph[i].push_back(j);
graph[j].push_back(i); // 无向图
}
// 读取两个信息中心的编号
int a, b;
cin >> a >> b;
// 查找解
int solution = -1;
for (int v = 1; v <= n; v++) {
if (v == a || v == b) continue; // 不能在信息中心安装嗅探器
// 尝试移除v,检查a和b是否连通
if (!isConnected(a, b, v)) {
solution = v;
break; // 找到编号最小的解就停止
}
}
// 输出结果
if (solution != -1) {
cout << solution << endl;
} else {
cout << "No solution" << endl;
}
return 0;
}