Luogu P2831 [NOIP2016 提高组] 愤怒的小鸟

本文介绍了NOIP2016提高组的一道题目——愤怒的小鸟,玩家需要利用抛物线轨迹消灭小猪。文章详细阐述了关卡规则、输入输出格式,并提供了解题思路和代码实现,主要涉及动态规划和贪心算法。

[NOIP2016 提高组] 愤怒的小鸟

题目描述

Kiana 最近沉迷于一款神奇的游戏无法自拔。

简单来说,这款游戏是在一个平面上进行的。

有一架弹弓位于 (0,0)(0,0)(0,0) 处,每次 Kiana 可以用它向第一象限发射一只红色的小鸟,小鸟们的飞行轨迹均为形如 y=ax2+bxy=ax^2+bxy=ax2+bx 的曲线,其中 a,ba,ba,b Kiana 指定的参数,且必须满足 a<0a < 0a<0a,ba,ba,b 都是实数。

当小鸟落回地面(即 xxx 轴)时,它就会瞬间消失。

在游戏的某个关卡里,平面的第一象限中有 nnn 只绿色的小猪,其中第 iii 只小猪所在的坐标为 (xi,yi)\left(x_i,y_i \right)(xi,yi)

如果某只小鸟的飞行轨迹经过了 (xi,yi)\left( x_i, y_i \right)(xi,yi),那么第 iii 只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;

如果一只小鸟的飞行轨迹没有经过 (xi,yi)\left( x_i, y_i \right)(xi,yi),那么这只小鸟飞行的全过程就不会对第 iii 只小猪产生任何影响。

例如,若两只小猪分别位于 (1,3)(1,3)(1,3)(3,3)(3,3)(3,3)Kiana 可以选择发射一只飞行轨迹为 y=−x2+4xy=-x^2+4xy=x2+4x 的小鸟,这样两只小猪就会被这只小鸟一起消灭。

而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。

这款神奇游戏的每个关卡对 Kiana 来说都很难,所以Kiana还输入了一些神秘的指令,使得自己能更轻松地完成这个游戏。这些指令将在【输入格式】中详述。

假设这款游戏一共有 TTT 个关卡,现在 Kiana 想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。由于她不会算,所以希望由你告诉她。

输入格式

第一行包含一个正整数 TTT,表示游戏的关卡总数。

下面依次输入这 TTT 个关卡的信息。每个关卡第一行包含两个非负整数 n,mn,mn,m,分别表示该关卡中的小猪数量和 Kiana 输入的神秘指令类型。接下来的 nnn 行中,第 iii 行包含两个正实数 xi,yix_i,y_ixi,yi,表示第 iii 只小猪坐标为 (xi,yi)(x_i,y_i)(xi,yi)。数据保证同一个关卡中不存在两只坐标完全相同的小猪。

如果 m=0m=0m=0,表示Kiana输入了一个没有任何作用的指令。

如果 m=1m=1m=1,则这个关卡将会满足:至多用 ⌈n/3+1⌉\lceil n/3 + 1 \rceiln/3+1 只小鸟即可消灭所有小猪。

如果 m=2m=2m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 ⌊n/3⌋\lfloor n/3 \rfloorn/3 只小猪。

保证 1≤n≤181\leq n \leq 181n180≤m≤20\leq m \leq 20m20<xi,yi<100 < x_i,y_i < 100<xi,yi<10,输入中的实数均保留到小数点后两位。

上文中,符号 ⌈c⌉\lceil c \rceilc⌊c⌋\lfloor c \rfloorc 分别表示对 ccc 向上取整和向下取整,例如:⌈2.1⌉=⌈2.9⌉=⌈3.0⌉=⌊3.0⌋=⌊3.1⌋=⌊3.9⌋=3\lceil 2.1 \rceil = \lceil 2.9 \rceil = \lceil 3.0 \rceil = \lfloor 3.0 \rfloor = \lfloor 3.1 \rfloor = \lfloor 3.9 \rfloor = 32.1=2.9=3.0=3.0=3.1=3.9=3

输出格式

对每个关卡依次输出一行答案。

输出的每一行包含一个正整数,表示相应的关卡中,消灭所有小猪最少需要的小鸟数量。

样例 #1

样例输入 #1

2
2 0
1.00 3.00
3.00 3.00
5 2
1.00 5.00
2.00 8.00
3.00 9.00
4.00 8.00
5.00 5.00

