1 条题解
-
0
H. 图中合并顶点 题解
一、题意理解
给定一个无向图,每次操作可以合并两个顶点 和 :
- 移除 和 ;
- 插入新顶点 , 与所有同时与 和 都相连的顶点相连。
操作进行到图中出现一个与所有其他顶点都直接相连的"中心顶点"时停止。若初始已存在这样的顶点,不能进行任何操作。
求最少和最多操作次数。
二、补图转化
考虑原图的补图 (顶点相同,原图有边则在补图中无边,原图无边则在补图中有边)。
原图中的合并操作:新顶点与交集中的顶点相连。
在补图中:
- 原图的邻接交集 补图的邻接并集(因为 在补图中的补是 )。
原图停止条件:存在一个顶点与所有其他顶点相连(度数为 )。
在补图中:该顶点与谁都不相连(度数为 ),即为孤立顶点。
三、问题重述
在补图 中:
- 操作:选择两个顶点,合并它们,新顶点的邻接集合是原来两个邻接集合的并集。
- 目标:形成一个孤立顶点。
- 求最少/最多操作次数。
四、最少操作次数
4.1 关键观察
要得到一个孤立顶点,需要合并一组顶点,使得它们与集合外的任何顶点都没有边相连。否则,合并后的顶点将会保留外部连接,无法成为孤立点。
这样的顶点集合正是补图的一个连通分量。
4.2 结论
最少操作次数 = 补图中最小连通分量的大小 。
- 若补图中已经有孤立顶点(即原图中存在度数为 的顶点),最少操作次数为 。
- 否则,选择一个最小的连通分量,将其内部所有顶点合并为一个孤立顶点,需要 次操作。
五、最多操作次数
5.1 分析
每次合并两个顶点,整个图的顶点数减少 。
最终状态:
- 若初始原图中没有顶点与所有其他顶点相连(补图中无孤立顶点),则可以一直合并到只剩一个顶点(该顶点自然孤立)。
因此,最多操作次数可达 。
但需注意:如果补图的某个连通分量大小为 (即原图中存在中心顶点),则无法进行任何操作,答案为 。
5.2 结论
$$\boxed{ \text{max\_ops} = \begin{cases} 0, & \text{若原图已有中心顶点(补图有孤立顶点)} \\[4pt] n - 1, & \text{否则} \end{cases} } $$
六、算法实现
6.1 遍历补图求连通分量
补图可能非常稠密(边数可达 ),无法显式构建。需要隐式遍历。
核心技巧(BFS/DFS 在补图上):
维护一个全局未访问顶点集合 。
从某个未访问顶点 开始:
- 标记 为已访问,将其从 中移除。
- 维护集合 :当前连通分量中顶点的所有原图邻居在原图中已被访问、但在补图中可能相连的候选。
- 遍历 中的顶点 :
- 若 (即 与 在原图中没有边),则在补图中 与 有边, 属于同一连通分量。
- 将 标记、移出 ,并更新 。
- 继续扩展直到无法找到新的连通顶点。
6.2 复杂度分析
- 每次找到一个补图中的邻居(即 ),归入分量并移出 。这种情况最多发生 次。
- 每次检查发现 (即原图中有边),这种事发生的总次数为所有顶点的原图度数之和 。
- 因此总复杂度为 (使用平衡树维护 和 )。
七、标程解析
private fun solveTest() { val n = readInt() val m = readInt() val g = Array(n) { mutableListOf<Int>() } repeat(m) { val x = readInt() - 1 val y = readInt() - 1 g[x].add(y) g[y].add(x) } val z = intarr(n, 0) // 访问标记 var a = (0..n - 1).toMutableSet() // 未访问顶点集合 var res = Int.MAX_VALUE // 最小连通分量大小 while (!a.isEmpty()) { var v = a.iterator().next() a.remove(v) z[v] = 1 var s = mutableSetOf<Int>() for (x in g[v]) { if (z[x] == 0) s.add(x) // v 的原图未访问邻居 } var k = 1 // 当前连通分量的顶点数 while (true) { var ok = false for (x in a) { if (!s.contains(x) && z[x] == 0) { // x 与当前分量在补图中有边 z[x] = 1 a.remove(x) var ss = mutableSetOf<Int>() for (y in g[x]) { if (z[y] == 0 && s.contains(y)) { ss.add(y) } } ok = true s = ss k++ break } } if (!ok) { res = min(res, k) break } } } if (res == 1) { // 存在孤立顶点 = 原图有中心顶点 println("0 0") } else { println("${res - 1} ${n - 1}") } }步骤解析:
- 构建原图邻接表 。
- 初始化未访问集合 。
- 循环直到 为空,每次取出一个连通分量:
- 从 中任取起点 ,标记已访问。
- 构建集合 : 的原图邻居中尚未访问的顶点(这些在补图中与 无边,不能直接到达)。
- 令当前分量大小 。
- 不断在 中寻找不在 中的顶点 (即补图中与分量相连),将其加入分量,更新 为 与 的交集相关的邻居。
- 当找不到这样的顶点时,当前连通分量已完整,记录大小 。
- 所有分量遍历完毕后,
res存的是最小分量大小。 - 若
res == 1,输出0 0;否则输出res-1和n-1。
八、样例验证
样例 1
5 7 1 2 3 2 2 4 3 5 1 4 5 1 4 3原图中心是 和 ?实际上补图:边为 等。补图的连通分量: 大小 , 大小 , 大小 。
- 最小分量大小 → 最少操作 ?不对,样例输出是 。
注意:标程的判断逻辑
if (res == 1)时输出0 0,但样例 中res应为 (因为分量 或 的大小为 ),最少操作 ,最多 。输出1 4✅样例 2
4 3 1 2 1 3 1 4原图中顶点 已经是中心(度数为 )。补图中 是孤立顶点。
- 找到大小为 的分量,输出
0 0✅
样例 3
200000 0原图无边,补图是完全图 ,整个图是一个连通分量,大小 。
- 最少操作
- 最多操作
输出
199999 199999✅
九、代码实现(C++)
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<vector<int>> g(n); for (int i = 0; i < m; i++) { int x, y; cin >> x >> y; x--; y--; g[x].push_back(y); g[y].push_back(x); } set<int> unvisited; for (int i = 0; i < n; i++) unvisited.insert(i); int min_comp = n + 1; vector<int> vis(n, 0); while (!unvisited.empty()) { int v = *unvisited.begin(); unvisited.erase(v); vis[v] = 1; set<int> s; // 原图中已访问但在补图中不能直达的候选 for (int x : g[v]) { if (!vis[x]) s.insert(x); } int comp_size = 1; while (true) { bool found = false; for (auto it = unvisited.begin(); it != unvisited.end(); ) { int x = *it; if (!s.count(x) && !vis[x]) { vis[x] = 1; it = unvisited.erase(it); set<int> ss; for (int y : g[x]) { if (!vis[y] && s.count(y)) { ss.insert(y); } } s = ss; comp_size++; found = true; break; } else { it++; } } if (!found) break; } min_comp = min(min_comp, comp_size); } if (min_comp == 1) { cout << "0 0\n"; } else { cout << min_comp - 1 << " " << n - 1 << "\n"; } return 0; }
十、复杂度分析
- 每个顶点从
unvisited中移除一次,遍历unvisited时失败匹配的总次数为 (每次匹配失败对应原图中的一条边)。 - 使用
set实现,总时间复杂度 。 - 空间复杂度 。
- 1
信息
- ID
- 6663
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者