1. 医院设置问题概述
P1364医院设置是一道经典的图论与数据结构题目,主要考察对树形结构的理解和应用能力。题目描述了一个医院选址的场景:给定一棵包含n个节点的树,每个节点代表一个居民区,附带一个人口数量。要求在这棵树上选择一个节点建立医院,使得所有居民到医院的"总距离代价"最小。
这里的"总距离代价"定义为:医院所在节点到每个居民区节点的距离,乘以该居民区的人口数,再将所有居民区的这个值相加。用数学表达式表示就是:
总代价 = Σ(人口[i] × dist[i][医院位置])
其中dist[i][j]表示节点i到节点j的最短距离(在树中即唯一路径的边数)。
2. 问题分析与解法思路
2.1 树结构特性利用
树是一种特殊的图,具有以下关键特性:
- 任意两点之间有且只有一条路径
- 没有环路
- 边数 = 节点数 - 1
这些特性使得我们可以采用比一般图更高效的算法。对于医院选址问题,最直观的解法是:
- 枚举每个节点作为医院位置
- 计算该位置下的总代价
- 选择总代价最小的节点
2.2 算法选择与优化
朴素算法的时间复杂度是O(n²),当n较大时效率不高。我们可以通过以下优化手段:
- 预处理节点深度:通过一次DFS或BFS遍历,记录每个节点的深度和父节点信息
- 快速计算距离:利用LCA(最低公共祖先)算法快速计算任意两节点间距离
- 动态规划思想:在遍历树时,维护子树的代价信息,避免重复计算
3. 详细实现步骤
3.1 数据结构定义
首先定义树的存储结构。对于本题,常用的表示方法有:
const int MAXN = 100; vector<int> tree[MAXN]; // 邻接表表示 int population[MAXN]; // 各节点人口 int n; // 节点数量3.2 距离计算优化
实现快速计算两点距离的函数:
int depth[MAXN]; int parent[MAXN][20]; // 二进制提升表 // DFS预处理深度和父节点信息 void dfs(int u, int p) { depth[u] = depth[p] + 1; parent[u][0] = p; for(int i = 1; i < 20; i++) parent[u][i] = parent[parent[u][i-1]][i-1]; for(int v : tree[u]) { if(v != p) dfs(v, u); } } // 计算LCA int lca(int u, int v) { if(depth[u] < depth[v]) swap(u, v); for(int i = 19; i >= 0; i--) if(depth[parent[u][i]] >= depth[v]) u = parent[u][i]; if(u == v) return u; for(int i = 19; i >= 0; i--) if(parent[u][i] != parent[v][i]) u = parent[u][i], v = parent[v][i]; return parent[u][0]; } // 计算两点距离 int getDistance(int u, int v) { return depth[u] + depth[v] - 2 * depth[lca(u, v)]; }3.3 代价计算与比较
实现计算总代价的函数:
int calculateCost(int hospital) { int total = 0; for(int i = 1; i <= n; i++) { total += population[i] * getDistance(i, hospital); } return total; } int findOptimalLocation() { dfs(1, 0); // 假设1为根节点 int minCost = INT_MAX; int bestPos = 1; for(int i = 1; i <= n; i++) { int cost = calculateCost(i); if(cost < minCost) { minCost = cost; bestPos = i; } } return bestPos; }4. 算法优化进阶
4.1 动态规划解法
上述解法虽然正确,但仍有优化空间。我们可以利用树形DP的思想,将时间复杂度降到O(n):
- 第一次DFS:计算以每个节点为根的子树总人口和子树代价
- 第二次DFS:利用父节点的信息推导其他节点的总代价
实现代码:
int subPopulation[MAXN]; // 子树总人口 int subCost[MAXN]; // 子树代价 int totalCost[MAXN]; // 各节点作为医院的总代价 void dfs1(int u, int p) { subPopulation[u] = population[u]; subCost[u] = 0; for(int v : tree[u]) { if(v == p) continue; dfs1(v, u); subPopulation[u] += subPopulation[v]; subCost[u] += subCost[v] + subPopulation[v]; } } void dfs2(int u, int p, int costAbove) { totalCost[u] = subCost[u] + costAbove; for(int v : tree[u]) { if(v == p) continue; int newCostAbove = totalCost[u] - (subCost[v] + subPopulation[v]) + (totalPopulation - subPopulation[v]); dfs2(v, u, newCostAbove); } } int findOptimalDP() { dfs1(1, 0); totalPopulation = subPopulation[1]; dfs2(1, 0, 0); int minCost = INT_MAX; int bestPos = 1; for(int i = 1; i <= n; i++) { if(totalCost[i] < minCost) { minCost = totalCost[i]; bestPos = i; } } return bestPos; }4.2 算法复杂度分析
- 朴素算法:O(n²)
- LCA优化:O(nlogn)预处理,O(logn)查询,总体O(nlogn)
- 树形DP:两次DFS,O(n)
5. 实际应用与变种
5.1 现实场景应用
医院选址问题在实际中有广泛应用:
- 城市公共服务设施规划
- 物流中心选址
- 网络服务器部署
- 教育资源分配
5.2 题目变种与扩展
- 加权边版本:边带有距离权重,而不仅仅是单位距离
- 多医院选址:需要选择k个位置建立医院
- 动态人口变化:居民区人口会随时间变化
- 图结构扩展:将树扩展为一般图的情况
6. 常见问题与调试技巧
6.1 常见错误
边界条件处理:
- 单节点树
- 所有人口为零的情况
- 线性链状树
实现错误:
- LCA实现不正确
- 动态规划状态转移错误
- 忘记初始化变量
6.2 调试建议
- 先测试小规模数据(n≤5)
- 验证LCA计算的正确性
- 打印中间计算结果检查
- 对比朴素算法和优化算法的结果
提示:在竞赛中,建议先实现朴素算法作为保底,再尝试优化算法。同时要仔细阅读题目输入格式要求,避免因输入处理错误导致WA。
7. 性能优化实践
对于大规模数据(n>1e5),还需要进一步优化:
- 使用更高效的LCA算法:二进制提升法已经足够高效,但也可以使用Tarjan离线算法或欧拉序+RMQ
- 内存优化:使用链式前向星代替vector存储树结构
- 输入输出优化:使用快速IO方法
示例代码片段:
const int MAXN = 1e5 + 5; struct Edge { int to, next; } edges[MAXN * 2]; int head[MAXN], edgeCnt; void addEdge(int u, int v) { edges[++edgeCnt] = {v, head[u]}; head[u] = edgeCnt; } // 遍历u的邻接点 for(int i = head[u]; i; i = edges[i].next) { int v = edges[i].to; // ... }8. 扩展思考
- 几何版本:如果居民区位于二维平面上,问题变为经典的几何中位数问题
- 动态树:支持动态添加/删除边的情况
- 分布式计算:对于超大规模数据,如何分布式计算最优位置
在实际工程应用中,可能还需要考虑:
- 建设成本差异
- 地形限制
- 未来发展预期
- 多目标优化
这些因素使得问题更加复杂,往往需要结合运筹学、启发式算法等方法来解决。