样例输出 #1

1
1

样例 #2

样例输入 #2

3
2 0
1.41 2.00
1.73 3.00
3 0
1.11 1.41
2.34 1.79
2.98 1.49
5 0
2.72 2.72
2.72 3.14
3.14 2.72
3.14 3.14
5.00 5.00

样例输出 #2

2
2
3

样例 #3

样例输入 #3

1
10 0
7.16 6.28
2.02 0.38
8.33 7.78
7.68 2.09
7.46 7.86
5.77 7.44
8.24 6.72
4.42 5.11
5.42 7.79
8.15 4.99

样例输出 #3

6

提示

【样例解释1】

这组数据中一共有两个关卡。

第一个关卡与【问题描述】中的情形相同,222只小猪分别位于(1.00,3.00)(1.00,3.00)(1.00,3.00)(3.00,3.00)(3.00,3.00)(3.00,3.00),只需发射一只飞行轨迹为y=−x2+4xy = -x^2 + 4xy=x2+4x的小鸟即可消灭它们。

第二个关卡中有555只小猪,但经过观察我们可以发现它们的坐标都在抛物线 y=−x2+6xy = -x^2 + 6xy=x2+6x上,故Kiana只需要发射一只小鸟即可消灭所有小猪。

【数据范围】

测试点编号n⩽n\leqslantnm=m=m=T⩽T\leqslantT
111222000101010
222222000303030
333333000101010
444333000303030
555444000101010
666444000303030
777555000101010
888666000101010
999777000101010
101010888000101010
111111999000303030
121212101010000303030
131313121212111303030
141414121212222303030
151515151515000151515
161616151515111151515
171717151515222151515
181818181818000555
191919181818111555
202020181818222555

分析

题目大意:平面直角坐标系的第一象限内有 nnn 只小猪,求:至少用多少条过原点且开口向下的抛物线(即发射小鸟)能将这些小猪全部覆盖(击杀)。

看看这数据量,保证 1≤n≤181\leq n \leq 181n18,想都不用想,状压 dpdpdp 啊!

状压的定义当然是每只猪存活与否,用一个二进制数表示, 111 为死亡, 000 为存活。问题在于:如何转移?

首先,明确一点:你不可能发射一只打不到猪的鸟 (虽然在游戏里往往不是这样) 。这个正确性是显而易见的,这样做只会白白浪费一次射鸟的机会。

其次,明确第二点:如果你一炮能打死 222 只小猪,你绝对不会只选择打死其中的 111 只。你可能会想:我一炮要是能干死两只猪,我的鸟又怎么可能只打死其中一只?这个想法是对的,但是干死其中两只意味着你能确定这条抛物线(两只猪 +++ 原点),而只干死一只猪是无法确定一条抛物线的。明确第二点的重要性在于,你已经想到:如果场上还有 222 只及以上的猪,你会枚举去打死哪两只猪(当然这条抛物线还有可能打死其它的猪),而不是只打死一只(只打死一只无法进行转移,且如果这条目标是打死一只猪的抛物线实际上能打死更多的猪,那么这种情况一定会被打死两只猪的抛物线所包括)。

最后,明确第三点:第二点的正确性只是在当场上还有 222 只及以上的猪时,而当场上只有一只猪时,你将不得不去打一炮只能干死一只猪的鸟。

根据以上思路,可以打出如下代码:

