🌈 蓝桥杯 2019 省 A 修改数组 - 数字魔法大冒险 题解
题目链接
题目描述
给定一个数组 A A A,从左到右处理每个元素:
- 若当前数字在之前未出现过,则保留
- 若当前数字已出现过,则不断加1直到找到一个未出现过的数字
最终输出一个没有重复数字的数组
输入格式
- 第一行: n n n(数组长度)
- 第二行: n n n 个整数 a 1 , a 2 , ... , a n a_1, a_2, \dots, a_n a1,a2,...,an
输出格式
修改后的数组(空格分隔)
算法思路
核心思想
使用并查集维护数字占用情况:
- 数字池 :预分配大范围数字空间( 10 6 + 10 5 10^6 + 10^5 106+105)
- 链式管理:每个数字指向下一个可用位置
- 路径压缩 :查询时压扁路径,实现 O ( α ( n ) ) O(\alpha(n)) O(α(n)) 查询
关键步骤
-
初始化并查集:
- 每个数字 i i i 初始指向自己(表示可用)
-
处理每个数字:
- 查询当前数字的根节点(实际可用位置)
- 使用该位置后,将其指向 r o o t + 1 root+1 root+1 的根节点
-
路径压缩:
- 查询过程中更新父节点,减少后续查询时间
复杂度分析
- 时间复杂度 : O ( n α ( M ) ) O(n \alpha(M)) O(nα(M))
- α \alpha α 为反阿克曼函数(接近 O ( 1 ) O(1) O(1))
- M M M 为最大数字范围( 1.2 × 10 6 1.2 \times 10^6 1.2×106)
- 空间复杂度 : O ( M ) O(M) O(M)
代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
const int MAX = 1200000; // 最大数字范围:10^6 + 10^5
vector<int> fa(MAX); // 并查集数组(父节点指针)
// 查找函数(带路径压缩)
int find(int x) {
if (fa[x] != x) {
fa[x] = find(fa[x]); // 路径压缩
}
return fa[x];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
// 初始化并查集
for (int i = 1; i < MAX; i++) {
fa[i] = i; // 每个数字初始可用
}
int n;
cin >> n;
vector<int> res(n); // 结果数组
for (int i = 0; i < n; i++) {
int x;
cin >> x;
// 找到下一个可用位置
int root = find(x);
res[i] = root; // 使用该位置
// 更新指针:指向下一个位置
fa[root] = find(root + 1);
}
// 输出结果
for (int i = 0; i < n; i++) {
if (i) cout << " ";
cout << res[i];
}
return 0;
}
代码解释
关键数据结构
- fa数组 :存储每个数字的下一个可用位置
fa[i] = i:数字 i i i 可用fa[i] > i:数字 i i i 已被使用,下一个可用位置为fa[i]
核心函数
-
find(x) :
cppif (fa[x] != x) fa[x] = find(fa[x]); return fa[x];递归查找并压缩路径,使后续查询 O ( 1 ) O(1) O(1)
处理流程
-
查询可用位置:
cppint root = find(x);获取 x x x 的实际可用位置
-
更新指针:
cppfa[root] = find(root + 1);将当前数字指向下一个可用位置,形成可用链
算法正确性证明
数字分配原理
设数字池 S = { 1 , 2 , 3 , ... , M } S = \{1, 2, 3, \dots, M\} S={1,2,3,...,M}
- 初始状态 : f a i = i fai=i fai=i,所有数字可用
- 使用数字 x x x :
- 分配 r o o t = find ( x ) root = \text{find}(x) root=find(x)
- 更新 f a r o o t = find ( r o o t + 1 ) faroot = \text{find}(root+1) faroot=find(root+1)
- 链式保证 :
- 每个被使用的数字指向下一个可用位置
- find ( x ) \text{find}(x) find(x) 总是返回最小可用数字 ≥ x \geq x ≥x
路径压缩效果
操作序列:
| 操作 | fa1 | fa2 | fa3 | fa4 | 说明 |
|---|---|---|---|---|---|
| 初始 | 1 | 2 | 3 | 4 | |
| find(1) | 1→2 | 2 | 3 | 4 | 使用1,指向find(2)=2 |
| find(1) | 2 | 2 | 3 | 4 | 路径压缩后直接返回2 |
| find(2) | 2 | 2→3 | 3 | 4 | 使用2,指向find(3)=3 |
| find(1) | 3 | 3 | 3 | 4 | 路径压缩后直接返回3 |
测试样例
输入 #1
data
5
2 1 1 3 4
输出 #1
result
2 1 3 4 5
解释:
- 2:首次出现 → 使用2
- 1:首次出现 → 使用1
- 1:已出现 → 尝试2(已用)→ 使用3
- 3:首次出现 → 使用4(因为3已被使用)
- 4:首次出现 → 使用5(因为4已被使用)
输入 #2
data
6
3 3 3 3 3 3
输出 #2
result
3 4 5 6 7 8
解释:连续6个3,依次使用3,4,5,6,7,8
复杂度证明
时间复杂度
设 M M M 为最大数字范围:
- 并查集初始化: O ( M ) O(M) O(M)
- n n n 次查询:每次 O ( α ( M ) ) O(\alpha(M)) O(α(M))
- 总时间: O ( M + n α ( M ) ) ≈ O ( n ) O(M + n\alpha(M)) \approx O(n) O(M+nα(M))≈O(n)(实际接近线性)
空间复杂度
- 存储并查集: O ( M ) O(M) O(M)
- 结果数组: O ( n ) O(n) O(n)
- 总空间: O ( M ) O(M) O(M)
拓展思考
- 动态扩容:支持更大的数字范围
- 删除操作:支持数字释放
- 范围限制:设置最大可用数字上限
相似题目
总结
通过并查集的巧妙应用,我们实现了:
- 从暴力O(n²)到高效O(n)的飞跃
- 10⁵数据量轻松处理
- 清晰简洁的代码逻辑
小明终于可以高效处理大数组啦!🎉