题目描述
伊娃喜欢从整个宇宙中收集硬币。
有一天,她去了一家宇宙购物中心购物,结账时可以使用各种硬币付款。
但是,有一个特殊的付款要求:每张帐单,她只能使用恰好两个硬币来准确的支付消费金额。
给定她拥有的所有硬币的面额,请你帮她确定对于给定的金额,她是否可以找到两个硬币来支付。
输入格式
第一行包含两个整数 N 和 M,分别表示硬币数量以及需要支付的金额。
第二行包含 N 个整数,表示每个硬币的面额。
输出格式
输出一行,包含两个整数 V1,V2,表示所选的两个硬币的面额,使得 V1≤V2 并且 V1+V2=M。
如果答案不唯一,则输出 V1 最小的解。
如果无解,则输出 No Solution。
数据范围
1≤N≤105,
1≤M≤1000
输入样例1:
8 15
1 2 8 7 2 4 11 15
输出样例1:
4 11
输入样例2:
7 14
1 8 7 2 4 11 15
输出样例2:
No Solution
哈希
这个题目有瑕疵,没有告诉我们硬币的大小范围,只有输入硬币的个数是1e5,
- 如果硬币的范围远大于1e5,那么用哈希就是一个比较好的选择;
- 如果硬币的范围小于1e5,但我们输入的硬币个数就是1e5个,所以必然存在重复硬币,可以不用哈希,而是直接开一个1e5的数组记录个数即可,下标表示硬币,数值表示硬币的个数。
首先需要肯定,硬币的大小一定是非负数,即大于等于1。
思路:
在输入时就处理数据,每输入一个数据x,如果数x大于等于m,说明一定匹配不到,可以找下一个输入数据了,否则就去查找以前已经输入过的数据,能否找到一个y,y=m-x
,使其能够凑成m,如果可以的话,再去判断已经存的结果 (v1,v2)中的v1 是否小于(x,y)中的较小者,如果小于的话,就是比已知 (v1,v2) 要更接近答案的选法。
由于只选两个硬币,所以硬币存在即可,可以不用存储重复元素。
特殊情况:当m是偶数的时候,可能存在x=y=m/2的情况,但是不需要特判,因为当输入的是x=m/2时,如果是第一次输入,肯定凑不成,然后将其插入;第二次输入x=m/2的时候,在先判断之前输入的有没有y=m/2时找到了,即可凑成m,然后判断当前x是否之前已经记录过了,如果没记录过再插入,这是限制了不存在重复元素,所以里面还是只有一个m/2,但是不影响结果。
一般情况则是x和y都插入。x+y=m
但是要注意比较y有没有之前记录过和插入x之间的顺序,要先比较,再插入。
因为,如果先插入的话,
试想:当x=m/2时,先插入x,然后再匹配y是否已经插入过时,此时y==x,匹配到的是刚插入的x,虽然实际上只有一个x,但是却因为顺序问题,给匹配成功了。
因此要先匹配原来存在于哈希表中的数y,然后判断x是否需要插入,再把x插入。
1. 手写哈希
#include <iostream>
#define read(x) scanf("%d",&x)
using namespace std;
const int N=3e5+1,M=1010; "N要是素数"
int hs[N];"初始化为0,当遇到0的时候说明哈希表这个位置为空,因为硬币的价值都是大于0的"
int null=0; "所以哈希表中的0表示null"
int find(int x)
{
int k=x%N;
while (hs[k] && hs[k]!=x) { "如果遇到的数hs[k]不是0并且不是不是要找的x就不停,继续解决冲突"
k++; "直到遇到0,说明表中不存在x,或者遇到x,说明找到了x"
if (k==N) k=0;
}
return k; "要么h[k]为空,表示x不存在,k是插入的位置;要么h[k]=x,k是匹配的位置"
} 其实,在这个题里,这个k就是x。
int main()
{
int n,m;
read(n),read(m);
int v1=M,v2; //(v1,v2)v1先初始化一个可比较且不可能的数
int x,y;
while (n--) {
read(x);
if (x>=m) continue; //必然不成立
y=m-x; //此时y必然大于0
int kx=find(x),ky=find(y); //找到x和y在哈希表中存储的下标,或者是否存在于哈希表中
if (hs[ky]) { //如果y存在的话,因为输入的是x,所以x必然存在
if (v1>min(x,y)) v1=min(x,y),v2=max(x,y);
}
//接下来判断x是否需要插入,当hs[kx]==null时,说明不存在x,需要插入
if (!hs[kx]) hs[kx]=x;
} "且这个插入必须放在判断if(hs[ky])后面,避免当出现x+x=m的情况时出错。否则当第一次插入x(x=m/2)时就会匹配到,会出错。"
v1==M?puts("No Solution"):printf("%d %d",v1,v2);
return 0;
}
2. (1)的简化写法
题目数据限制最多输入1e5个硬币,选出其中两个要凑够M,M最大值是1000。
我们不知道输入硬币的面额是多少,假设面额就是从1到1e5,设立一个bubble数组,开到1e5,下标对应面额,对应的值表示该面额有几个即可。
#include <iostream>
#define read(x) scanf("%d",&x)
using namespace std;
const int N=1e5+10,M=1010;
int bubble[N]; "记录从1到N的面值的硬币的个数"
int main()
{
int n,m;
read(n),read(m);
int v1=M,v2,x,y;
while (n--) {
read(x);
y=m-x;
if(y>0 && bubble[y]) //避免数组越界,y为负数时,即相当于x>=m时不处理
if (v1>min(x,y)) v1=min(x,y),v2=max(x,y);
bubble[x]++;
}
v1==M?puts("No Solution"):printf("%d %d",v1,v2);
return 0;
}
3. unordered_set实现
#include <iostream>
#include <unordered_set>
using namespace std;
#define read(x) scanf("%d",&x)
const int M=1010;
int main()
{
int n,m;
read(n),read(m);
int v1=M,v2,x,y;
unordered_set<int> hash; //set中不允许有重复元素
while (n--) { //边读入边处理
read(x);
y=m-x;
if (hash.count(y)) {//x已经必然存在
if (v1>min(x,y)) v1=min(x,y),v2=max(x,y);
}
hash.insert(x); //重复则不插入
}
v1==M?puts("No Solution"):printf("%d %d",v1,v2);
return 0;
}
双指针
不再边输入边处理,输入完再统一处理。
#include <iostream>
#include <algorithm>
#define read(x) scanf("%d",&x)
using namespace std;
const int N=1e5+10;
int main()
{
int n,m;
read(n),read(m);
int coin[N];
for (int i=0;i<n;i++) read(coin[i]);
sort(coin,coin+n); //默认升序
int i,j;
for (i=0,j=n-1; i<j ; ) { //左边往右走,右边往左走,相碰就可以停,相碰说明只剩下选一个硬币的方案,即没答案
if (coin[i]+coin[j]>m) j--; "只能用if"
else if (coin[i]+coin[j]<m) i++;
else break; //碰到的第一个匹配到的就可以停止,coin[i]此时最小
}
i==j?puts("No Solution"):printf("%d %d",coin[i],coin[j]); "必然不会出现i>j的情况,只可能i==j退出"
return 0;
}
对比二者处理的区别
int i,j;
for (i=0,j=n-1; i<j ; i++) { //左边往右走,右边往左走,相碰就可以停,相碰说明只剩下选一个硬币的方案,即没答案
while (i<j && coin[i]+coin[j]>m) j--;
if (i<j && coin[i]+coin[j]==m) break;
}
i>=j?puts("No Solution"):printf("%d %d",coin[i],coin[j]); "可能出现i>j的情况"