题目描述
Kiana最近沉迷于一款神奇的游戏无法自拔。
简单来说,这款游戏是在一个平面上进行的。
有一架弹弓位于 ( 0 , 0 ) (0, 0) (0, 0) 处,每次Kiana可以用它向第一象限发射一只红色的小鸟, 小鸟们的飞行轨迹均为形如 y = a x 2 + b x y = ax^2 + bx y = ax2 + bx 的曲线,其中 a , b a,b a,b 是Kiana指定的参数,且必须满足 a < 0 a < 0 a < 0 。
当小鸟落回地面(即 x x x 轴)时,它就会瞬间消失。
在游戏的某个关卡里,平面的第一象限中有 n n n 只绿色的小猪,其中第 i i i 只小猪所在的坐标为 ( x i , y i ) (x_i, y_i) (xi, yi)。
如果某只小鸟的飞行轨迹经过了 ( x i , y i ) (x_i, y_i) (xi, yi),那么第 i 只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;
如果一只小鸟的飞行轨迹没有经过 ( x i , y i ) (x_i, y_i) (xi, yi) ,那么这只小鸟飞行的全过程就不会对第 i i i 只小猪产生任何影响。
例如,若两只小猪分别位于 ( 1 , 3 ) (1, 3) (1, 3) 和 ( 3 , 3 ) (3, 3) (3, 3) ,Kiana可以选择发射一只飞行轨迹为 y = − x 2 + 4 x y = −x^2 + 4x y = −x2 + 4x 的小鸟,这样两只小猪就会被这只小鸟一起消灭。
而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。
这款神奇游戏的每个关卡对Kiana来说都很难,所以Kiana还输入了一些神秘的指令,使得自己能更轻松地完成这个这个游戏。
这些指令将在输入格式中详述。
假设这款游戏一共有 T 个关卡,现在Kiana想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。
由于她不会算,所以希望由你告诉她。
输入格式
第一行包含一个正整数T,表示游戏的关卡总数。
下面依次输入这T个关卡的信息。
每个关卡第一行包含两个非负整数 n , m n,m n,m,分别表示该关卡中的小猪数量和Kiana输入的神秘指令类型。
接下来的n行中,第i行包含两个正实数 ( x i , y i ) (x_i, y_i) (xi, yi),表示第i只小猪坐标为 ( x i , y i ) (x_i, y_i) (xi, yi),数据保证同一个关卡中不存在两只坐标完全相同的小猪。
如果 m = 0 m=0 m=0,表示Kiana输入了一个没有任何作用的指令。
如果 m = 1 m=1 m=1,则这个关卡将会满足:至多用 ⌈ n / 3 + 1 ⌉ ⌈n/3+1⌉ ⌈n/3+1⌉只小鸟即可消灭所有小猪。
如果 m = 2 m=2 m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 ⌊ n / 3 ⌋ ⌊n/3⌋ ⌊n/3⌋只小猪。
保证 1 ≤ n ≤ 18 , 0 ≤ m ≤ 2 , 0 < x i , y i < 10 1≤n≤18,0≤m≤2,0<x_i, y_i<10 1≤n≤18,0≤m≤2,0<xi, yi<10,输入中的实数均保留到小数点后两位。
上文中,符号 ⌈ c ⌉ ⌈c⌉ ⌈c⌉ 和 ⌊ c ⌋ ⌊c⌋ ⌊c⌋ 分别表示对 c c c 向上取整和向下取整,例如 : ⌈ 2.1 ⌉ = ⌈ 2.9 ⌉ = ⌈ 3.0 ⌉ = ⌊ 3.0 ⌋ = ⌊ 3.1 ⌋ = ⌊ 3.9 ⌋ = 3 ⌈2.1⌉=⌈2.9⌉=⌈3.0⌉=⌊3.0⌋=⌊3.1⌋=⌊3.9⌋=3 ⌈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
算法思想
根据题目描述,在每个测试样例中,求至少需要发射多少只小鸟才能消灭所有的小猪,即至少需要多少条小鸟的轨迹曲线才能够覆盖所有小猪所在的坐标点。
小鸟们的飞行轨迹均为形如
y
=
a
x
2
+
b
x
y = ax^2 + bx
y = ax2 + bx 的曲线,那么只需要两点
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
(x_1,y_1),(x_2,y_2)
(x1,y1),(x2,y2)就能确定曲线的系数
a
、
b
a、b
a、b,也就是能惟一确定一条曲线。注意这两个点必须满足:这两个点的横坐标不相同,即
x
1
≠
x
2
x_1\ne x_2
x1=x2,否则不可能在同一条抛物线上。如下图所示。
因此,可以预处理出任意两点
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
(x_1,y_1),(x_2,y_2)
(x1,y1),(x2,y2)所确定的曲线(一共有
n
n
n个点,可能的曲线有
n
2
n^2
n2),然后再计算出每条曲线能覆盖哪些点。那么问题就变为最少选择几条曲线,能够覆盖所有的点,每个点至少覆盖一次,即重复覆盖问题。
可以使用DFS思想,解决重复覆盖问题:
//state表示当前哪些列已经被覆盖
//cnt表示当前已经使用了多少条曲线
void dfs(int state, int cnt)
{
//如果已经覆盖了所有的列,求出最小值并返回
//任选一个没有被覆盖的列x
//枚举所有能覆盖x的曲线
//覆盖x得到的新状态
dfs(新状态,cnt + 1);
}
进一步优化,可以考虑使用记忆化的方式,使用f[state]
存储不同状态的最优解,使用状压DP的思想来解决本问题。
算法实现
一、预处理任意两点所确定的曲线,计算出每条曲线能覆盖哪些点:
- 通过曲线的方程
y
=
a
x
2
+
b
x
y = ax^2 + bx
y = ax2 + bx,计算出任意两点
i、j
,其坐标分别为 ( x 1 , y 1 ) (x_1,y_1) (x1,y1)、 ( x 2 , y 2 ) (x_2,y_2) (x2,y2),所确定的曲线 a 、 b a、b a、b: a = y 1 x 1 − y 2 x 2 x 1 − x 2 a = \frac{\frac{y_1}{x_1}-\frac{y_2}{x_2}}{x1-x2} a=x1−x2x1y1−x2y2 b = y 1 x 1 − a x 1 b = \frac{y_1}{x_1}-ax_1 b=x1y1−ax1 - 枚举其它点,如果在这条曲线上,则将其纳入到集合
state
- 由点
i、j
所确定的曲线、其覆盖的点的集合为:path[i][j] = state
二、状态计算
- 枚举所有集合状态
state
- 任选一个没有被覆盖的列
x
- 枚举所有列
j
,那么经过x、j
点的曲线覆盖的所有点的集合为path[x][j]
- 打擂台求出覆盖
x
点的新状态state | path[x][j]
的最优解,f[state | path[x][j]] = min(f[state | path[x][j]], f[state] + 1)
。f[state] + 1
表示引入一条新曲线。
- 任选一个没有被覆盖的列
时间复杂度
状态数:
2
n
2^n
2n
状态转移计算次数:
n
n
n
时间复杂度:
O
(
2
n
×
n
)
O(2^n\times n)
O(2n×n)
代码实现
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;
typedef pair<double, double> PDD;
const int N = 18;
const double eps = 1e-6;
int n, m;
PDD p[N];
//path[i][j]表示覆盖i、j点时的曲线集合
int path[N][N];
int f[1 << N];
int cmp(double a, double b)
{
if(fabs(a - b) < eps) return 0;
if(a > b) return 1;
else return -1;
}
int main()
{
int T;
cin >> T;
while(T --)
{
//输入点的信息
cin >> n >> m;
for(int i = 0; i < n; i ++) cin >> p[i].first >> p[i].second;
memset(path, 0, sizeof path);
//预处理经过所有的点的曲线,以及曲线经过的点的集合
for(int i = 0; i < n; i ++)
{
path[i][i] = 1 << i; //只经过i点的曲线
for(int j = 0; j < n; j ++)
{
//计算点i、j的曲线系数a、b
double x1 = p[i].first, y1 = p[i].second;
double x2 = p[j].first, y2 = p[j].second;
//注意:如果i、j两点不能在同一列
if(cmp(x1, x2) == 0) continue;
double a = (y1 / x1 - y2 / x2) / (x1 - x2);
double b = y1 / x1 - a * x1;
if(cmp(a, 0) >= 0) continue; //曲线必须开口向下
//枚举所有点,计算曲线a、b所能覆盖的点的集合state
int state = 0;
for(int k = 0; k < n; k ++)
{
double x = p[k].first, y = p[k].second;
//如果点(x,y)在曲线ab上
if(!cmp(a * x * x + b * x, y))
{
state += 1 << k; //将k点纳入到曲线ab所能覆盖的点的集合中
}
}
//点i、j所确定的曲线能够覆盖的点的集合为state
path[i][j] = state;
}
}
//初始状态
memset(f, 0x3f, sizeof f);
f[0] = 0;
//状态计算,注意到已经包含所有列时,就不需要再计算了
for(int state = 0; state + 1 < 1 << n; state ++)
{
//找到任意一个不在state中的列x
int x = 0;
for(int i = 0; i < n; i ++)
{
//i点不在state中的列x
if((state >> i & 1) == 0)
{
x = i;
break;
}
}
//枚举所有点,计算覆盖x点的新状态state | path[x][j]的最优解
for(int i = 0; i < n; i ++)
{
f[state | path[x][i]] = min(f[state | path[x][i]], f[state] + 1);
}
}
//最终答案,覆盖所有点时的最优解
cout << f[(1 << n) - 1] << endl;
}
return 0;
}