1. 题目背景与核心挑战解析
"P1084 [NOIP 2012 提高组] 疫情控制"作为全国青少年信息学奥林匹克联赛(NOIP)提高组的经典题目,考察的是图论算法在实际场景中的综合应用能力。题目设定在一个树形结构的国家中,军队需要被调度到特定节点阻断疫情传播,要求找到最短的调度时间。这类题型在竞赛中被称为"树上问题",其核心难点在于:
- 树结构的特殊性导致常规图论算法需要针对性优化
- 军队移动与时间计算的动态规划特性
- 多军队协同调度的最优策略选择
这道题在当年竞赛中区分度极高,正确率不足15%,主要卡点在于如何高效处理军队的"跨子树调度"问题。实际解题时需要结合二分答案、贪心策略和深度优先搜索(DFS)等多种算法。
2. 题目建模与算法选择
2.1 树形结构的数学表示
给定一棵包含n个节点的树,我们可以用邻接表存储结构:
cpp复制vector<vector<pair<int,int>>> tree(n+1); // 邻接表:存储{相邻节点,边权值}
其中边权值表示两个节点间的移动时间。根节点(首都)固定为1号节点,疫情从叶子节点向根节点传播。
2.2 问题转化为决策问题
题目要求"所有军队在限定时间内完成部署",这提示我们可以使用二分法确定最小时间。判断函数的核心逻辑:
python复制def is_possible(T):
# 判断是否能在时间T内完成所有部署
# 返回True/False
2.3 关键算法组件
- 二分框架:在[0, max_time]范围内搜索最小可行时间
- 贪心策略:优先处理距离首都最远的待保护节点
- DFS预处理:计算各节点到根节点的路径信息
- 双指针匹配:协调可调度军队与待保护节点的对应关系
3. 详细解题步骤
3.1 输入处理与初始化
cpp复制int n, m;
cin >> n;
vector<vector<pair<int,int>>> tree(n+1);
for(int i=1; i<n; ++i){
int u, v, w;
cin >> u >> v >> w;
tree[u].emplace_back(v, w);
tree[v].emplace_back(u, w);
}
cin >> m;
vector<int> armies(m);
for(int i=0; i<m; ++i) cin >> armies[i];
3.2 DFS预处理关键信息
需要预计算每个节点的:
cpp复制void dfs(int u, int parent){
fa[u][0] = parent;
for(int k=1; k<20; ++k)
fa[u][k] = fa[fa[u][k-1]][k-1];
for(auto [v,w]: tree[u]){
if(v == parent) continue;
depth[v] = depth[u] + 1;
dis[v] = dis[u] + w;
dfs(v, u);
}
}
3.3 二分判断函数实现
这是整个算法的核心,伪代码如下:
- 标记所有必须被控制的节点
- 对每个军队,计算其在时间T内能到达的最远节点
- 收集可以到达根节点且仍有剩余时间的军队
- 对未被覆盖的节点,用剩余军队进行匹配
- 检查是否所有关键节点都被覆盖
3.4 关键优化技巧
-
倍增法加速上移:使用二进制拆分思想快速计算军队在树上的移动
cpp复制int move_up(int u, int limit){ for(int k=19; k>=0; --k){ if(fa[u][k] && dis[u]-dis[fa[u][k]] <= limit){ limit -= (dis[u]-dis[fa[u][k]]); u = fa[u][k]; } } return u; } -
双指针贪心匹配:将待保护节点按到根距离排序,可用军队按剩余时间排序
4. 完整代码框架
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 5e4+10;
vector<vector<pair<int,int>>> tree;
int depth[N], dis[N], fa[N][20];
int n, m;
vector<int> armies;
bool check(int T){
// 实现判断逻辑
return true;
}
int main(){
// 输入处理
cin >> n;
tree.resize(n+1);
// 建树...
// 预处理
dfs(1, 0);
// 二分答案
int l=0, r=1e9, ans=-1;
while(l <= r){
int mid = (l+r)/2;
if(check(mid)){
ans = mid;
r = mid-1;
}else{
l = mid+1;
}
}
cout << ans << endl;
return 0;
}
5. 典型测试用例分析
考虑如下测试数据:
code复制7
1 2 1
1 3 1
2 4 1
2 5 1
3 6 1
3 7 1
3
1 2 3
这表示:
- 7个节点组成的完美二叉树
- 3支军队初始在节点1、2、3
- 边权均为1
正确解法:
- 二分初始范围[0, 3]
- 当T=2时:
- 军队1:可覆盖节点4或5
- 军队2:可覆盖节点6
- 军队3:可覆盖节点7
- 因此最小时间为2
6. 竞赛中的常见错误与调试技巧
6.1 边界条件处理
- 单节点树特殊情况
- 军队数等于叶子节点数的情况
- 所有军队初始就在叶子节点的情况
6.2 调试建议
- 可视化小规模测试用例的树结构
- 打印二分过程中的中间状态
- 检查倍增数组是否正确初始化
- 验证贪心匹配的排序顺序
关键提示:在NOIP赛场上,建议先写出暴力解法确保正确性,再逐步优化。本题的暴力解法(枚举所有调度方案)可以通过约30%的测试点。
7. 算法复杂度分析
- 预处理阶段:
- DFS遍历:O(n)
- 倍增数组:O(nlogn)
- 二分框架:
- 外循环:O(logT)
- 内判断:O(mlogn + nlogn)
- 总复杂度:O(nlogn + mlognlogT)
在实际竞赛中,该算法可以处理n,m≤5e4的数据规模。
8. 同类问题拓展
掌握本题后,可以解决以下变种问题:
- 多源点覆盖问题
- 带权重的节点覆盖
- 动态树结构下的实时查询
- 最大化最小覆盖能力
类似题目推荐:
- [POJ 3310] Caterpillar
- [Codeforces 555E] Case of Computer Network
- [洛谷 P5021] 赛道修建
9. 竞赛实战建议
- 编码规范:提前准备好树问题的模板(DFS、LCA、倍增等)
- 测试策略:设计三种以上不同类型的测试用例
- 时间分配:建议在90分钟内完成本题的编码与调试
- 调试输出:使用条件编译控制调试信息
cpp复制#define DEBUG #ifdef DEBUG #define debug(...) fprintf(stderr, __VA_ARGS__) #else #define debug(...) #endif
在实际比赛中,建议先完成30分的暴力解法,再尝试100分的正解。对于树形DP不熟悉的选手,可以重点练习以下几个基础问题:
- 树的最大独立集
- 树的重心
- 树的直径
- 最近公共祖先(LCA)
这类树上问题在NOIP/省选中出现频率极高,2020年NOIP提高组Day1T3"树的重心"就是典型代表。掌握本题涉及的算法思想,可以为后续学习更复杂的树分治、树链剖分等高级算法打下坚实基础。