#include<bits/stdc++.h>
using namespace std;
struct nod{
	double x,y;
}a[30];
int pos,dp[1000010],vh[30],t,n,m;
double ax,bx,cx;//表示解析式
void getf(double x1,double x2,double x3,double y1,double y2,double y3){//三点求解析式
	ax=((y1-y2)/(x1-x2)-(y1-y3)/(x1-x3))/(x2-x3);
	bx=(y1-y2)/(x1-x2)-ax*(x1+x2);
	cx=y1-ax*x1*x1-bx*x1;//实际上这里cx肯定是0,不需要求
}
double work(double x0){return ax*x0*x0+bx*x0+cx;}//已知解析式和横坐标,求纵坐标
int _min(int u,int v){return u>v?v:u;}
int main(){
	cin>>t;
	while(t--){
		cin>>n>>m;//m是拿部分分用的,我们这里用不到
		memset(dp,127,sizeof(dp));
		dp[0]=0;//没有猪死亡当然是一炮都不打喽
		for(int i=1;i<=n;++i) cin>>a[i].x>>a[i].y;
		for(int i=0;i<(1<<n)-1;++i){
			pos=0;//存活猪的数量
			for(int j=0;j<n;++j) if(!((1<<j)&i)) vh[++pos]=j+1;//vh用于存储存活的猪
			for(int j=1;j<pos;++j)
				for(int l=j+1;l<=pos;++l){
					int nxt=i;
					if(a[vh[j]].x==a[vh[l]].x) continue;//猪在同一列,不可能在同一抛物线上,舍去
					getf(0,a[vh[j]].x,a[vh[l]].x,0,a[vh[j]].y,a[vh[l]].y);
					if(ax>=0) continue;//开口向上/一次函数,舍去
					for(int r=1;r<=pos;++r) if(abs((a[vh[r]].y-work(a[vh[r]].x)))<=0.000001) nxt|=(1<<(vh[r]-1));//杀死抛物线上的其它小猪
					dp[nxt]=_min(dp[nxt],dp[i]+1);
				}
			for(int j=1;j<=n;++j) dp[i|(1<<(j-1))]=_min(dp[i]+1,dp[i|(1<<(j-1))]);//就当只打死一只猪
		}
		cout<<dp[(1<<n)-1]<<endl;
	}
	return 0;
}

这段代码只有 85pts85pts85pts ,超时,因为它是 O(2nn3t)O(2^nn^3t)O(2nn3t) 的,最坏情况下会超时 (虽然也就是开个 O2 的事)。那么哪里能优化呢?

ttt ?这不可能优化……
状态枚举?同样无法优化……
枚举两只猪?貌似也不行……
杀死抛物线上的其它小猪?

彳亍!

我们可以先预处理出来每两只猪及原点所构成的抛物线能杀死的小猪有哪些(用二进制数表示),在使用时和状态进行并运算不就行了 (位运算真是个好东西……)

于是乎,代码就打出来力@w@!

Code

#include<bits/stdc++.h>
using namespace std;
struct nod{
	double x,y;
}a[30];
int pos,dp[1000010],vh[30];
int t,n,m,ok[30][30];//ok数组就用于存储每条抛物线所能杀死的小猪
double ax,bx,cx;
void getf(double x1,double x2,double x3,double y1,double y2,double y3){
	ax=((y1-y2)/(x1-x2)-(y1-y3)/(x1-x3))/(x2-x3);
	bx=(y1-y2)/(x1-x2)-ax*(x1+x2);
	cx=y1-ax*x1*x1-bx*x1;
}
double work(double x0){
	return ax*x0*x0+bx*x0+cx;
}
int _min(int u,int v){
	return u>v?v:u;//当时卡了卡常,毕竟min函数太慢了
}
int main(){
	cin>>t;
	while(t--){
		cin>>n>>m;
		memset(dp,127,sizeof(dp));
		memset(ok,0,sizeof(ok));
		dp[0]=0;
		for(int i=1;i<=n;++i) cin>>a[i].x>>a[i].y;
		for(int i=1;i<n;++i){//预处理
			for(int j=i+1;j<=n;++j){
				if(a[i].x==a[j].x) continue;
				getf(0,a[i].x,a[j].x,0,a[i].y,a[j].y);
				if(ax>=0) continue;
				for(int l=1;l<=n;++l) if(abs((a[l].y-work(a[l].x)))<=0.000001) ok[i][j]|=(1<<(l-1));
			}
		}
		for(int i=0;i<(1<<n)-1;++i){
			pos=0;
			for(int j=0;j<n;++j){
				if(!((1<<j)&i)) vh[++pos]=j+1;
			}
			for(int j=1;j<pos;++j){
				for(int l=j+1;l<=pos;++l){
					dp[i|ok[vh[j]][vh[l]]]=_min(dp[i|ok[vh[j]][vh[l]]],dp[i]+1);
				}
			}
			for(int j=1;j<=n;++j){
				dp[i|(1<<(j-1))]=_min(dp[i]+1,dp[i|(1<<(j-1))]);
			}
		}
		cout<<dp[(1<<n)-1]<<endl;
	}
	return 0;
}

时间复杂度 O(2nn2t)O(2^nn^2t)O2nn2t(实际上比这个小一点,因为枚举的是 pos2+npos^2+npos2+n ) 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值