洛谷 P2831 NOIp2016提高组 愤怒的小鸟 题解【DP】【状态压缩】【高斯消元】【枚举】
点击量:206
题面很长的题?不过不是很难做,精度问题坑死了……
题目描述
Kiana最近沉迷于一款神奇的游戏无法自拔。
简单来说,这款游戏是在一个平面上进行的。
有一架弹弓位于(0,0)处,每次Kiana可以用它向第一象限发射一只红色的小鸟,小鸟们的飞行轨迹均为形如 $ y=ax^2+bx$ 的曲线,其中a,b是Kiana指定的参数,且必须满足a<0,a,b都是实数。
当小鸟落回地面(即x轴)时,它就会瞬间消失。
在游戏的某个关卡里,平面的第一象限中有n只绿色的小猪,其中第i只小猪所在的坐标为 $ \left(x_i,y_i \right)$。
如果某只小鸟的飞行轨迹经过了$ \left( x_i, y_i \right)$,那么第i只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;
如果一只小鸟的飞行轨迹没有经过$ \left( x_i, y_i \right)$,那么这只小鸟飞行的全过程就不会对第i只小猪产生任何影响。
例如,若两只小猪分别位于(1,3)和(3,3),Kiana可以选择发射一只飞行轨迹为$ y=-x^2+4x$的小鸟,这样两只小猪就会被这只小鸟一起消灭。
而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。
这款神奇游戏的每个关卡对Kiana来说都很难,所以Kiana还输入了一些神秘的指令,使得自己能更轻松地完成这个游戏。这些指令将在【输入格式】中详述。
假设这款游戏一共有T个关卡,现在Kiana想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。由于她不会算,所以希望由你告诉她。
输入输出格式
输入格式:
第一行包含一个正整数T,表示游戏的关卡总数。
下面依次输入这T个关卡的信息。每个关卡第一行包含两个非负整数n,m,分别表示该关卡中的小猪数量和Kiana输入的神秘指令类型。接下来的n行中,第i行包含两个正实数$ x_i,y_i$,表示第i只小猪坐标为$ (x_i,y_i)$。数据保证同一个关卡中不存在两只坐标完全相同的小猪。
如果m=0,表示Kiana输入了一个没有任何作用的指令。
如果m=1,则这个关卡将会满足:至多用$ \lceil n/3 + 1 \rceil$只小鸟即可消灭所有小猪。
如果m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少$ \lfloor n/3 \rfloor$只小猪。
保证$ 1\leq n \leq 18, 0\leq m \leq 2,0 < x_i,y_i < 10$,输入中的实数均保留到小数点后两位。
上文中,符号$ \lceil c \rceil$和$ \lfloor c \rfloor$分别表示对$ c$向上取整和向下取整,例如:$ \lceil 2.1 \rceil = \lceil 2.9 \rceil = \lceil 3.0 \rceil = \lfloor 3.0 \rfloor = \lfloor 3.1 \rfloor = \lfloor 3.9 \rfloor = 3$。
输出格式:
对每个关卡依次输出一行答案。
输出的每一行包含一个正整数,表示相应的关卡中,消灭所有小猪最少需要的小鸟数量。
输入输出样例
输入样例#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: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: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】
这组数据中一共有两个关卡。
第一个关卡与【问题描述】中的情形相同,$ 2$只小猪分别位于$ (1.00,3.00)$和$ (3.00,3.00)$,只需发射一只飞行轨迹为$ y=-x^2+4x$的小鸟即可消灭它们。
第二个关卡中有5只小猪,但经过观察我们可以发现它们的坐标都在抛物线$ y = -x^2 + 6x$上,故Kiana只需要发射一只小鸟即可消灭所有小猪。
【数据范围】
题解:
对于$ n\le 18$的数据范围来说,很容易想到状压,状压就会想到状压DP。也就是用f[二进制数]来存打掉二进制下1所在位置的猪头所需最小步数。那么状态怎么转移呢?
我们知道,小鸟发射的轨迹一定是一条开口向下的抛物线,而三点确定一条抛物线,因此我们有以下两种选择:
①枚举每两只猪与原点构成的抛物线,再枚举各个猪看有没有其他猪在抛物线上。
②扔出去一只鸟只打某一头猪,不论轨迹怎样。
这样算下来,枚举状态是$ O(2^n)$,枚举抛物线是$ O(n^2)$,枚举猪头又是一个$ O(n)$,总共$ O(2^n\cdot n^3)$的复杂度,就算给了2秒也跑不过去的。不过我们可以预处理每条抛物线能打到的猪头,因为每次都是由除原点外的两点确定的抛物线,可以看出,每次都枚举了很多相同的部分。不妨将抛物线预处理,就不用特判每头猪有没有被打过,只要能打掉任意一头猪就可以对答案做出贡献。这样枚举抛物线的复杂度只有$ O(n^2)$,总共是$ O(2^n\cdot n^2)$。
说了这么多,如何确定二元一次方程组的$ a,b$呢?因为有这两个未知数,也有两个已知方程,我们可以试着用高斯消元来解(可能有点浪费资源)。然后在预处理过程中$ O(n^3)$解决出能打到的猪头,在DP过程中转移对打猪做出贡献的抛物线(f[hit]|i!=i)就可以了。
还有要注意的就是精度问题,取$ \varepsilon =10^{-5}$的话会错1至2个点,像这种判相等的题就尽可能卡得小一点(1e-7),但不要太小,因为谁也不知道double会出现怎样的玄学问题。还有,$ 2^{18}>260000$!
Code:
#include<cstdio>
#include<cstring>
#define eps 1e-7
int min(int x,int y)
{
return x<y?x:y;
}
double x[20],y[20];
int hit[400];
double gauss[5][5];
int f[333333];
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
memset(hit,0,sizeof(hit));
memset(f,0x3f,sizeof(f));
f[0]=0;
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%lf%lf",&x[i],&y[i]);
int cnt=0;
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
{
cnt++;
double a,b;
gauss[1][1]=x[i]*x[i];//很长的高斯消元,不过没必要,推两下方程就出来了
gauss[1][2]=x[i];
gauss[1][3]=y[i];
gauss[2][1]=x[j]*x[j];
gauss[2][2]=x[j];
gauss[2][3]=y[j];
double tmp=gauss[1][1]/gauss[2][1];
for(int i=1;i<=3;i++)
gauss[2][i]*=tmp;
for(int i=1;i<=3;i++)
gauss[2][i]-=gauss[1][i];
gauss[2][3]/=gauss[2][2];
gauss[2][2]=1;
gauss[1][1]/=gauss[1][2];
gauss[1][3]/=gauss[1][2];
gauss[1][2]=0;
gauss[1][3]-=gauss[2][3];
gauss[1][3]/=gauss[1][1];
gauss[1][1]=1;
a=gauss[1][3],b=gauss[2][3];
if(a>=0)
{
cnt--;
continue;
}
for(int k=1;k<=n;k++)
{
if(a*x[k]*x[k]+b*x[k]-y[k]<eps&&a*x[k]*x[k]+b*x[k]-y[k]>-eps)
hit[cnt]|=(1<<k-1);
}
}
for(int i=1;i<=n;i++)
hit[++cnt]=(1<<i-1);
for(int i=0;i<(1<<n)-1;i++)
//枚举子集
{
//枚举当前为0的点
for(int j=1;j<=cnt;j++)
if((hit[j]|i)!=i)
f[hit[j]|i]=min(f[hit[j]|i],f[i]+1);
}
printf("%d\n",f[(1<<n)-1]);
}
return 0;
}
说点什么