暴力枚舉算法的優化:抽籤問題

題目描述

將寫有數字的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;
}

這題雖然不難,但是思路很值得借鑑,從一個複雜度高的算法出發,逐步優化,不斷降低算法的複雜度直到滿足要求,這種方式很奏效。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章