愤怒的小鸟
Kiana最近沉迷于一款神奇的游戏无法自拔。
简单来说,这款游戏是在一个平面上进行的。
有一架弹弓位于 (0, 0) 处,每次Kiana可以用它向第一象限发射一只红色的小鸟, 小鸟们的飞行轨迹均为形如 y = ax2 + bx 的曲线,其中 a, b 是Kiana指定的参数,且必须满足 a < 0 。
当小鸟落回地面(即 x 轴)时,它就会瞬间消失。
在游戏的某个关卡里,平面的第一象限中有 n 只绿色的小猪,其中第 i 只小猪所在的坐标为 (xi, yi) 。
如果某只小鸟的飞行轨迹经过了 (xi, yi) ,那么第 i 只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;
如果一只小鸟的飞行轨迹没有经过 (xi, yi) ,那么这只小鸟飞行的全过程就不会对第 i 只小猪产生任何影响。
例如,若两只小猪分别位于 (1, 3) 和 (3, 3) ,Kiana可以选择发射一只飞行轨迹为 y = −x2 + 4x 的小鸟,这样两只小猪就会被这只小鸟一起消灭。
而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。
这款神奇游戏的每个关卡对Kiana来说都很难,所以Kiana还输入了一些神秘的指令,使得自己能更轻松地完成这个这个游戏。
这些指令将在输入格式中详述。
假设这款游戏一共有 T 个关卡,现在Kiana想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。
由于她不会算,所以希望由你告诉她。
注意:本题除NOIP原数据外,还包含加强数据。
输入格式
第一行包含一个正整数T,表示游戏的关卡总数。
下面依次输入这T个关卡的信息。
每个关卡第一行包含两个非负整数n,m,分别表示该关卡中的小猪数量和Kiana输入的神秘指令类型。
接下来的n行中,第i行包含两个正实数(xi,yi),表示第i只小猪坐标为(xi,yi),数据保证同一个关卡中不存在两只坐标完全相同的小猪。
如果m=0,表示Kiana输入了一个没有任何作用的指令。
如果m=1,则这个关卡将会满足:至多用⌈n/3+1⌉只小鸟即可消灭所有小猪。
如果m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 ⌊n/3⌋只小猪。
保证1≤n≤18,0≤m≤2,0<xi,yi<10,输入中的实数均保留到小数点后两位。
上文中,符号 ⌈c⌉ 和 ⌊c⌋ 分别表示对 c 向上取整和向下取整,例如 :⌈2.1⌉=⌈2.9⌉=⌈3.0⌉=⌊3.0⌋=⌊3.1⌋=⌊3.9⌋=3。
输出格式
对每个关卡依次输出一行答案。
输出的每一行包含一个正整数,表示相应的关卡中,消灭所有小猪最少需要的小鸟数量。
输入样例:
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
题解:
一般抛物线方程:
题目中的抛物线有两个特点:
过原点, 即 c=0,c=0
开口向下,即 a<0,a<0
因此抛物线方程为:,有两个未知数,因此两点即可确定一条抛物线。
因此最多有 n2n2 个不同的抛物线。接下来求出所有不同的抛物线,及其能覆盖的所有点的点集。
此时问题变成了经典的“重复覆盖问题”,即给定01矩阵,要求选择尽量少的行,将所有列覆盖住。这里标准做法是使用 Dancing Links。
但是因为这里数据比较小所以我们用状压做,并且可以少写很多代码。
我们定义path[x][y]为x点穿过y点这条抛物线的状态。f[i]代表的是我们状态为i的答案。
所以我们最终的答案一定是f[(1<<n)-1](包含了所有的点)
那么我们一旦遇到一个抛物线不包含一个点的时候怎么处理成为了我们的关键转移问题。
如果不包含j点那么我们一定要找包含j点的抛物线并且这个抛物线能尽可能的包含更多的点。所以我们path就起了作用。因为我们枚举的状态不包含x的时候那么我们下一个状态一定要保证这个值最大。否则我们就当前状态+1,多用一个抛物线去包含他。
#include<bits/stdc++.h>
using namespace std;
const int N=20;
int f[1<<20];
typedef pair<double, double> PDD;
PDD q[N];
int path[N][N],n,m;
const double eps = 1e-8;
int cmp(double x, double y)
{
if (fabs(x - y) < eps) return 0;
if (x < y) return -1;
return 1;
}
int main()
{
int t; cin>>t;
while(t--){
cin>>n>>m;
for(int i=0;i<n;i++) cin>>q[i].first>>q[i].second;
memset(path,0,sizeof path);
for(int i=0;i<n;i++){
path[i][i]=1<<i;
for(int j=0;j<n;j++){
double x1 = q[i].first, y1 = q[i].second;
double x2 = q[j].first, y2 = q[j].second;
if (!cmp(x1, x2)) continue;//垂直向上是不可能的
double a = (y1 / x1 - y2 / x2) / (x1 - x2);
double b = y1 / x1 - a * x1;
if(cmp(a,0)>=0) continue; //保证斜率为负
int state=0;
for(int k=0;k<n;k++){
double x=q[k].first,y=q[k].second;
if(!cmp(a*x*x+b*x,y)){
state+=1<<k;
}
}
path[i][j]=state;
}
}
memset(f,0x3f,sizeof f);
f[0]=0;
for(int i=0;i+1<1<<n;i++){
int x=0;
for(int j=0;j<n;j++){
if(!(i&1<<j)){
x=j;
break;
}
}
for(int j=0;j<n;j++){
f[i|path[x][j]]=min(f[i|path[x][j]],f[i]+1);
}
}
cout<<f[(1<<n)-1]<<endl;
}
}
旧题重做感悟:
因为我们的点数很小,所以我们如果穿过这个点那么二进制就代表了1,我们预处理的时候就是两点之间形成抛物线可以造成的状态(穿过哪些点)。然后用集合论的分析DP方法显然我们的last点就是某种状态下没经过的那个点,然后我们用那个点枚举他和其他点形成抛物线的状态再进行状态间的转移。状压的套路就是枚举状态然后找last点进行转移。有很多时候预处理可以解决超时情况。