1. 题目背景与问题解析
UVa 11981 "Corrupted Friendship" 是一道经典的图论题目,考察对树结构和图遍历算法的深入理解。题目描述了一组朋友关系形成的树结构,其中某个节点的"背叛"会导致连锁反应,我们需要计算这种"腐败"传播的特定模式。
这类树形结构上的动态传播问题在实际中有广泛应用,比如社交网络中的信息扩散、计算机网络中的故障传播等。题目要求我们处理节点状态变化对整体结构的影响,这与现实中的许多场景高度吻合。
2. 核心算法思路
2.1 树结构表示与输入处理
首先需要将输入的朋友关系表示为树结构。通常采用邻接表的方式存储:
cpp复制vector<vector<int>> tree(N+1); // N个节点
for(int i=0; i<N-1; i++) {
int u, v;
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
这里需要注意树的根节点选择。虽然树是无向的,但为了遍历方便,我们需要选择一个根(通常是节点1)并将其转换为有根树。
2.2 腐败传播的模拟
题目中的"腐败"传播遵循特定规则:当一个节点被腐蚀后,它会腐蚀所有子节点中编号最大的那个。这提示我们需要:
- 对每个节点的子节点按编号排序
- 递归模拟腐败传播过程
cpp复制void corrupt(int u) {
if(tree[u].empty()) return;
// 找到编号最大的子节点
int max_v = *max_element(tree[u].begin(), tree[u].end());
corrupted[max_v] = true;
corrupt(max_v);
}
2.3 腐败序列的生成
题目要求输出腐败发生的顺序。我们需要在模拟过程中记录被腐蚀的节点:
cpp复制vector<int> corruption_order;
void corrupt(int u) {
if(tree[u].empty()) return;
int max_v = *max_element(tree[u].begin(), tree[u].end());
if(!corrupted[max_v]) {
corruption_order.push_back(max_v);
corrupted[max_v] = true;
corrupt(max_v);
}
}
3. 完整解决方案实现
3.1 数据结构设计
完整的解决方案需要以下数据结构:
cpp复制const int MAXN = 1e5+5;
vector<vector<int>> tree(MAXN);
vector<bool> corrupted(MAXN, false);
vector<int> parent(MAXN);
vector<int> corruption_order;
3.2 树的预处理
在读取输入后,我们需要对树进行预处理,建立父子关系:
cpp复制void build_tree(int u, int p) {
parent[u] = p;
for(int v : tree[u]) {
if(v != p) {
build_tree(v, u);
}
}
}
3.3 主算法流程
主算法包括以下步骤:
- 读取输入并构建树
- 预处理建立父子关系
- 从初始腐败节点开始传播
- 输出结果
cpp复制int main() {
int N, M;
cin >> N >> M;
// 读取树结构
for(int i=0; i<N-1; i++) {
int u, v;
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
// 构建有根树
build_tree(1, -1);
// 处理每个初始腐败节点
while(M--) {
int u;
cin >> u;
if(!corrupted[u]) {
corrupted[u] = true;
corruption_order.push_back(u);
corrupt(u);
}
}
// 输出结果
for(int node : corruption_order) {
cout << node << " ";
}
cout << endl;
return 0;
}
4. 算法优化与注意事项
4.1 时间复杂度分析
基础实现的时间复杂度为O(N + MD),其中D是树的深度。在最坏情况下(退化成链),这可能达到O(NM),对于大规模数据需要优化。
4.2 优化思路
- 预处理子节点排序:可以预先对每个节点的子节点按编号排序,避免每次查找最大值
- 路径压缩:记录每个节点到下一个被腐蚀节点的直接关系,避免重复遍历
- 批量处理:对多个初始腐败节点,可以考虑它们的共同影响
4.3 常见错误与调试技巧
- 根节点选择:确保树的遍历从正确的根开始
- 重复腐蚀检查:避免同一节点被多次加入结果序列
- 输入规模:注意题目给出的N和M范围,选择合适的数据结构
- 边界条件:单独测试N=1或M=0的情况
调试提示:可以先用小规模手工计算的样例验证算法正确性,再逐步扩大测试规模。
5. 扩展思考与实际应用
5.1 问题变种
- 如果腐败传播规则改变(如随机选择子节点),算法如何调整?
- 如果朋友关系形成的是图而非树,该如何处理?
- 如果每个节点有腐败抵抗力,需要多次腐蚀才生效,算法如何修改?
5.2 实际应用场景
- 社交网络分析:模拟谣言或趋势的传播路径
- 网络安全:研究恶意软件在网络中的传播模式
- 组织管理:分析层级结构中决策或信息的影响路径
6. 完整代码示例
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 1e5+5;
vector<vector<int>> tree(MAXN);
vector<bool> corrupted(MAXN, false);
vector<int> parent(MAXN);
vector<int> corruption_order;
void build_tree(int u, int p) {
parent[u] = p;
// 移除父节点,只保留子节点
tree[u].erase(remove(tree[u].begin(), tree[u].end(), p), tree[u].end());
for(int v : tree[u]) {
build_tree(v, u);
}
}
void corrupt(int u) {
if(tree[u].empty()) return;
// 预先对子节点排序
sort(tree[u].begin(), tree[u].end());
int max_v = tree[u].back();
if(!corrupted[max_v]) {
corrupted[max_v] = true;
corruption_order.push_back(max_v);
corrupt(max_v);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M;
cin >> N >> M;
for(int i=0; i<N-1; i++) {
int u, v;
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
build_tree(1, -1);
while(M--) {
int u;
cin >> u;
if(!corrupted[u]) {
corrupted[u] = true;
corruption_order.push_back(u);
corrupt(u);
}
}
for(size_t i=0; i<corruption_order.size(); i++) {
if(i) cout << " ";
cout << corruption_order[i];
}
cout << "\n";
return 0;
}
7. 性能测试与优化对比
为了验证算法效率,我们可以设计不同规模的测试数据:
- 平衡树测试:完全二叉树,检验递归深度
- 链式结构测试:退化成链表,测试最坏情况
- 随机树测试:随机生成的树结构,检验平均性能
优化前后的性能对比可能如下:
| 测试案例 | 节点数 | 初始腐败数 | 原始算法(ms) | 优化算法(ms) |
|---|---|---|---|---|
| 平衡树 | 10,000 | 100 | 45 | 12 |
| 链式结构 | 10,000 | 100 | 320 | 28 |
| 随机树 | 50,000 | 500 | 210 | 85 |
8. 学习路径建议
要完全掌握此类问题,建议的学习路径:
- 基础数据结构:熟练掌握树和图的表现方法
- 经典算法:深度优先搜索(DFS)、广度优先搜索(BFS)
- 递归思维:理解递归在树问题中的应用
- 复杂度分析:学会评估算法效率
- 竞赛训练:通过UVa、Codeforces等平台练习类似题目
对于想进一步挑战的选手,可以尝试以下进阶题目:
- UVa 1218 Perfect Service
- Codeforces 734E Tree Coloring
- ICPC World Finals 2018 Problem D Gem Island