1. 题目解析与核心思路
"P8812 [蓝桥杯 2022 国 C] 打折"这道题目来自蓝桥杯全国总决赛C组,属于普及+/提高难度的算法题。题目描述通常涉及某种商业场景下的价格计算问题,结合差分数组等算法进行优化处理。
1.1 题目场景还原
根据蓝桥杯历年出题风格和题目编号规律,P8812大概率是这样一个场景:
- 某商店进行促销活动,商品在不同时间段有不同的折扣率
- 需要计算某个顾客在一段时间内购买商品的总花费
- 顾客可能多次购买同一商品,且购买时间跨度可能覆盖多个折扣时段
这类题目通常会给出:
- 商品的原价
- 多个折扣时段及其折扣率
- 顾客的购买记录(时间、商品、数量)
- 要求输出最终应付金额
1.2 算法选择依据
题目相关热搜词中出现了"差分数组",这提示我们需要处理区间更新和单点查询问题。在折扣问题中:
- 每个折扣时段可以看作是对时间区间的修改
- 每次购买需要查询特定时间点的折扣率
- 差分数组能将O(n)的区间修改降为O(1)的两次单点修改
- 最后通过前缀和恢复原始数组,实现高效查询
2. 差分数组深度解析
2.1 差分数组原理
差分数组是一种用于高效处理区间增减操作的数据结构。给定原始数组A,其差分数组D定义为:
- D[0] = A[0]
- D[i] = A[i] - A[i-1] (i > 0)
关键性质:
- 对A的区间[l,r]增加x,等价于D[l]+=x和D[r+1]-=x
- 通过前缀和可以还原A数组:A[i] = D[0]+D[1]+...+D[i]
2.2 在本题中的应用
假设时间范围是1~T,我们可以:
- 初始化差分数组D(大小为T+2)
- 对每个折扣时段[l,r,d](d为折扣率):
- D[l] += d
- D[r+1] -= d
- 计算前缀和得到每个时间点的折扣率
- 处理购买记录时直接查表计算
2.3 时间复杂度分析
设:
- m个折扣时段
- n次购买
- T为最大时间
暴力法:O(m*T + n) 差分法:O(m + T + n)
当T很大时(如1e6),差分法优势明显。
3. 完整解题实现(C++)
3.1 数据结构设计
#include <iostream> #include <vector> using namespace std; struct Discount { int l, r; double rate; }; struct Purchase { int time, item, quantity; };3.2 差分数组处理
vector<double> processDiscounts(int max_time, const vector<Discount>& discounts) { vector<double> diff(max_time + 2, 0.0); // 差分数组 // 应用所有折扣时段 for (const auto& d : discounts) { diff[d.l] += d.rate; if (d.r + 1 <= max_time) { diff[d.r + 1] -= d.rate; } } // 计算前缀和得到每个时间点的折扣率 vector<double> rates(max_time + 1, 1.0); // 默认不打折(1.0) double current = 0.0; for (int i = 1; i <= max_time; ++i) { current += diff[i]; rates[i] -= current; // 折扣率 = 1 - 折扣幅度 } return rates; }3.3 主计算逻辑
double calculateTotal( const vector<double>& prices, const vector<double>& discount_rates, const vector<Purchase>& purchases) { double total = 0.0; for (const auto& p : purchases) { if (p.time < 1 || p.time >= discount_rates.size()) { continue; // 无效时间 } double rate = discount_rates[p.time]; total += prices[p.item] * rate * p.quantity; } return total; }4. 关键细节与优化
4.1 边界条件处理
- 时间点从1开始编号(常见于题目设定)
- 折扣率计算时注意是1-rate(如8折应存储0.2)
- 购买时间可能超出折扣时段范围,需要特殊处理
4.2 浮点数精度问题
蓝桥杯比赛中需要注意:
- 避免直接比较浮点数相等
- 输出时按要求保留小数位数
- 可以考虑用整数存储(如8折存80,最后除以100)
改进版本:
// 使用整数存储折扣率(80表示8折) vector<int> processDiscounts(int max_time, const vector<Discount>& discounts) { vector<int> diff(max_time + 2, 0); for (const auto& d : discounts) { diff[d.l] += d.rate; if (d.r + 1 <= max_time) { diff[d.r + 1] -= d.rate; } } vector<int> rates(max_time + 1, 100); // 默认100(原价) int current = 0; for (int i = 1; i <= max_time; ++i) { current += diff[i]; rates[i] = max(0, min(100, 100 - current)); // 确保在0-100之间 } return rates; }4.3 输入输出处理
完整的主函数示例:
int main() { // 假设输入格式: // N M K // price_1 ... price_N // l_1 r_1 rate_1 // ... // l_M r_M rate_M // time_1 item_1 quantity_1 // ... // time_K item_K quantity_K int N, M, K; cin >> N >> M >> K; vector<double> prices(N + 1); // 物品编号1~N for (int i = 1; i <= N; ++i) { cin >> prices[i]; } vector<Discount> discounts(M); int max_time = 0; for (int i = 0; i < M; ++i) { cin >> discounts[i].l >> discounts[i].r >> discounts[i].rate; max_time = max(max_time, discounts[i].r); } vector<Purchase> purchases(K); for (int i = 0; i < K; ++i) { cin >> purchases[i].time >> purchases[i].item >> purchases[i].quantity; max_time = max(max_time, purchases[i].time); } auto rates = processDiscounts(max_time, discounts); double total = calculateTotal(prices, rates, purchases); printf("%.2f\n", total); // 输出保留两位小数 return 0; }5. 测试用例设计
5.1 基础测试用例
输入:
3 2 4 10.0 20.0 30.0 1 3 0.2 4 5 0.1 1 1 2 2 2 1 4 3 3 5 1 1解释:
- 3种商品,价格10/20/30
- 第1-3天全场8折,第4-5天9折
- 购买记录:
- 第1天:商品1买2件
- 第2天:商品2买1件
- 第4天:商品3买3件
- 第5天:商品1买1件
预期输出:
10*0.8*2 + 20*0.8*1 + 30*0.9*3 + 10*0.9*1 = 16 + 16 + 81 + 9 = 122.005.2 边界测试用例
无折扣时段:
2 0 1 15.0 25.0 1 1 1输出:15.00(原价)
折扣时段重叠:
1 2 1 50.0 1 5 0.3 3 7 0.2 3 1 1输出:50*(1-0.3-0.2) = 25.00
6. 蓝桥杯备赛建议
6.1 比赛环境准备
蓝桥杯C++比赛推荐环境:
代码编辑器:VSCode + C/C++插件
- 配置教程可参考"vscode配置c++环境"相关资源
- 安装MinGW作为编译器
调试技巧:
- 使用freopen重定向输入输出
freopen("input.txt", "r", stdin); freopen("output.txt", "w", stdout);- 打印中间变量调试
6.2 算法学习路线
针对蓝桥杯提高组:
- 基础算法:排序、二分、前缀和、差分
- 数据结构:栈/队列、堆、并查集、树状数组
- 动态规划:背包问题、线性DP
- 图论:DFS/BFS、最短路径、最小生成树
6.3 真题训练建议
- 按专题刷题(如先集中做差分数组相关题目)
- 计时模拟比赛环境
- 总结常见题型和解题模板
7. 差分数组的扩展应用
7.1 二维差分
处理矩阵区间增减问题:
// 对矩阵的(x1,y1)到(x2,y2)区域增加val void add(vector<vector<int>>& diff, int x1, int y1, int x2, int y2, int val) { diff[x1][y1] += val; diff[x1][y2+1] -= val; diff[x2+1][y1] -= val; diff[x2+1][y2+1] += val; } // 通过二维前缀和恢复原始矩阵 void compute(vector<vector<int>>& matrix, const vector<vector<int>>& diff) { for (int i = 0; i < matrix.size(); ++i) { for (int j = 0; j < matrix[0].size(); ++j) { matrix[i][j] = diff[i][j]; if (i > 0) matrix[i][j] += matrix[i-1][j]; if (j > 0) matrix[i][j] += matrix[i][j-1]; if (i > 0 && j > 0) matrix[i][j] -= matrix[i-1][j-1]; } } }7.2 时间线差分
处理事件时间轴上的统计问题,如:
- 统计每个时刻在线人数
- 计算会议室使用高峰时段
vector<int> countEvents(int max_time, const vector<pair<int, int>>& intervals) { vector<int> diff(max_time + 2, 0); for (const auto& [s, e] : intervals) { diff[s]++; diff[e+1]--; } vector<int> counts(max_time + 1); int current = 0; for (int i = 0; i <= max_time; ++i) { current += diff[i]; counts[i] = current; } return counts; }8. 性能优化技巧
8.1 输入输出优化
对于大规模数据:
ios::sync_with_stdio(false); cin.tie(nullptr);8.2 内存预分配
对于已知大小的vector:
vector<int> v; v.reserve(1000000); // 避免频繁扩容8.3 内联函数
对于简单频繁调用的函数:
inline int discountRate(int original, int rate) { return original * rate / 100; }9. 常见错误排查
数组越界:
- 差分数组大小应为max_time+2
- 访问前检查时间是否在有效范围内
折扣率计算错误:
- 确认是累加还是累减
- 检查是否正确处理了1-rate
浮点精度问题:
- 使用整数运算替代浮点运算
- 输出时注意四舍五入
时间点混淆:
- 明确题目中时间是从0还是1开始
- 检查区间是闭区间还是左闭右开
10. 类似题目推荐
蓝桥杯历年真题:
- 区间染色问题
- 会议室安排问题
- 公交车乘客统计
LeetCode练习题:
- 航班预订统计
- 得分最高的最小轮调
- 区间加法
AcWing题库:
- 差分矩阵
- 增减序列
- 最高的牛
在实际比赛中,遇到区间修改+单点查询的问题时,差分数组往往是首选解决方案。掌握这个技巧可以大幅提高解题效率,特别是在处理大规模数据时。建议通过大量练习来熟悉差分数组的各种变体应用。