题目
题目背景
对应的选择、判断题:试题 - GESP 202603 C++ 五级 - 洛谷有题
题目描述
给定一个包含 n 个互不相同的正整数的数组 A 与一个包含 m 个互不相同的正整数的数组 B,请你帮忙计算有多少个数在数组 A 与数组 B 中均出现。
输入格式
第一行包含两个整数 n,m。
第二行包含 n 个正整数 a1,a2,⋯,an 表示数组 A。
第三行包含 m 个正整数 b1,b2,⋯,bm 表示数组 B。
输出格式
输出一个整数,表示在数组 A 与数组 B 中均出现的数的个数。
输入输出样例
输入 #1
3 5
4 2 3
3 1 5 4 6
输出 #1
2
说明/提示
样例解释
样例 1 中,4、3 在数组 A 与 B 中均出现。
数据范围
对于 40% 的数据,保证 1≤n,m≤1000。
对于 100% 的数据,保证 1≤n,m≤,1≤
,
≤
。
解题时间
相信大家第一反应都是用二重循环,但是1e9的数据告诉你这样不行,所以我们得换一种思路,例如取尺法或离散化。
暴力法
当然,对于暴力的方法,还是能拿40分的,由于思路比较简单,这里直接上代码:
cpp
//超时
#include<bits/stdc++.h>
using namespace std;
int n,m;
int main(){
cin>>n>>m;
int a[n+5],b[m+5];
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
cin>>b[i];
}
long long ans=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i]==b[j]) ans++;
}
}
cout<<ans;
return 0;
}
离散化
接下来是离散化的代码:
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m;
cin>>n>>m;
vector<int> A(n),B(m);
vector<int> all;
for(int i=0;i<n;i++){
cin>>A[i];
all.push_back(A[i]);
}
for(int i=0;i<m;i++){
cin>>B[i];
all.push_back(B[i]);
}
sort(all.begin(),all.end());
all.erase(unique(all.begin(),all.end()),all.end());
auto get_id=[&](int val)->int{
return lower_bound(all.begin(),all.end(),val)-all.begin();
};
vector<bool> mark(all.size(),false);
for(int x:A){
mark[get_id(x)]=true;
}
int ans=0;
for(int x:B){
if(mark[get_id(x)]){
ans++;
}
}
cout<<ans;
return 0;
}
-
all容器:收集了两个数组中所有的元素,这是离散化的原料。 -
排序去重 :
sort后unique+erase是离散化的标准写法,去重后每个数值对应唯一的离散化编号。 -
get_idlambda :通过二分查找lower_bound找到原值在all中的下标,这就是离散化后的编号。时间复杂度O(logK)O(logK),其中KK是不同元素个数。 -
vector<bool>标记:离散化后下标范围变小了(最多n+mn+m个不同值),可以直接用数组下标标记,省去了哈希表的开销。
set神器
其实还有更简单的解法,并且实现难度不逊于离散化和暴力,先上代码:
cpp
//用set函数
#include<bits/stdc++.h>
using namespace std;
int n,m;
set<int> s;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int x;
cin>>x;
s.insert(x);
}
long long ans=0;
for(int i=1;i<=m;i++){
int y;
cin>>y;
s.count(y)?ans++:ans+=0;
}
cout<<ans;
return 0;
}
这段代码巧妙运用到了set函数,用s.insert(x)将x存入容器s中,然后用s.count(y)来判断容器s中是否有重复的数,如果有ans就加1,否则不操作(用三目运算符更简便,还能节省打代码的时间)。
取尺法
取尺法的核心思想是:将两个数组排序后,用两个指针分别扫描,比较当前元素的大小关系。
具体做法:
-
将数组A和数组B分别从小到大排序
-
初始化两个指针i=0(指向A开头),j=0j(指向B开头)
-
当i<n且j<m时循环:
-
若Ai<Bj,说明Ai太小,i右移
-
若Ai>Bj,说明Bj太小,j右移
-
若Ai==Bj,说明找到公共元素,答案加一,同时i和j都右移(跳过这个相等的数,避免重复统计)
-
这种方法的优势是:
-
不需要额外的哈希表或离散化映射
-
只需要O(1)的额外空间(除了存储原数组)
-
排序后一次线性扫描即可完成
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m;
cin>>n>>m;
vector<int> A(n),B(m);
for(int i=0;i<n;i++) cin>>A[i];
for(int i=0;i<m;i++) cin>>B[i];
sort(A.begin(),A.end());
sort(B.begin(),B.end());
int i=0,j=0,ans=0;
while(i<n&&j<m){
if(A[i]<B[j]) i++;
else if(A[i]>B[j]) j++;
else{
ans++;
i++;
j++;
}
}
cout<<ans;
return 0;
}
算法图解
假设A=3,1,4,2,B=5,2,3,1
排序后:
-
A=1,2,3,4
-
B=1,2,3,5
双指针扫描过程:
| 步骤 | i | j | Ai | Bj | 比较 | 操作 | ans |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 1 | 1 | 相等 | ans++,i++,j++ | 1 |
| 2 | 1 | 1 | 2 | 2 | 相等 | ans++,i++,j++ | 2 |
| 3 | 2 | 2 | 3 | 3 | 相等 | ans++,i++,j++ | 3 |
| 4 | 3 | 3 | 4 | 5 | 4<5 | i++ | 3 |
| 5 | 4 | 3 | - | 5 | i==n退出 | - | 3 |
最终答案:3(公共元素为1,2,3)
正确性证明
引理:排序不会改变两个数组中公共元素的集合。
证明:排序只是重新排列元素顺序,元素本身没有变化,所以A和B中共同拥有的数值集合保持不变。
算法正确性:
-
当Ai<Bj时,由于两个数组都是升序的,Ai不可能与B中j及j之后的任何元素相等(因为Bj已经是最小的未比较元素且比Ai大),所以Ai可以安全跳过
-
当Ai>Bj时同理,Bj可以安全跳过
-
当Ai==Bj时,找到了一个公共元素,将两个指针同时后移,避免重复计数
由于每次循环至少移动一个指针,最多移动n+m次,所以算法必然在有限步内结束并得到正确结果。
总结
本题的算法实现非常多,但是思路各有不同。我个人建议在考场上可以用set和取尺法,时间紧迫时可以用暴力骗点分,如果在家里写代码的话可以尝试用离散化突破自我,如果你有更好的解题方案,可以告诉我,本期就到这里,掰掰?