蓝桥杯备赛复盘:我是如何用C++在2020年B组省赛中“卡”在平面切分与子串分值上的?
蓝桥杯省赛复盘:从平面切分与子串分值的解题误区到算法思维突破
去年参加蓝桥杯省赛时,我本以为凭借几个月的刷题准备能够轻松应对,却在两道中等难度题目上栽了跟头——平面切分和子串分值。这两道题看似常规,却暗藏玄机。今天我将用4000字详细拆解当时的思维误区,并分享最终找到的优化解法。这不是一篇标准题解,而是一个真实参赛者的思考轨迹记录。
1. 平面切分:被忽略的集合去重陷阱
当我看到题目描述"计算N条直线将平面分成的部分数"时,第一反应是套用公式:n条直线最多将平面划分为(n²+n+2)/2部分。于是迅速写下了这样的代码:
int countPartitions(int n) {
return (n*n + n + 2)/2;
}
提交后只通过了30%的测试用例。这时才意识到问题没那么简单——题目中的直线可能存在 平行或重合 的情况,而上述公式仅适用于所有直线两两相交且无三线共点的理想情况。
1.1 正确的解决思路
实际应该采用增量法:每新增一条直线,计算它与已有直线的 有效交点数量k ,则新增区域数为k+1。关键点在于:
- 使用set存储直线参数(A,B)进行去重
- 使用set存储交点坐标实现自动去重
优化后的核心代码结构:
set<pair<double, double>> lines; // 存储唯一直线
set<pair<double, double>> points; // 存储唯一交点
for (auto newLine : inputLines) {
if (lines.count(newLine)) continue; // 忽略重合直线
points.clear();
for (auto oldLine : lines) {
if (parallel(newLine, oldLine)) continue; // 平行线无交点
auto point = intersection(newLine, oldLine);
points.insert(point); // 自动去重
}
regions += points.size() + 1;
lines.insert(newLine);
}
1.2 易错点分析
-
浮点数精度问题 :直接比较
double类型可能导致误判,需要自定义比较函数bool isEqual(double a, double b) { return fabs(a - b) < 1e-8; } -
边界情况处理 :
- 零条直线时平面区域数为1
- 全部直线平行时区域数为n+1
- 存在多条直线重合的情况
-
时间复杂度优化 :使用STL的set虽然方便,但对于n=1000的情况,O(n²)的复杂度需要约1e6次操作,在蓝桥杯环境中仍可接受
2. 子串分值:从暴力枚举到贡献度分析
题目要求计算字符串所有子串的不同字符数之和。我的第一版解法是典型的暴力枚举:
int result = 0;
for (int i = 0; i < s.length(); i++) {
set<char> distinct;
for (int j = i; j < s.length(); j++) {
distinct.insert(s[j]);
result += distinct.size();
}
}
这个O(n²)的解法在小数据量尚可,但当n=1e5时完全无法通过。赛后研究才发现需要 贡献度思维 ——不是计算每个子串的独特字符数,而是计算每个字符在多少个子串中作为唯一出现。
2.1 贡献度法的关键思路
对于字符s[i],找出其左右边界:
- 左侧第一个相同字符位置left
- 右侧第一个相同字符位置right
则该字符的贡献度为:(i - left) × (right - i)
以字符串"ababc"为例:
- 第二个'a'的左右边界分别为0和∞,贡献(2-0)×(∞-2)
- 第一个'b'的左右边界分别为-∞和3,贡献(1-(-∞))×(3-1)≈2×2=4
实际实现时需要预处理每个字符的位置:
vector<int> pos[26]; // 记录各字符出现位置
long long ans = 0;
for (int i = 0; i < n; i++) {
int c = s[i] - 'a';
int left = pos[c].empty() ? -1 : pos[c].back();
int right = n; // 默认右边界为字符串末尾
// 在pos[c]中二分查找i的后继位置
auto it = upper_bound(pos[c].begin(), pos[c].end(), i);
if (it != pos[c].end()) right = *it;
ans += (i - left) * (right - i);
pos[c].push_back(i);
}
2.2 算法优化对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用数据规模 |
|---|---|---|---|
| 暴力枚举 | O(n²) | O(n) | n ≤ 1e3 |
| 贡献度法 | O(n) | O(n) | n ≤ 1e6 |
3. 调试技巧:我在赛场上的救命稻草
当标准解法卡壳时,这些调试方法帮我拿到了部分分数:
-
小数据验证法 :先手工计算n=1,2,3的情况,验证程序输出
- 平面切分:1条直线→2区域;2条相交→4区域;3条两两相交→7区域
- 子串分值:"a"→1;"ab"→3;"aa"→2
-
极限测试法 :
// 平面切分的极端情况测试 vector<pair<int,int>> lines = { {1,1}, {1,1}, {1,2} // 两条重合+一条平行 }; assert(calculateRegions(lines) == 3); -
输出中间结果 :
// 在子串分值计算中输出前100个结果 for (int i = 0; i < min(100, n); i++) { cout << "i=" << i << " contribution=" << contribution[i] << endl; }
4. 从失败中提炼的备赛建议
-
基础公式的适用条件 :所有数学公式都有其前提假设,套用前必须验证条件是否满足
-
复杂度预判训练 :看到题目先估算最大数据规模,快速判断算法可行性
- 1e3 → O(n²)可行
- 1e5 → 需要O(nlogn)或更好
- 1e6 → 通常需要O(n)
-
STL的熟练运用 :
set用于自动排序和去重unordered_set用于快速查找lower_bound/upper_bound用于高效二分搜索
-
贡献度思维培养 :许多统计类问题都可以转化为单个元素的贡献计算
-
调试模板准备 :提前准备好常用调试代码片段,如:
#define DEBUG #ifdef DEBUG #define debug(...) printf(__VA_ARGS__) #else #define debug(...) #endif
这次失利让我明白,算法竞赛不仅是知识储备的比拼,更是思维灵活性的较量。现在每次练习时,我都会问自己三个问题:这个解法的最坏复杂度是多少?是否有更本质的计算方式?边界情况是否考虑周全?这种思维方式不仅在比赛中,在实际工程中也让我受益匪浅。
更多推荐



所有评论(0)