一、题目描述
给定一棵由 n n n 个节点组成的树和 m m m 个无序数对 ( a i , b i ) (a_i, b_i) (ai,bi),要求找到一条边,使得砍断这条边后,所有数对中的两个节点都不连通。若存在多条满足条件的边,输出编号最大的;否则输出 -1
。
二、题目分析
暴力实现思路
- 枚举每条边:尝试删除每条边,检查是否满足所有数对不连通
- 连通性检查:对每个数对使用DFS/BFS检查连通性
- 时间复杂度: O ( m × n 2 ) O(m \times n^2) O(m×n2),只能通过30%数据
正解思路
- 关键观察:满足条件的边必须被所有数对的路径覆盖
- 高效统计:使用树上差分快速统计每条边被路径覆盖的次数
- 算法选择:LCA(最近公共祖先)+ 树上差分
三、解题思路 + 算法介绍
1. LCA预处理
- 倍增法预处理每个节点的 2 k 2^k 2k 级祖先
- 支持 O ( log n ) O(\log n) O(logn) 时间查询任意两节点的LCA
2. 树上差分
- 边差分:将边权下放到子节点
- 对每个数对 ( a i , b i ) (a_i, b_i) (ai,bi):
diff[a_i]++
,diff[b_i]++
(路径端点标记)diff[lca(a_i, b_i)] -= 2
(消除LCA以上影响)
3. 统计覆盖次数
- DFS后序遍历累加差分值
diff[v]
表示边 u → v u→v u→v 被覆盖的次数
四、代码实现
暴力代码(30分)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
vector<int> path;
int w[N], h[N], e[N<<1], ne[N<<1], idx;void add(int a, int b) { e[idx]=b, ne[idx]=h[a], h[a]=idx++; }bool dfs(int u, int fa, int v) {if (u == v) return true;for (int i = h[u]; ~i; i = ne[i]) {int j = e[i];if (j == fa) continue;path.push_back(i/2 + 1);if (dfs(j, u, v)) return true;path.pop_back();}return false;
}int main() {memset(h, -1, sizeof h);int n, m; cin >> n >> m;for (int i = 1; i < n; i++) {int a, b; cin >> a >> b;add(a, b), add(b, a);}for (int i = 0; i < m; i++) {int a, b; cin >> a >> b;path.clear();dfs(a, -1, b);for (int e : path) w[e]++;}int ans = -1;for (int i = n-1; i >= 1; i--)if (w[i] == m) { ans = i; break; }cout << ans;
}
正解代码(100分)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m, diff[N], depth[N], f[N][20], cnt[N], ans = -1;
int h[N], e[N<<1], ne[N<<1], id[N<<1], idx;void add(int a, int b, int c) { e[idx]=b, id[idx]=c, ne[idx]=h[a], h[a]=idx++; }void bfs() {memset(depth, 0x3f, sizeof depth);queue<int> q; q.push(1); depth[1] = 1;while (q.size()) {int u = q.front(); q.pop();for (int i = h[u]; ~i; i = ne[i]) {int v = e[i];if (depth[v] > depth[u] + 1) {depth[v] = depth[u] + 1;f[v][0] = u;for (int k = 1; k < 20; k++)f[v][k] = f[f[v][k-1]][k-1];q.push(v);}}}
}int lca(int a, int b) {if (depth[a] < depth[b]) swap(a, b);for (int k = 19; k >= 0; k--)if (depth[f[a][k]] >= depth[b])a = f[a][k];if (a == b) return a;for (int k = 19; k >= 0; k--)if (f[a][k] != f[b][k])a = f[a][k], b = f[b][k];return f[a][0];
}void dfs(int u, int fa) {for (int i = h[u]; ~i; i = ne[i]) {int v = e[i];if (v == fa) continue;dfs(v, u);diff[u] += diff[v];if (id[i] != -1 && diff[v] == m)ans = max(ans, id[i]);}
}int main() {memset(h, -1, sizeof h);cin >> n >> m;for (int i = 1; i < n; i++) {int a, b; cin >> a >> b;add(a, b, i), add(b, a, i);}bfs();for (int i = 0; i < m; i++) {int a, b; cin >> a >> b;diff[a]++, diff[b]++;diff[lca(a, b)] -= 2;}dfs(1, -1);cout << ans;
}
五、重点细节
- 边编号处理:
- 无向边存储两次,通过
id[i]
避免重复统计 i/2 + 1
将邻接表索引转为边编号
- 无向边存储两次,通过
- 差分还原:
- DFS后序遍历确保先处理子树
diff[v]
直接表示边 u → v u→v u→v 的覆盖次数
- LCA优化:
- 使用BFS初始化避免递归栈溢出
- 哨兵节点
depth[0] = 0
处理边界
六、复杂度分析
算法 | 时间复杂度 | 空间复杂度 |
---|---|---|
暴力 | O ( m n 2 ) O(mn^2) O(mn2) | O ( n ) O(n) O(n) |
正解 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) |
七、总结
- 暴力法:简单直观但效率低,适用于小数据
- 正解法:
- 利用LCA快速定位路径
- 树上差分高效统计覆盖次数
- 完美结合树结构的递归特性
- 关键技巧:边权下放、差分标记、后序统计
- 适用场景:需要高效处理树上路径覆盖统计的问题