P8686 [蓝桥杯 2019 省 A] 修改数组 - 数字魔法大冒险 题解

🌈 蓝桥杯 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

输出格式

修改后的数组(空格分隔)

算法思路

核心思想

使用并查集维护数字占用情况:

  1. 数字池 :预分配大范围数字空间( 10 6 + 10 5 10^6 + 10^5 106+105)
  2. 链式管理:每个数字指向下一个可用位置
  3. 路径压缩 :查询时压扁路径,实现 O ( α ( n ) ) O(\alpha(n)) O(α(n)) 查询

关键步骤

  1. 初始化并查集

    • 每个数字 i i i 初始指向自己(表示可用)
  2. 处理每个数字

    • 查询当前数字的根节点(实际可用位置)
    • 使用该位置后,将其指向 r o o t + 1 root+1 root+1 的根节点
  3. 路径压缩

    • 查询过程中更新父节点,减少后续查询时间

复杂度分析

  • 时间复杂度 : 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)

    cpp 复制代码
    if (fa[x] != x) fa[x] = find(fa[x]);
    return fa[x];

    递归查找并压缩路径,使后续查询 O ( 1 ) O(1) O(1)

处理流程

  1. 查询可用位置

    cpp 复制代码
    int root = find(x);

    获取 x x x 的实际可用位置

  2. 更新指针

    cpp 复制代码
    fa[root] = find(root + 1);

    将当前数字指向下一个可用位置,形成可用链

算法正确性证明

数字分配原理

设数字池 S = { 1 , 2 , 3 , ... , M } S = \{1, 2, 3, \dots, M\} S={1,2,3,...,M}

  1. 初始状态 : f a i = i fai=i fai=i,所有数字可用
  2. 使用数字 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)
  3. 链式保证
    • 每个被使用的数字指向下一个可用位置
    • 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)

拓展思考

  1. 动态扩容:支持更大的数字范围
  2. 删除操作:支持数字释放
  3. 范围限制:设置最大可用数字上限

相似题目

总结

通过并查集的巧妙应用,我们实现了:

  • 从暴力O(n²)到高效O(n)的飞跃
  • 10⁵数据量轻松处理
  • 清晰简洁的代码逻辑
    小明终于可以高效处理大数组啦!🎉
相关推荐
小星星闪亮登场1 小时前
Codeforces Round 1115 ( Div. 2)
数据结构·c++·算法·贪心算法·排序算法·codeforces
计算机小白一个1 小时前
蓝桥杯 Java B 组之哈希表应用(两数之和、重复元素判断)
java·数据结构·算法·蓝桥杯
Loge编程生活1 小时前
打卡信奥刷题(3449)用C++实现信奥题 P10429 [蓝桥杯 2024 省 B] 拔河
开发语言·数据结构·c++·算法·青少年编程
2601_957174653 小时前
大模型算法 简单 容易 用这个方法
算法
VALENIAN瓦伦尼安教学设备3 小时前
激光对中仪采购要点
数据库·嵌入式硬件·算法
lilili也4 小时前
visual studio add matlab
c语言·c++
沫璃染墨4 小时前
《Qt从零入门系列(一):Qt框架初识——从发展历史到跨平台开发》
c++·qt·系统架构·gui
Forever Nore4 小时前
LeetCode 1 两数之和
算法·leetcode·职场和发展
To_OC5 小时前
LC 3 无重复字符的最长子串:从入门滑动窗口到优化写法,再也不怕面试官追问
javascript·算法·leetcode