題目描述
將寫有數字的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;
}
這題雖然不難,但是思路很值得借鑑,從一個複雜度高的算法出發,逐步優化,不斷降低算法的複雜度直到滿足要求,這種方式很奏效。