【题解】洛谷2831[NOIP2016] 愤怒的小鸟

拿到题目就知道肯定是道状压(n<=18,T<=5),直接能够推出正解复杂度为 O ( 2 n × n ) O(2^n\times n ) O(2n×n) ,这个想必不用多说了吧。。。我们直接进入正题——


我们知道,确定一条抛物线需要三个点(数学部分在后面)。所以朴素的暴力做法是:枚举每一个猪的集合 S S S ,代表这个集合里的猪已经全部被击杀,再枚举两只活着的猪和原点组成一条抛物线。当然我们在这里还需要枚举剩下的每头猪是否在抛物线上,因为一只鸟可能会同时打到两只以上的猪,这样可以保证结果最优化。目测复杂度 O ( 2 n × n 3 ) O(2^n\times n^3) O(2n×n3) .

转移方程:

d p [ S ] = m i n ( d p [ S ∣ ( 1 < < i ) ∣ ( 1 < < j ) ∣ ( 1 < < a 1 ) ∣ ( 1 < < a 2 ) ∣ . . . ∣ ( 1 < < a k ) ] ) dp[S]=min(dp[S |(1<<i)|(1<<j)|(1<<a_1)|(1<<a_2)|...|(1<<a_k)]) dp[S]=min(dp[S(1<<i)(1<<j)(1<<a1)(1<<a2)...(1<<ak)])

这里 i , j i,j i,j 代表枚举的两头猪, a i a_i ai 表示后面在这条抛物线上的点。

【题解】洛谷2831[NOIP2016] 愤怒的小鸟_第1张图片

让我们回头看一眼数据范围—— e m m m m m m . . . emmmmmm... emmmmmm... 这个算法好像是有50分的。

那这个算法可以怎么优化呢?

不难想到,其实每一条抛物线是可以预处理的——我们预先枚举每两个点,暴力计算出经过这两个点和原点的抛物线解析式,然后用一个 v e c t o r vector vector 来存后面过这条抛物线的点即可。 v e c t o r [ i ] [ j ] vector[i][j] vector[i][j] 表示在过 i , j i,j i,j 的抛物线上的点的集合。这样我们每次 d p dp dp 的时候就不用枚举后面的点了—— O ( 2 n × n 3 ) O(2^n\times n^3) O(2n×n3) -> O ( 2 n × n 2 ) O(2^n\times n^2) O(2n×n2)

但是这个结果离我们的最终目标显然还有一个 n n n

经过反复思考后(这里不一定要证明,想通即可),我们可以发现一个结论:我们处理猪的先后与答案没有任何关系。也就是说, 1 , 2 1,2 1,2 两头猪组成的抛物线与 3 , 4 3,4 3,4 两头猪组成的抛物线其实并没有先后之分,因为当你选定的抛物线一定时,这些抛物线的处理顺序对答案没有影响。那么我们就可以人为的规定,每次发动攻击时必须要打死最前面的那头活猪 ,因为这头猪早晚都要死,现在死或不死对最终答案没有影响。这样我们就只用枚举一个点了,复杂度也终于降到了预期。


这里面还有几个小问题值得注意:

  1. 数学问题 >> 再看一遍题(或许你已经注意到了这个问题),你就会发现,这个抛物线有可能是不存在的——也就是说,你的解方程函数有可能会出现 x / 0 x/0 x/0 的情况 (不过这种情况只会在两个点在同一条竖线上时才会出现) ,我们只需要把这种情况和抛物线二次项系数 a > = 0 a>=0 a>=0 当做同样处理即可。【对应测试点:2】

  2. 精度问题 >> 如果你和我一样,挂在了第五、第八个点上(就像下面展示的那样),那么恭喜你,你需要吧判断的 eps 改成 1 e − 6 1e-6 1e6 ,这样才能保证对点的坐标判断基本没有误差,亲测有效【对应测试点:5、8】

    【题解】洛谷2831[NOIP2016] 愤怒的小鸟_第2张图片
    注意这几个细节就可以 A C AC AC


数学部分——如何用两个点的坐标推出抛物线解析式(会的可以跳过)
我们设两个点的坐标为 ( x 1 , y 1 ) , ( x 2 , y 2 ) (x_1,y_1),(x_2,y_2) (x1,y1),(x2,y2) ,则有
a × x 1 2 + b × x 1 = y 1 , a × x 2 2 + b × x 2 = y 2 a\times x_1^2+b\times x_1=y_1, a\times x_2^2+b\times x_2=y_2 a×x12+b×x1=y1,a×x22+b×x2=y2
整理得
b × ( x 1 x 2 2 − x 1 2 x 2 ) = y 1 x 2 2 − y 2 x 1 2 b\times (x_1x_2^2-x_1^2x_2)=y_1x_2^2-y_2x_1^2 b×(x1x22x12x2)=y1x22y2x12

b = y 1 x 2 2 − y 2 x 1 2 x 1 x 2 2 − x 1 2 x 2 b=\frac{y_1x_2^2-y_2x_1^2}{x_1x_2^2-x_1^2x_2} b=x1x22x12x2y1x22y2x12
将算出的 b b b 代回原式,解出
a = y 1 − b ∗ x 1 x 1 2 a=\frac{y_1-b*x_1}{x_1^2} a=x12y1bx1
这样我们就得到了整个抛物线的解析式


说了这么多,剩下的就是上代码了:

#include
#include
#include
#define abs(a) ((a)>0?(a):-(a))
using namespace std;
int n,dp[1<<19]; double a,b,x[25],y[25];
vector<int> q[25][25];
void work(int i,int j) {	//解抛物线方程
	double x1=x[i],y1=y[i],x2=x[j],y2=y[j];
	if(x1==x2)	{a=0; return;}
	b=(y1*x2*x2-y2*x1*x1)/(x1*x2*x2-x1*x1*x2);
	a=(y1-b*x1)/(x1*x1);
}
void init() {	//预处理初始化
	for(int i=0;i<n-2;i++)
		for(int j=i+1;j<n-1;j++)
			q[i][j].clear();
	for(int i=0;i<n-2;i++)
		for(int j=i+1;j<n-1;j++) {
			work(i,j);
			if(a>=0)	continue;
			for(int k=j+1;k<n;k++)
				if(abs(a*x[k]*x[k]+b*x[k]-y[k])<=1e-6)
					q[i][j].push_back(k);
		}
}
int main()
{
	int T,gg;
	cin>>T;
	while(T--)
	{
		cin>>n>>gg;
		for(int i=0;i<n;i++)
			cin>>x[i]>>y[i];
		init();
		memset(dp,0x3f,sizeof(dp));
		dp[0]=0;
		for(int S=0;S<(1<<n);S++)
		{
			int firs;
			for(firs=0;firs<n;firs++)
				if(!(S&(1<<firs)))
					break;	//寻找第一头活猪
			dp[S|(1<<firs)]=min(dp[S|(1<<firs)],dp[S]+1);
			if(firs==n)	continue; 
			
			for(int i=firs+1;i<n;i++)
				if(!(S&(1<<i)))
				{
					work(firs,i);
					if(a>=0)
						continue;
					int nS=S;
					nS=nS|(1<<(firs))|(1<<i);
					for(int j=0;j<q[firs][i].size();j++)
						nS=nS|(1<<(q[firs][i][j]));
					dp[nS]=min(dp[nS],dp[S]+1);
				}
		}
		cout<<dp[(1<<n)-1]<<endl;
	}
	return 0;
}

你可能感兴趣的:(题解)