2021年寒假每日一题,2017~2019年的省赛真题。本文内容由倪文迪(华东理工大学计算机系软件192班)和罗勇军老师提供。每日一题,关注蓝桥杯专栏: https://blog.csdn.net/weixin_43914593/category_10721247.html
每题提供C++、Java、Python三种语言的代码。
2019省赛A组第8题“修改数组” ,题目链接:
http://oj.ecustacm.cn/problem.php?id=1459
https://www.dotcpp.com/oj/problem2301.html
1、题目描述
给定一个长度为N 的数组A = [A1, A2,…,AN],数组中有可能有重复出现的整数。
现在小明要按以下方法将其修改为没有重复整数的数组。小明会依次修改A2, A3, …, AN。
当修改Ai 时,小明会检查Ai 是否在A1~ Ai-1 中出现过。
如果出现过,则小明会给Ai 加上1 ;
如果新的Ai 仍在之前出现过,小明会持续给Ai 加1 ,直到Ai 没有在A1~Ai-1中出现过。
当AN 也经过上述修改之后,显然A数组中就没有重复的整数了。
现在给定初始的A 数组,请你计算出最终的A 数组。
输入:第一行包含一个整数N(1<=N<=100000)
第二行包含N个整数A1,A2,…,AN(1<=Ai<=1000000)
输出:输出N个整数,依次是最终的A1,A2,…,AN
2、题解
2.1 暴力
先尝试暴力的方法:每读入一个新的数,就检查前面是否出现过,每一次需要检查前面所有的数。共有 n n n个数,每个数检查 O ( n ) O(n) O(n)次,所以总复杂度是 O ( n 2 ) O(n^2) O(n2)的,超时。
2.2 hash
容易想到一个改进的方法:用hash。定义 v i s [ ] vis[] vis[]数组, v i s [ i ] vis[i] vis[i]表示数字 i i i是否已经出现过。这样就不用检查前面所有的数了,基本上可以在 O ( 1 ) O(1) O(1)的时间内定位到。
然而,本题有个特殊的要求:“如果新的 A i A_i Ai仍在之前出现过,小明会持续给 A i A_i Ai加1 ,直到 A i A_i Ai没有在 A 1 A_1 A1 ~ A i − 1 A_{i-1} Ai−1中出现过。”这导致在某些情况下,仍然需要大量的检查。以5个9为例:A[]={9, 9, 9, 9, 9}。
第一次检查A[1]=9,设置vis[9]=1;
第二次检查A[2]=9,先查到vis[9]=1,则把A[2]加1,变为a[2]=10,设置vis[10]=1;
第三次检查A[3]=9,先查到vis[9]=1,则把A[3]加1得A[3]=10;再查到vis[10]=1,再把A[3]加1得A[3]=11,设置vis[11]=1;
…
复杂度仍然是 O ( n 2 ) O(n^2) O(n2)的。
下面给出hash代码。
#include<bits/stdc++.h>
using namespace std;
#define N 1000002
int vis[N]={
0}; //hash: vis[i]=1表示数字i已经存在
int main(){
int n; scanf("%d",&n);
for(int i=0;i<n;i++){
int a;
scanf("%d",&a); //读一个数字
while(vis[a]==1) //若a已经出现过,则该数加1。若加1后再出现,则继续加
a++;
vis[a]=1; //标记该数字
printf("%d ",a); //打印
}
}
2.3 并查集
上文提到,本题用Hash方法,在特殊情况下仍然需要大量的检查。问题出在“持续给 A i A_i Ai 加1 ,直到 A i A_i Ai没有在 A 1 A_1 A1 ~ A i − 1 A_{i-1} Ai−1中出现过”。也就是说,问题出在那些相同的数字上。当处理一个新的 A [ i ] A[i] A[i]时,需要检查所有与它相同的数字。
如果把这些相同的数字看成一个集合,就能用并查集处理。
这种并查集,必须是用“路径压缩”优化的,才能加快检查速度。没有路径压缩的并查集,仍然超时。
学习并查集和“路径压缩”,参考博文:https://blog.csdn.net/weixin_43914593/article/details/104108049
2.3.1 Python代码
N=1000002
s=[] #并查集
def find_set(x): #有路径压缩优化的查询
if(x != s[x]):
s[x] = find_set(s[x]) #集的根是最新的一个数
return s[x]
n=int(input())
A=str(input()).split(' ')
for i in range(N): #并查集初始化
s.append(i)
for i in range(n):
A[i]=int(A[i])
root=find_set(A[i])
A[i]=root
s[root]=find_set(root+1) #加1
for i in A:
print(i,end=' ')
2.3.2 C++代码
#include<bits/stdc++.h>
using namespace std;
const int N=1000002;
int A[N];
int s[N]; //并查集
int find_set(int x){
//用“路径压缩”优化的查询
if(x != s[x])
s[x] = find_set(s[x]); //路径压缩
return s[x];
}
int main(){
for(int i=1;i<N;i++) //并查集初始化
s[i]=i;
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&A[i]);
int root = find_set(A[i]); //查询到并查集的根
A[i] = root;
s[root] = find_set(root+1); //加1
}
for(int i=1;i<=n;i++)
printf("%d ",A[i]);
return 0;
}
2.3.3 Java代码
//oj.ecustacm.c User: 20180861115
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Main {
static int max = 1000005;
static int num[] = new int[max];
public static int find(int x) {
if(x == num[x]) {
return x;
}
return num[x] = find(num[x]);
}
public static void main(String[] args) {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
int n = Integer.parseInt(in.readLine());
int k = 0;
String[] s = in.readLine().split(" ");
for(int i=1;i<=1000000;i++) {
num[i] = i;
}
k = Integer.parseInt(s[0]);
k = find(k);
num[k] = find(k+1);
System.out.print(k);
for(int i=1;i<n;i++) {
k = Integer.parseInt(s[i]);
k = find(k);
System.out.print(" "+k);
num[k] = find(k+1);
}
}catch (Exception e) {
e.printStackTrace();
}
}
}