1. 供应链总销售额问题解析
这道PAT甲级考题考察的是树形结构的构建与遍历,以及基于供应链层级的价格计算。题目要求我们计算供应链中所有零售商销售商品的总金额,其中价格会随着供应链层级的增加而按一定比率递增。
供应链本质上是一个树形结构,每个节点代表供应链中的一个环节(供应商、分销商或零售商)。根节点是供应链的起点,叶子节点是最终的零售商。我们需要从根节点出发,通过广度优先搜索(BFS)遍历整棵树,记录每个节点的层级,然后根据层级计算对应零售商的销售额。
关键点:价格计算公式为kp(1+r%)^h,其中k是销售数量,p是基础价格,r是增长率,h是节点层级。
2. 数据结构设计与实现
2.1 链式前向星建树
我选择使用链式前向星来表示这棵树,这是处理树和图结构的经典方法。链式前向星通过数组模拟链表,相比指针实现的链表,它在OJ题目中更高效且不易出错。
cpp复制const int N=1e5+10;
int head[N];
struct Edge{
int to,next;
}e[N];
int cnt=0;
head数组存储每个节点的第一条边Edge结构体包含to(子节点)和next(下一条边)cnt是边的计数器
添加边的操作如下:
cpp复制void add(int u,int v){
e[cnt].next=head[u];
e[cnt].to=v;
head[u]=cnt++;
}
这种表示法的优势在于:
- 内存连续,访问效率高
- 不需要动态内存分配,避免内存泄漏
- 适合处理大规模数据(本题N可达10^5)
2.2 输入处理与初始化
输入处理需要特别注意节点类型判断:
- 非叶子节点:先输入子节点数量k,然后输入k个子节点
- 叶子节点:k为0,接着输入该零售商的销售数量
cpp复制void input(){
memset(head,-1,sizeof(head)); // 初始化head数组
cin>>n>>p>>r;
for(int i=0;i<n;++i){
int k;
cin>>k;
for(int j=0;j<k;++j){
int v;
cin>>v;
add(i,v); // 添加边
}
if(!k){ // 叶子节点处理
int value;
cin>>value;
v[i]=value; // 记录销售数量
}
}
}
3. 广度优先搜索实现
3.1 BFS核心逻辑
使用队列实现BFS,同时需要跟踪当前层级的节点数量,以确定何时进入下一层级:
cpp复制queue<int>q;
q.push(0); // 根节点入队
int num=1,h=0; // num当前层节点数,h当前层级
double res=0; // 总销售额
while(!q.empty()){
int u=q.front();
q.pop();num--;
// 如果是零售商,计算销售额
if(v[u]){
double fp=p;
for(int i=0;i<h;++i){
fp*=1+r/100;
}
res+=fp*v[u];
}
// 将子节点入队
for(int i=head[u];i!=-1;i=e[i].next){
q.push(e[i].to);
}
// 当前层处理完毕,进入下一层
if(num==0){
h++;num=q.size();
}
}
3.2 层级跟踪技巧
这里使用了一个巧妙的层级跟踪方法:
num初始为1(只有根节点)- 每处理一个节点,
num减1 - 当
num为0时,说明当前层已处理完,此时:- 层级h加1
num更新为下一层的节点数(当前队列大小)
这种方法避免了使用额外的数据结构来记录节点层级,节省了空间。
4. 价格计算优化
原始代码中使用循环计算价格增长:
cpp复制double fp=p;
for(int i=0;i<h;++i){
fp*=1+r/100;
}
这会导致时间复杂度为O(h),当h很大时效率不高。可以优化为使用幂运算:
cpp复制double fp = p * pow(1+r/100, h);
不过在实际OJ环境中,由于h通常不会太大(供应链层级一般不会太深),两种方法的时间差异不大。但后者更简洁,数学意义更明确。
5. 常见问题与调试技巧
5.1 初始化问题
- 忘记初始化
head数组为-1会导致遍历时无法正确判断边是否结束 - 解决方案:使用
memset(head,-1,sizeof(head))
5.2 精度控制
题目要求输出保留一位小数,必须使用正确的输出方式:
cpp复制cout<<fixed<<setprecision(1)<<res;
常见错误:
- 忘记
fixed导致科学计数法输出 - 精度设置不正确导致四舍五入错误
5.3 边界情况
需要测试的特殊情况包括:
- 单节点树(只有根节点是零售商)
- 链式结构(每个节点只有一个子节点)
- 完全二叉树
- 大规模数据(测试时间效率)
6. 性能分析与优化
6.1 时间复杂度
- 建树:O(N),每个节点处理一次
- BFS遍历:O(N),每个节点访问一次
- 价格计算:最坏O(H),H为树高
总体时间复杂度为O(N*H),在PAT的数据规模下是可接受的。
6.2 空间复杂度
- 链式前向星:O(N)
- 队列:最坏O(N)
- 其他数组:O(N)
总体空间复杂度为O(N),效率很高。
6.3 进一步优化方向
- 预处理价格增长系数,避免重复计算
- 使用深度优先搜索(DFS)替代BFS,减少队列操作
- 对于特别深的树,可以考虑记忆化搜索
7. 完整代码实现
以下是整合了所有优化和注释的完整代码:
cpp复制/**
* PAT甲级 1106. Supply Chain - Total Sales
* 解法:链式前向星建树 + BFS层级遍历
*/
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int head[N]; // 链式前向星头指针数组
struct Edge{
int to,next; // 边的终点和下一条边
}e[N];
int cnt=0; // 边计数器
int v[N]; // 节点销售数量(非零表示零售商)
// 添加边u->v
void add(int u,int v){
e[cnt].next=head[u];
e[cnt].to=v;
head[u]=cnt++;
}
int n; // 节点总数
double p,r; // 基础价格和增长率
// 输入处理函数
void input(){
memset(head,-1,sizeof(head)); // 初始化head数组
cin>>n>>p>>r;
for(int i=0;i<n;++i){
int k;
cin>>k;
for(int j=0;j<k;++j){
int v;
cin>>v;
add(i,v); // 添加边
}
if(!k){ // 叶子节点处理
int value;
cin>>value;
v[i]=value; // 记录销售数量
}
}
}
// 主求解函数
void solve(){
input();
queue<int>q;
q.push(0); // 根节点入队
int num=1,h=0; // num当前层节点数,h当前层级
double res=0; // 总销售额
while(!q.empty()){
int u=q.front();
q.pop();num--;
// 如果是零售商,计算销售额
if(v[u]){
double fp = p * pow(1+r/100, h); // 优化后的价格计算
res += fp * v[u];
}
// 遍历所有子节点
for(int i=head[u];i!=-1;i=e[i].next){
q.push(e[i].to);
}
// 当前层处理完毕,进入下一层
if(num==0){
h++;
num=q.size();
}
}
// 输出结果,保留1位小数
cout<<fixed<<setprecision(1)<<res;
}
int main(){
ios::sync_with_stdio(0); // 加速IO
cin.tie(0);cout.tie(0);
solve();
return 0;
}
8. 类似题目对比
这道题与PAT的另一道题目"Highest Price in Supply Chain (25)"非常相似,主要区别在于:
- 本题计算所有零售商的总销售额
- "Highest Price"只计算最深的零售商的价格
- 两题都可以使用相同的树形结构和遍历方法
解决这类供应链问题的通用思路:
- 正确建立树形结构(链式前向星或邻接表)
- 选择适当的遍历方法(BFS适合层级计算,DFS适合深度相关计算)
- 在遍历过程中维护必要的状态信息(如当前层级)
- 根据题目要求进行相应的计算
在实际编程竞赛中,熟练掌握这种树形结构的表示和遍历方法,可以高效解决一大类图论和树形DP问题。我建议初学者多练习类似的题目,直到能够不假思索地写出链式前向星和BFS/DFS的模板代码。