1. 题目背景与理解
这道题目来自IOI 2005国际信息学奥林匹克竞赛,题目编号P5847,题目名为"mea"。作为一道经典的竞赛题目,它考察了选手对序列处理、数学推理和算法优化的综合能力。
题目大意是:给定一个整数序列M,要求构造一个整数序列S,使得对于每个i,M[i]等于S[1..i]的中位数。中位数定义为排序后位于中间位置的数(对于偶数长度取较小的那个)。我们需要找到所有可能的S序列中,字典序最小的那个。
注意:在信奥竞赛中,理解题目定义非常关键。这里的中位数定义与数学上的严格定义一致,即对于奇数长度取中间值,偶数长度取中间两个数中较小的那个。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路分析
2.1 问题转化
首先我们需要明确几个关键点:
- 序列S的长度与M相同
- 每个M[i]是S[1..i]的中位数
- 在所有满足条件的S中,选择字典序最小的
这个问题可以转化为:如何根据中位数序列M,逆向推导出原始序列S。由于要求字典序最小,我们需要在每一步选择尽可能小的合法数字。
2.2 关键观察
通过分析,我们可以得到以下重要性质:
- S[1]必须等于M[1],因为单个元素的中位数就是它本身
- 对于i>1,S[i]的值会影响前i个元素的中位数
- 中位数的变化只能是逐步递增或保持不变的
2.3 算法选择
基于这些观察,我们可以采用贪心算法:
- 初始化S[1] = M[1]
- 对于每个后续位置i,选择能满足M[i]是中位数的最小可能值
- 维护当前序列的中位数性质
3. C++实现详解
3.1 数据结构选择
为了高效维护和查询中位数,我们需要合适的数据结构。常见的选择有:
- 两个堆(最大堆和最小堆)
- 平衡二叉搜索树
- C++中的multiset(基于红黑树实现)
这里我们选择使用两个multiset来分别存储较小的一半和较大的一半数字,这样可以高效地获取和更新中位数。
cpp复制#include <iostream>
#include <vector>
#include <set>
using namespace std;
vector<int> solve(const vector<int>& M) {
int n = M.size();
vector<int> S(n);
multiset<int> left, right;
S[0] = M[0];
left.insert(S[0]);
for (int i = 1; i < n; ++i) {
// 维护左右两个multiset的大小关系
if (left.size() > right.size()) {
right.insert(*left.rbegin());
left.erase(--left.end());
}
// 尝试设置S[i]为可能的最小值
int candidate = M[i];
if (candidate > *left.rbegin()) {
right.insert(candidate);
} else {
left.insert(candidate);
}
// 调整使left的大小等于i+1的一半(向上取整)
while (left.size() > (i+2)/2) {
right.insert(*left.rbegin());
left.erase(--left.end());
}
while (left.size() < (i+2)/2) {
left.insert(*right.begin());
right.erase(right.begin());
}
S[i] = *left.rbegin(); // 当前中位数
}
return S;
}
3.2 算法复杂度分析
- 时间复杂度:O(n log n),因为每个元素最多被插入、删除和移动3次,每次操作都是O(log n)
- 空间复杂度:O(n),用于存储两个multiset
4. 代码优化与边界处理
4.1 输入输出处理
在实际竞赛中,输入输出处理也很重要。我们可以优化IO速度:
cpp复制int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> M(n);
for (int i = 0; i < n; ++i) {
cin >> M[i];
}
vector<int> S = solve(M);
for (int num : S) {
cout << num << " ";
}
cout << endl;
return 0;
}
4.2 边界情况测试
需要考虑的特殊情况包括:
- 单元素序列
- 严格递增序列
- 严格递减序列
- 所有元素相同的序列
- 大随机测试用例
5. 竞赛技巧与经验分享
5.1 调试技巧
在竞赛中调试这类题目时,可以:
- 打印中间结果,观察multiset的状态
- 对小样例手动计算验证
- 使用assert检查不变量
cpp复制// 调试代码示例
void debugPrint(const multiset<int>& left, const multiset<int>& right) {
cout << "Left: ";
for (int x : left) cout << x << " ";
cout << "\nRight: ";
for (int x : right) cout << x << " ";
cout << endl;
}
5.2 性能优化
如果遇到时间限制严格的情况,可以考虑:
- 使用数组代替vector
- 手写平衡树替代STL容器
- 预先分配内存
5.3 常见错误
在实现这类题目时容易犯的错误:
- 没有正确处理偶数长度序列的中位数定义
- 在multiset中删除元素时使用了错误的迭代器
- 没有及时调整两个multiset的大小关系
- 忽略了字典序最小的要求
6. 算法扩展与变种
6.1 滑动窗口中位数
这个问题可以扩展到滑动窗口中位数问题,使用类似的双堆技术:
cpp复制vector<double> medianSlidingWindow(vector<int>& nums, int k) {
multiset<int> window(nums.begin(), nums.begin() + k);
auto mid = next(window.begin(), k / 2);
vector<double> medians;
for (int i = k; ; ++i) {
// 计算当前中位数
medians.push_back((double(*mid) + *prev(mid, 1 - k%2)) / 2.0);
if (i == nums.size()) break;
// 插入新元素
window.insert(nums[i]);
if (nums[i] < *mid) --mid;
// 删除旧元素
if (nums[i-k] <= *mid) ++mid;
window.erase(window.lower_bound(nums[i-k]));
}
return medians;
}
6.2 动态中位数查询
如果需要支持动态插入和查询中位数,可以维护两个堆:
cpp复制class MedianFinder {
priority_queue<int> left; // 最大堆
priority_queue<int, vector<int>, greater<int>> right; // 最小堆
public:
void addNum(int num) {
if (left.empty() || num <= left.top()) {
left.push(num);
} else {
right.push(num);
}
// 平衡两个堆的大小
if (left.size() > right.size() + 1) {
right.push(left.top());
left.pop();
} else if (right.size() > left.size()) {
left.push(right.top());
right.pop();
}
}
double findMedian() {
if (left.size() == right.size()) {
return (left.top() + right.top()) / 2.0;
} else {
return left.top();
}
}
};
7. 信奥刷题建议
7.1 刷题策略
- 按专题刷题:先掌握基础算法,再攻克难题
- 重视题目分析:理解题目背后的数学模型
- 多做模拟赛:适应竞赛环境和时间压力
7.2 学习资源推荐
- 《算法竞赛入门经典》- 刘汝佳
- OI Wiki:全面的算法竞赛知识库
- Codeforces、AtCoder等在线评测平台
7.3 调试与验证
在实现这类算法时,建议:
- 先在小样例上手动计算验证
- 编写暴力解法作为对拍程序
- 使用assert检查关键不变量
cpp复制// 对拍程序示例
vector<int> bruteForce(const vector<int>& M) {
int n = M.size();
vector<int> S(n);
S[0] = M[0];
for (int i = 1; i < n; ++i) {
S[i] = 1; // 从最小可能开始尝试
while (true) {
vector<int> prefix(S.begin(), S.begin() + i + 1);
sort(prefix.begin(), prefix.end());
int median = prefix[i/2];
if (median == M[i]) break;
S[i]++;
}
}
return S;
}
在实际刷题过程中,我发现这类中位数相关的问题往往需要耐心调试和细致分析。特别是在处理multiset的迭代器时,很容易因为边界条件而出错。建议在实现时多写测试用例,特别是针对边界情况的测试。
