题目:将写有数字的n个纸片放入口袋中,你可以从口袋中抽取4次纸片,每次记下纸片上的数字后都将其放回口袋中。在抽取前心中记住一个数字m,编写一个程序,判断当纸片上所写的数字一次为Ki(i=1,2,3.....n)时,这四次抽取的纸片上的数字之后有没有可能为m,如果可能就输出"Yes",不可能则输出"No".
限制:1<=n<=50
1<=m<=10^8
1<=Ki<=10^8
输入示例1: 输出示例1:
n=3 Yes
m=10
k={1,3,5}
输入示例2: 输出示例2:
n=3 No
m=9
k={1,3,5}
其实拿到这题,有个很简单的思路,就是四重for循环遍历出所有的情况,判断是否存在上述情况,很好想,代码也很好写,如下:
#include<iostream>
#include<cstdlib>
#include<algorithm>
using namespace std;
int main()
{
int n,m,arr[50];
cin>>n>>m;
bool isOK=false;
for(int i=0;i<n;i++)
cin>>arr[i];
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(arr[a]+arr[b]+arr[c]+arr[d]==m)
isOK=true;
if(isOK) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
这种方法虽然好像,但是大家也自然能够看到这种方法所带来的代价是不小的,复杂度为O(n^4),在n还小时还可以勉强使用,但是一旦n变大,例如1<=n<1000,那么这种方法带来的代价为10^12,显然这时这种方法就不靠谱了。
其实大家这里可以使用一个逆向思维,当我们摸出了三张纸片ka,kb,kc时,在摸第四次之前,我们这时扒开袋子看看袋子中是否存在数字为m-ka-kb-kc的纸片。而在查找时,我们不使用线性搜索的方式查找,而是将已经排好序的数字纸片利用二分查找,这样查找的复杂度为O(logn),这样,算上之前三次摸纸片的复杂度,则复杂度就变成了O(n^3logn),其实就相当于在第一种方法的四重for循环中将最后一个循环变为二分查找。
至于单纯二分查找的代码如下:
#include<iostream>
#include<cstdlib>
#include<algorithm>
using namespace std;
int Binary_search(int n,int a[],int key)
{
int start=0,end=n-1;
int mid=(start+end)/2;
while(end>=start && a[mid]!=key)
{
cout<<"start="<<start<<" mid="<<mid<<" end="<<end<<endl;
if(a[mid]>key) end=mid-1;
else start=mid+1;
mid=(start+end)/2;
}
if(end>=start)
return mid;
else
return -1;
}
int main()
{
int a[10]={1,2,3,4,5,6,7,8,9,10},key;
sort(a,a+10);
cin>>key;
cout<<Binary_search(10,a,key);
return 0;
}
上面我的代码中,找到则返回元素在数组中下标(从0开始),找不到则返回-1,
但真实的比赛时,STL库中已经有了现成的函数binary_search(start,end,key),这样对于第一种方法的改进而来的代码如下:
#include<iostream>
#include<cstdlib>
#include<algorithm>
using namespace std;
int main()
{
int n,m,arr[50];
cin>>n>>m;
bool isOK=false;
for(int i=0;i<n;i++)
cin>>arr[i];
sort(arr,arr+n);
for(int a=0;a<n;a++)
for(int b=0;b<n;b++)
for(int c=0;c<n;c++)
if(binary_search(arr,arr+n,m-arr[a]-arr[b]-arr[c]))//binary_search(头迭代器指针,尾迭代器指针,查找值)
isOK=true;
if(isOK) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
对于上述复杂度为O(n^3logn)的方法,在1<=n<1000时,显然还是力不从心的,按照上述的改进的思路,我们能不能将四重循环中的内两层循环再"脱掉",在第一次和第二次摸出纸片ka,kb后,我们就"超前"想象一下在后两次的摸取后,后两次的数字kc,kd能否凑成和为m-ka-kb。
这样的话我们如何进行"超前的想象"呢?那就是在总体抽取前将n张纸片上的数字两两相加,变成一个大小为n*n的数组arryN,这样,在前两次摸取后,我们再在arryN中利用二分查找m-ka-kb。就相当于在两层for循环找到ka,kb后,再内嵌一个二分查找,具体的代码如下:
#include<iostream>
#include<cstdlib>
#include<algorithm>
using namespace std;
int main()
{
int n,m,arr[50],arrN[50*50];
cin>>n>>m;
bool isOK=false;
for(int i=0;i<n;i++)
cin>>arr[i];
for(int k=0;k<n;k++)
for(int j=0;j<n;j++)
arrN[k*n+j]=arr[k]+arr[j];
sort(arrN,arrN+n*n);
for(int a=0;a<n;a++)
for(int b=0;b<n;b++)
if(binary_search(arrN,arrN+n*n,m-arr[a]-arr[b]))//binary_search(头迭代器指针,尾迭代器指针,查找值)
isOK=true;
if(isOK) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
现在来计算下该方法的复杂度:
大小为n^2二分查找的复杂度为O(log n^2)=O(2logn),两层for循环的复杂度为O(n^2),这样总体的复杂度为O(n^2*2logn),去掉常系数则复杂度为(n^2*logn),而对于大小为n^2的数组进行排序,复杂度为O(n^2log(n^2))=O(n^2*2logn),去掉常系数则复杂度仍为(n^2*logn),综上情况的话,第三种方法的复杂度则为(n^2*logn),显然相对于O(n^4)优化了不少。
可能有的同学会继续进行"超前想象",在值摸出一张纸片后,就想象后三次纸片上数字,这样,光对大小为n^3的数组进行排序复杂度就有(n^3*logn)显然没有达到优化的效果。
还有个小细节,那就是本题实质是针对有放回的排列组合的优化,如果取出的纸片不能放回,那么这种方法就不适用了,目前我能想到的就是利用递归将所有不重复的排列组合列出来,再进行求和判断~
PS:这题在层层进行优化的过程中,其实很有意思,让我掌握了对于复杂度较高的程序的一个优化思想,挺好~