题目跳转链接:[蓝桥杯 2019 省 A] 修改数组 - 洛谷
虚假的题解:
题目内容就不复述了,大家可以点击上面的连接去查看;主要想分享一下这道题的解法确实很巧妙;做的时候一看题目描述很简单,上来两三下就写完,结果很意料之中啊,果然只得了80分:
附上80分代码:
#include <bits/stdc++.h>
using namespace std;
#define N 100010
#define ll long long
int main()
{
int n;
cin >> n;
vector<int> num(n);
for (int i = 0; i < n; ++i)
{
cin >> num[i];
}
unordered_map<int, int> mp;
for (int i = 0; i < n; ++i)
{
while (mp.find(num[i]) != mp.end())
{
num[i]++;
}
mp[num[i]]++;
}
for (int i = 0; i < n; ++i)
{
cout << num[i] << " ";
}
return 0;
}
做之前确实想到肯定要卡时间复杂度的,所以偷偷看了一眼提示写到了“并查集”,但是感觉并查集的时间复杂度也会超时,不过根据提示还是让GPT帮忙生成了一个并查集的模板,套了一下发现依然80,也是意料之中:
附上并查集80分模板:
#include <bits/stdc++.h>
using namespace std;
#define N 100010
#define ll long long
class UnionFind
{
private:
vector<int> parent; // 存储每个元素的父节点
vector<int> rank; // 存储树的深度,用于优化
public:
// 构造函数,初始化并查集
UnionFind(int size) : parent(size), rank(size, 0)
{
for (int i = 0; i < size; ++i)
{
parent[i] = i; // 初始时,每个元素的父节点是它自己
}
}
// 查找操作,带路径压缩
int find(int x)
{
if (x != parent[x])
{
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
// 合并操作
bool unionSets(int x, int y)
{
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY)
{
return false; // 已经是同一棵树
}
// 根据树的深度来合并,使树保持较平衡
if (rank[rootX] > rank[rootY])
{
parent[rootY] = rootX;
}
else if (rank[rootX] < rank[rootY])
{
parent[rootX] = rootY;
}
else
{
parent[rootY] = rootX;
rank[rootX]++; // 如果两棵树的深度相同,就将其中一个的深度加1
}
return true;
}
};
int main()
{
int n;
cin >> n;
vector<int> num(n);
UnionFind u(N);
cin >> num[0];
for (int i = 1; i < n; ++i)
{
cin >> num[i];
while (u.find(num[i]) == u.find(num[i - 1]))
{
num[i]++;
}
u.unionSets(num[i - 1], num[i]);
}
for (int temp : num)
{
cout << temp << " ";
}
return 0;
}
最后楼主直接前去看题解,直呼“妙啊妙啊”,最后说一下看了题解之后楼主捋清的思路:
真正题解:
首先初始化都差不多,就是所有元素最开始都是指向自己的,也就是自己的父亲序号就是自己的值,f[i]==i;,然后当我们输入了一个元素之后,我们将这个元素的父亲序号加1,每次只输出输入节点的父亲序号。乍一看很绕口,这句话怎么进一步理解呢,假如我们第一个输入的元素的4, 那么正如上面所说的,我们输出4的父亲序号(因为是第一个元素,所以4的父亲序号就是自己,也就是4),那么进行输出操作后,输出完没结束呢,我们要将4的父亲序号从4变成5,为什么要这么加1呢,因为如果后面我们遇到的输入还是4的话,我们要做的是不是也是跟上面一样输出4的父亲序号,此时这个序号因为变成了5,所以我们就会输出5了。看到这肯定有人有疑问了,那万一之前5也输入过了呢?那么我们不妨来想一下,如果在输入第二个4之前输入过5,那么5的父亲节点是不是又变成6了,这时候我们再输入第二个4,会发现find(4)输出的是6,为什么呢?仔细观察find函数的终止条件是f[i]==i,也就是说此时find(4)输出的不会是5,因为f[5]已经不等于5了,变成6了,那么find函数会继续从5递归,然后递归到6发现f[6]==6,那么他就会返回6,意思也就是4的父亲序号是6,所以目前的父子关系是4->5->6,然后我们输出6之后还要记得将6的父亲序号变成7,这样关系变成4->5->6->7,因为之前通过输入454,我们得到的数组是456了,所以下一次如果有输入是4或者5或者6的话,就会根据父子关系输出7,达到不重复的作用~;
题解代码:
#include <bits/stdc++.h>
using namespace std;
#define N 100010
#define ll long long
int f[N];
int find(int num)
{
if (num != f[num])
{
f[num] = find(f[num]);
}
return f[num];
}
int main()
{
int n;
cin >> n;
vector<int> num(n);
for (int i = 0; i < N; ++i)
{
f[i] = i;
}
for (int i = 0; i < n; ++i)
{
cin >> num[i];
cout << find(num[i]) << " ";
f[find(num[i])]++;
}
return 0;
}
最后提醒一点吧,就是并查集f数组的大小是N也就是1000010,这个是根据题目描述来的,题目中输入的最大值是1000000,所以我们尽量开的比这个大一点,千万不要搞错了,开成了n(数组数量),这样会报错的哦~~~,因为一开始初始化每个数的爹都是自己,所以我输入一个99999,那肯定他的爹得是99999呀~
本题的收获:
本题利用的并查集的特性还是比较巧妙地,相信很多人也是第一次遇见,平时我们用并查集其实还是以合并和查找为主,本题没有用到合并,还是利用并查集中的父子关系,通过寻找父子关系的特性,将目前可以输出的数当成最终的父亲,输出后就将父亲加1。本题乍一看其实和哈希映射还有点像,学过数据结构的小伙伴可能会有点印象,在学哈希冲突的时候有类似的案例,其实本题楼主最开始想的时候考虑过用一个数组f[i]记录当输入为i时下一个不重复的元素,不过问题就是例如输入了4和5,f[i]此时变成了5,但是5已经被输入过了,不过这时候应该可以通过循环输出f[f[5]]也就是6实现,其实也有点类似本题中并查集的作用,本题中使用并查集倒是更方便一些~