1. 项目概述:供应链最高价问题解析
这道来自PAT(Programming Ability Test)的"Highest Price in Supply Chain"题目,是浙江大学计算机程序设计能力考试中的经典题型。题目编号(25)表明其难度属于甲级中等偏上水平,主要考察考生对树形结构的理解和DFS/BFS算法的应用能力。
供应链价格问题本质上是一个树形结构的遍历问题。给定一个供应链网络(即一棵树),其中根节点代表生产商,中间节点代表分销商,叶节点代表零售商。每个节点会将自己的进货价格乘以一定比率作为销售价格传递给下级节点。我们需要找出所有零售商(叶节点)中的最高售价,并统计达到该最高售价的零售商数量。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题建模与算法选择
2.1 输入数据格式分析
题目典型输入包含三部分:
- 节点总数N(≤10^5)
- 根节点提供的初始价格P
- 价格增长率r(百分比)
- 接下来的N行,每行给出一个节点的父节点编号(根节点的父节点记为-1)
这种数据规模提示我们需要使用O(N)或O(NlogN)时间复杂度的算法,暴力解法显然不适用。
2.2 树形结构的存储方案
对于大规模树形数据,常见的存储方式有:
- 邻接表:使用vector<vector
>存储每个节点的子节点 - 父指针表示法:使用数组parent[N]存储每个节点的父节点
- 孩子兄弟表示法:复杂场景下使用
在本题中,邻接表是最直观的选择:
cpp复制vector<vector<int>> tree(N);
for(int i=0; i<N; i++){
if(parent[i] != -1)
tree[parent[i]].push_back(i);
}
2.3 遍历算法选择
两种基本遍历策略:
- DFS(深度优先搜索):适合寻找路径相关属性,实现简单
- BFS(广度优先搜索):适合层次遍历,可以记录每个节点的深度
由于我们需要计算从根到叶子的完整路径价格,DFS的递归特性更符合需求。但需要注意递归深度可能导致的栈溢出问题(虽然PAT测试数据通常不会)。
3. 核心算法实现细节
3.1 DFS递归实现
cpp复制void dfs(int node, double currentPrice, double& maxPrice, int& count){
if(tree[node].empty()){ // 叶子节点
if(currentPrice > maxPrice){
maxPrice = currentPrice;
count = 1;
}else if(currentPrice == maxPrice){
count++;
}
return;
}
for(int child : tree[node]){
dfs(child, currentPrice * (1 + r/100), maxPrice, count);
}
}
3.2 迭代式DFS实现
对于极端深度的树,递归可能导致栈溢出,可以使用显式栈的迭代实现:
cpp复制stack<pair<int, double>> s;
s.push({root, P});
double maxPrice = 0.0;
int count = 0;
while(!s.empty()){
auto [node, price] = s.top();
s.pop();
if(tree[node].empty()){ // 叶子节点
if(price > maxPrice){
maxPrice = price;
count = 1;
}else if(price == maxPrice){
count++;
}
}else{
for(auto it = tree[node].rbegin(); it != tree[node].rend(); ++it){
s.push({*it, price * (1 + r/100)});
}
}
}
3.3 价格计算精度处理
浮点数比较是这类题目的常见坑点。建议:
- 使用double而非float保证精度
- 比较时使用相对误差而非绝对相等:
cpp复制const double EPS = 1e-8;
if(fabs(price - maxPrice) < EPS){
count++;
}else if(price > maxPrice){
maxPrice = price;
count = 1;
}
4. 性能优化与边界条件
4.1 输入优化
对于大规模数据(N=10^5),普通的cin/cout可能超时:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
或者使用更快的输入方式:
cpp复制int read(){
int x=0; char ch=getchar();
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
return x;
}
4.2 特殊边界情况
需要考虑的边界case:
- 单节点树(只有生产商没有零售商)
- 所有节点都在一条链上(退化成链表)
- 完全平衡的二叉树
- 最大规模数据测试(验证算法时间复杂度)
4.3 内存优化
对于极端情况,可以优化邻接表内存:
cpp复制vector<int> tree[N]; // 全局数组,但可能栈溢出
// 或者
vector<vector<int>> tree;
tree.reserve(N); // 预分配内存
5. 完整参考代码实现
cpp复制#include <iostream>
#include <vector>
#include <cmath>
#include <iomanip>
using namespace std;
const double EPS = 1e-8;
vector<vector<int>> tree;
double P, r;
double maxPrice = 0.0;
int count = 0;
void dfs(int node, double currentPrice){
if(tree[node].empty()){
if(fabs(currentPrice - maxPrice) < EPS){
count++;
}else if(currentPrice > maxPrice){
maxPrice = currentPrice;
count = 1;
}
return;
}
for(int child : tree[node]){
dfs(child, currentPrice * (1 + r/100));
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N >> P >> r;
tree.resize(N);
int root = -1;
for(int i=0; i<N; i++){
int parent;
cin >> parent;
if(parent == -1){
root = i;
}else{
tree[parent].push_back(i);
}
}
dfs(root, P);
cout << fixed << setprecision(2) << maxPrice << " " << count;
return 0;
}
6. 常见错误与调试技巧
6.1 典型错误类型
- 浮点精度问题:直接使用==比较浮点数
- 根节点识别错误:未正确处理parent[i]==-1的情况
- 叶节点判断错误:错误地将非空节点视为叶子
- 初始化问题:maxPrice未初始化为0
- 计数错误:在发现新的maxPrice时未重置count
6.2 测试用例设计
建议自测用例:
code复制// 普通情况
9 1.80 1.00
1 5 4 4 -1 4 5 3 3
// 单节点
1 100.00 5.00
-1
// 链式结构
5 1.00 10.00
1 2 3 4 -1
// 完全二叉树
7 1.00 10.00
1 2 -1 1 2 3 3
6.3 调试输出技巧
在DFS中添加调试输出:
cpp复制void dfs(int node, double currentPrice, int depth){
cout << "Node:" << node << " Price:" << currentPrice
<< " Depth:" << depth << endl;
// ...
}
7. 算法扩展与变种思考
7.1 问题变种
- 加权供应链:不同层级的增长率不同
- 多源供应链:存在多个生产商(多根树)
- 网络结构:存在循环依赖(图结构)
- 动态更新:实时更新价格并查询
7.2 实际应用场景
- 电商平台价格体系计算
- 物流成本传导分析
- 金融产品收益率计算
- 组织架构中的信息传递效率
7.3 进阶学习建议
- 学习更多树形DP问题
- 掌握Tarjan等离线算法处理树形结构
- 了解并查集在树形结构中的应用
- 学习跳表等数据结构优化树形查询
这道题目很好地展示了如何将现实中的供应链问题抽象为树形结构算法问题。在实际编程训练中,建议不仅满足于通过测试用例,更要思考各种边界情况和性能优化方案。我在最初实现时曾因浮点数比较问题卡了很久,后来才意识到需要使用相对误差比较,这种经验教训往往比算法本身更宝贵。
