1. 项目概述:信奥刷题与P3939题目解析
最近在洛谷刷信奥题时遇到了P3939"数颜色"这道经典题目,发现它很好地考察了基础算法和C++容器的灵活运用。这道题看似简单,但想要写出高效解法需要掌握vector和map的组合使用技巧。作为从零开始刷信奥题的过来人,我想分享下这道题的完整解题思路和优化过程。
信奥竞赛(信息学奥林匹克)对算法效率和代码规范有着严格要求,P3939正是典型的训练题。题目大意是:给定一个颜色序列,需要快速查询某个区间内特定颜色的出现次数,同时还要支持动态修改某个位置的颜色值。这种"查询+修改"的组合操作在信奥题中非常常见,也是区分新手和熟练选手的重要标志。
2. 题目分析与核心算法选择
2.1 题目需求拆解
P3939的核心需求可以分解为两个操作:
- 查询操作:给定区间[l,r]和颜色c,统计该区间内c的出现次数
- 修改操作:将位置x的颜色改为y
最直观的暴力解法是:
- 查询:遍历区间[l,r],逐个比较计数
- 修改:直接修改数组对应位置
但这种解法在数据量大时(比如n=3e5)会超时,因为查询时间复杂度是O(n),无法通过信奥的时间限制。
2.2 高效解法设计
经过分析,我们需要将查询时间复杂度降到O(logn)级别。这里可以采用"颜色到位置索引"的逆向思维:
-
使用map<int, vector
>数据结构: - key存储颜色值
- value存储该颜色出现的所有位置(保持升序)
-
查询操作转化为:
- 在对应颜色的vector中,用二分查找统计处于[l,r]范围内的元素个数
- 即:upper_bound(r) - lower_bound(l)
-
修改操作需要:
- 在原颜色的vector中删除x
- 在新颜色的vector中插入x(保持有序)
这种设计将查询复杂度降到了O(logn),修改复杂度为O(n)(因为vector插入删除是O(n))。虽然修改不是最优,但在信奥题的数据范围下已经足够。
3. C++实现详解与核心代码
3.1 数据结构初始化
cpp复制#include <bits/stdc++.h>
using namespace std;
map<int, vector<int>> color_map; // 颜色到位置列表的映射
vector<int> colors; // 原始颜色数组(按题目要求从1开始索引)
void init(int n) {
colors.resize(n + 1); // 题目中位置从1开始
for (int i = 1; i <= n; ++i) {
cin >> colors[i];
color_map[colors[i]].push_back(i);
}
// 对各颜色位置列表排序(题目不保证输入有序)
for (auto &[c, vec] : color_map) {
sort(vec.begin(), vec.end());
}
}
3.2 查询操作实现
cpp复制int query(int l, int r, int c) {
if (!color_map.count(c)) return 0;
auto &vec = color_map[c];
// 使用lower_bound和upper_bound进行范围统计
auto left = lower_bound(vec.begin(), vec.end(), l);
auto right = upper_bound(vec.begin(), vec.end(), r);
return right - left;
}
3.3 修改操作实现
cpp复制void modify(int x, int y) {
int old_color = colors[x];
// 从旧颜色列表中删除x(二分查找位置)
auto &old_vec = color_map[old_color];
auto it = lower_bound(old_vec.begin(), old_vec.end(), x);
if (it != old_vec.end() && *it == x) {
old_vec.erase(it);
}
// 向新颜色列表插入x(保持有序)
colors[x] = y;
auto &new_vec = color_map[y];
it = lower_bound(new_vec.begin(), new_vec.end(), x);
new_vec.insert(it, x);
}
4. 性能优化与边界处理
4.1 输入输出加速
信奥题必须注意IO效率:
cpp复制ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
4.2 特殊情况处理
-
颜色不存在时:
- query中直接判断color_map.count(c)
-
重复修改相同颜色:
- 可以添加判断 if(old_color == y) return;
-
空vector情况:
- erase前检查it有效性
4.3 更优解法的思考
虽然上述解法能AC,但还可以优化:
- 修改操作可以用平衡树替代vector,将复杂度降到O(logn)
- 使用unordered_map + vector组合,减少map的log开销
- 分块处理法,将复杂度均摊到O(sqrt(n))
5. 完整AC代码与测试用例
cpp复制#include <bits/stdc++.h>
using namespace std;
map<int, vector<int>> color_map;
vector<int> colors;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
colors.resize(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> colors[i];
color_map[colors[i]].push_back(i);
}
// 预处理排序
for (auto &[c, vec] : color_map) {
sort(vec.begin(), vec.end());
}
while (m--) {
int op, l, r, c, x, y;
cin >> op;
if (op == 1) {
cin >> x >> y;
if (colors[x] == y) continue;
// 删除旧位置
int old = colors[x];
auto &v_old = color_map[old];
auto it = lower_bound(v_old.begin(), v_old.end(), x);
if (it != v_old.end() && *it == x) {
v_old.erase(it);
}
// 添加新位置
colors[x] = y;
auto &v_new = color_map[y];
it = lower_bound(v_new.begin(), v_new.end(), x);
v_new.insert(it, x);
} else {
cin >> l >> r >> c;
if (!color_map.count(c)) {
cout << 0 << '\n';
continue;
}
auto &vec = color_map[c];
auto left = lower_bound(vec.begin(), vec.end(), l);
auto right = upper_bound(vec.begin(), vec.end(), r);
cout << right - left << '\n';
}
}
return 0;
}
测试用例:
code复制输入:
6 5
1 2 3 2 1 3
2 1 6 2
1 3 2
2 1 6 2
1 2 1
2 1 6 1
输出:
2
3
3
6. 刷题经验与技巧分享
6.1 信奥刷题必备工具链
-
代码编辑器配置:
- VSCode + C/C++插件
- 配置Clangd实现代码提示
- 安装Competitive Programming Helper插件
-
调试技巧:
- 使用assert进行边界检查
- 编写随机数据生成器对拍
- 在本地测试极限数据(n=3e5)
6.2 常见错误排查
-
段错误(Segmentation Fault):
- 检查数组是否越界
- vector是否resize足够大小
- 题目通常从1开始索引
-
时间超限(TLE):
- 检查算法复杂度是否合理
- 避免在循环内使用cin/cout
- 使用快读快写优化
-
答案错误(WA):
- 仔细阅读题目描述
- 检查边界条件(l==r时)
- 输出格式是否符合要求
6.3 进阶学习建议
-
数据结构延伸:
- 学习使用pb_ds库中的平衡树
- 掌握线段树解法
- 了解分块算法的应用
-
相关题目推荐:
- P1972 [SDOI2009] HH的项链
- P1903 [国家集训队] 数颜色 / 维护队列
- P4113 [HEOI2012] 采花
-
书籍推荐:
- 《算法竞赛进阶指南》
- 《挑战程序设计竞赛》
- 《C++ Primer》查漏补缺
在实际刷题过程中,建议建立自己的代码模板库,将常用算法封装成可复用的函数。比如二分查找、快速IO等可以预先准备好,比赛时直接调用。同时要养成写注释的习惯,方便debug时快速定位问题。
