1. 项目概述:信奥刷题与P3939题解实战
信奥赛(信息学奥林匹克竞赛)是检验算法与编程能力的重要赛事,而刷题是提升竞赛水平的必经之路。今天我们要拆解的是洛谷P3939「数颜色」这道经典题目,它考察了对数据结构的基础应用能力。这道题在信奥赛初赛阶段具有典型性,能帮助选手巩固数组操作和基础算法的理解。
选择C++实现是因为它是信奥赛的官方指定语言,兼具执行效率与语法灵活性。在实际比赛中,C++的STL库往往能大幅减少编码时间,而这道题恰好能体现vector和map等容器的实用价值。通过这道题,新手可以学习如何将实际问题转化为程序逻辑,而有经验的选手则能优化解法效率。
提示:信奥赛题目往往有严格的时空限制,建议在本地IDE(如VS Code或Dev-C++)调试通过后,再提交到洛谷平台验证。
2. 题目分析与核心思路
2.1 题目需求拆解
P3939的题意可简化为:给定一个颜色序列,需要支持两种操作:
- 查询区间[L,R]内特定颜色C的出现次数
- 交换相邻两个位置的颜色值
样例输入:
code复制6 5
1 2 3 2 3 3
1 1 3 2
1 4 6 3
2 3
1 1 3 2
1 4 6 3
对应输出:
code复制1
3
0
2
2.2 算法选择依据
最直观的暴力解法是每次查询时遍历区间统计颜色,但时间复杂度为O(Q*N),在数据量较大时(N,Q≤3e5)必然超时。高效解法需要预处理颜色位置信息:
- 哈希表+二分查找:用unordered_map记录每个颜色出现的所有位置(保持有序),查询时通过二分查找定位区间边界。交换操作只需调整相邻元素的存储位置。
- 分块处理:将序列分成√n大小的块,维护每个块的颜色统计信息。适合修改操作频繁的场景。
实测中,方法1在随机数据下表现更优,平均时间复杂度为O(Q log K),其中K是颜色出现次数。下面重点讲解这种实现。
3. C++实现详解
3.1 数据结构设计
cpp复制#include <bits/stdc++.h>
using namespace std;
unordered_map<int, vector<int>> colorPos; // 颜色->位置集合
vector<int> arr; // 原始数组
这里选用unordered_map而非map,因为:
- 不需要按键排序(节省O(log n)时间)
- 平均访问复杂度O(1)
- 配合vector存储位置,内存连续访问更快
3.2 核心操作实现
查询操作伪代码:
code复制function query(L, R, C):
if C not in colorPos: return 0
positions = colorPos[C]
left = lower_bound(positions, L)
right = upper_bound(positions, R)
return right - left
交换操作实现要点:
cpp复制void swapAdjacent(int x) {
if(x >= arr.size()) return;
int a = arr[x-1], b = arr[x];
if(a == b) return; // 相同颜色无需处理
// 在各自颜色集合中查找位置
auto &va = colorPos[a];
auto &vb = colorPos[b];
auto ita = lower_bound(va.begin(), va.end(), x-1);
auto itb = lower_bound(vb.begin(), vb.end(), x);
*ita += 1; // 前一个元素位置+1
*itb -= 1; // 后一个元素位置-1
std::swap(arr[x-1], arr[x]);
}
3.3 完整AC代码
cpp复制#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
arr.resize(n);
for(int i=0; i<n; ++i) {
cin >> arr[i];
colorPos[arr[i]].push_back(i+1); // 位置从1开始记录
}
while(q--) {
int op;
cin >> op;
if(op == 1) {
int l, r, c;
cin >> l >> r >> c;
if(!colorPos.count(c)) {
cout << "0\n";
continue;
}
auto& vec = colorPos[c];
int left = lower_bound(vec.begin(), vec.end(), l) - vec.begin();
int right = upper_bound(vec.begin(), vec.end(), r) - vec.begin();
cout << right - left << "\n";
} else {
int x;
cin >> x;
if(x >= arr.size() || arr[x-1] == arr[x]) continue;
int &a = arr[x-1], &b = arr[x];
auto &va = colorPos[a], &vb = colorPos[b];
auto ita = lower_bound(va.begin(), va.end(), x);
auto itb = lower_bound(vb.begin(), vb.end(), x+1);
*ita = x+1;
*itb = x;
swap(a, b);
}
}
return 0;
}
4. 关键优化与调试技巧
4.1 输入输出加速
信奥赛对IO效率要求严格,必须添加:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
这可以关闭C++与C的IO同步,使cin/cout达到与scanf/printf相近的速度。
4.2 边界条件处理
常见坑点包括:
- 位置索引从0还是1开始(题目通常说明)
- 交换操作中x=n的情况(越界检查)
- 相同颜色交换时的冗余操作
4.3 测试用例设计
建议自测用例:
code复制3 3
1 1 1
1 1 3 1 // 全区间查询
2 2 // 无效交换
1 2 2 1 // 单点查询
应输出:
code复制3
1
5. 同类题目拓展
掌握本解法后,可尝试以下进阶题目:
- P3740 分组统计(需要维护多维度信息)
- P5357 前缀统计(AC自动机应用)
- P3374 树状数组模板题(练习更高效的区间查询)
对于想系统提升的同学,建议按专题刷题:
- 线性结构:数组、链表、栈、队列
- 树形结构:二叉树、堆、并查集
- 图论:最短路、最小生成树
- 动态规划:背包、区间DP
经验分享:在VS Code中配置Competitive Programming Helper插件,可以快速测试样例数据。建立自己的代码模板库,将常用算法(如二分查找、快速排序)封装成可复用的函数。
