暴力枚举算法的优化:抽签问题

题目描述

将写有数字的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;
}

这题虽然不难,但是思路很值得借鉴,从一个复杂度高的算法出发,逐步优化,不断降低算法的复杂度直到满足要求,这种方式很奏效。

点赞