题目描述
将写有数字的n个纸片放入口袋中,你可以一次从口袋抽取4次纸片,每次记下纸片的数字后将其放回口袋。如果这4个数字的和是m,那么你就赢了,否则你就输了。编写程序,判断当纸片上的数字是k1,k2,…,kn时,是否存在抽取4次和为m的方案。如果存在,输出Yes;否则输出No.
限制条件,数据规模
1<=n<=1000,1<=m<=10^8,1<=ki<=10^8.
时间限制为1s.
输入的第一行表示n,第二行表示m,第三行的n个数字表示k.
样例输入
3
10
1 3 5
3
9
1 3 5
样例输出
Yes
No
编程求解
这属于很经典的枚举问题。刚一拿到这一题目的想法就是用4个for循环暴力枚举所有可能的方案,再判断是否存在k[a]+k[b]+k[c]+k[d]的和为m,存在就输出Yes,否则输出No.这是很直观也很自然的思路,算法的时间复杂度是O(n4).
#include<iostream>
using namespace std;
const int MAX_N=1000;
int main()
{
freopen("in.txt","r",stdin);
int n,m,k[MAX_N];
while(cin>>n>>m)
{
for(int i=0;i<n;++i)
cin>>k[i];
bool flag=false;
for(int a=0;a<n;++a)
{
for(int b=0;b<n;++b)
{
for(int c=0;c<n;++c)
{
for(int d=0;d<n;++d)
{
if(k[a]+k[b]+k[c]+k[d]==m)
flag=true;
}
}
}
}
if(flag)
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
return 0;
}
上面的代码通过测试用例没有问题。但是,只要一提交肯定要超时,因为时间复杂度为n^4,当n取1000的时候,n^4=10^12,超时是一定的,所以需要改进算法。其实算法最核心的地方就是那4个for循环。最内层的for循环所做的事情就是检测在k[n]中是否存在这样一个d,使得
ka+kb+kc+kd=m
其实这个可以换一种方式表达,
kd=m-ka-kb-kc.
也就是说,检查数组k中的所有元素,判断是否有m-ka-kb-kc.其实在一个排序的数组中检查有个很快的检查方法就是二分搜索。所以优化的思路就来了,可以考虑将k先进行一次排序,然后看k最中间的数值:
假设m-ka-kb-kc的值为x,
如果比x小,x只可能在它的后半段;
如果比x大,x只可能在它的前半段。
再这样递归地进行搜索,最终就可以确定x是否存在。弱存在则输出Yes,否则输出No。
二分搜索算法每次将候选区间缩小至原来的一半,所以算法的时间复杂度就是排序的时间和循环的时间。其中,排序时间为O(nlogn),循环的时间为O(n^3logn),所以最终的时间复杂度为O(n^3logn)。算法实现如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int MAX_N=1000;
int n,m,k[MAX_N];
bool binary_search(int x)
{
//搜索范围是k[l],k[l+1],...,k[r-1]
int l=0,r=n;
while(r-l>=1)
{
int mid=(l+r)/2;
if(k[mid]==x)
return true;
else if(k[mid]<x)
l=mid+1;
else
r=mid;
}
return false;
}
int main()
{
freopen("in.txt","r",stdin);
while(cin>>n>>m)
{
for(int i=0;i<n;++i)
cin>>k[i];
sort(k,k+n);
bool flag=false;
for(int a=0;a<n;++a)
{
for(int b=0;b<n;++b)
{
for(int c=0;c<n;++c)
{
if(binary_search(m-k[a]-k[b]-k[c]))
flag=true;
}
}
}
if(flag)
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
return 0;
}
算法的时间复杂度是O(n^3logn),当n=1000的时候,还是无法满足时间要求,所以算法还需要进一步优化。刚刚关注的是最内层的循环,其实可以目光看得开一点,关注内层的两个循环,内层的两个循环所做的工作就是检查在数组k中是否存在c和d,使得kc+kd=m-ka-kb.
在这种情况下并不能直接使用二分搜索。但是,如果事先枚举出kc+kd所得的n^2个数值后再排序就可以利用二分搜索直接进行查表就可以了。其实,更加准确地说,kc+kd所得的数字去除重复后只有n*(n+1)/2个。
其中排序时间是O(n^2logn),查找时间是O(n^2logn),所以算法的时间复杂度就是O(n^2logn),所以当n=1000时也可以满足要求。AC代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int MAX_N=1000;
int n,m,k[MAX_N],temp[MAX_N*MAX_N];
bool binary_search(int x)
{
//搜索范围是k[l],k[l+1],...,k[r-1]
int l=0,r=n*n;
while(r-l>=1)
{
int mid=(l+r)/2;
if(temp[mid]==x)
return true;
else if(temp[mid]<x)
l=mid+1;
else
r=mid;
}
return false;
}
int main()
{
freopen("in.txt","r",stdin);
while(cin>>n>>m)
{
for(int i=0;i<n;++i)
cin>>k[i];
//枚举kc+kd
for(int c=0;c<n;++c)
{
for(int d=0;d<n;++d)
{
temp[c*n+d]=k[c]+k[d];
}
}
sort(temp,temp+n*n);
bool flag=false;
for(int a=0;a<n;++a)
{
for(int b=0;b<n;++b)
{
if(binary_search(m-k[a]-k[b]))
flag=true;
}
}
if(flag)
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
return 0;
}
这题虽然不难,但是思路很值得借鉴,从一个复杂度高的算法出发,逐步优化,不断降低算法的复杂度直到满足要求,这种方式很奏效。