|
|
#include <bits/stdc++.h>
|
|
|
|
|
|
using namespace std;
|
|
|
const int N = 18; // 小猪的数量上限
|
|
|
const int M = 1 << N; // 用二进制可以模拟出N只小猪的所有状态 0~2^N-1种状态
|
|
|
const double eps = 1e-8; // 浮点数的精度
|
|
|
const int INF = 0x3f3f3f3f; // 正无穷
|
|
|
|
|
|
// 结构体,小猪坐标
|
|
|
struct Node {
|
|
|
double x, y;
|
|
|
} pig[N];
|
|
|
|
|
|
int path[N][N]; // path[i][j]:第i只和第j只小猪构成的抛物线能覆盖的所有状态(是一个状态压缩值,比如0101=5)
|
|
|
int f[M]; // f[i]表示覆盖掉目前i的状态表示中所有小猪,最少需要多少条抛物线,也就是最少需要多少只小鸟
|
|
|
int n; // n只小猪
|
|
|
int m; // Kiana 输入的神秘指令类型,此变量没有用到,据说可以用来骗分,没有仔细研究
|
|
|
|
|
|
int cmp(double a, double b) { // 浮点数比较
|
|
|
return abs(a - b) < eps;
|
|
|
}
|
|
|
|
|
|
int main() {
|
|
|
int T;
|
|
|
scanf("%d", &T);
|
|
|
|
|
|
while (T--) {
|
|
|
scanf("%d%d", &n, &m);
|
|
|
// 读入每只小猪的坐标位置
|
|
|
for (int i = 0; i < n; i++) scanf("%lf%lf", &pig[i].x, &pig[i].y); // 因为采用状压DP,下标从0开始
|
|
|
|
|
|
// 1、预处理所有可能的抛物线,记录抛物线与每个小猪的覆盖关系
|
|
|
memset(path, 0, sizeof path); // 多组数据,每次清空
|
|
|
|
|
|
/*通过分析知道 y=ax^2+bx (∵小鸟从圆点出发,∴c=0)
|
|
|
如何确定一条抛物线呢?其实就是确定下来a,b的值。
|
|
|
方法是通过枚举任意两个小猪:(x1,y1),(x2,y2)确定下来一条抛物线
|
|
|
*/
|
|
|
for (int i = 0; i < n; i++) { // 枚举第一只小猪
|
|
|
path[i][i] = 1 << i; // 注意:存在某些只覆盖一个点的抛物线,这个不容易想到,但打过愤怒的小鸟游戏的人,应该都知道一次未必就能打下两只以上的小猪
|
|
|
for (int j = i + 1; j < n; j++) { // j从i+1开始,防止出现重复,比如(1,3)和(3,1)其实是同一条抛物线
|
|
|
double x1 = pig[i].x, y1 = pig[i].y;
|
|
|
double x2 = pig[j].x, y2 = pig[j].y;
|
|
|
if (cmp(x1, x2) & cmp(y1, y2)) continue; // 如果存在重复输入的数据, 事实上出题人比较良心,没有给我们准备恶心的特判数据
|
|
|
// 但是,为了保护下面的代码运算过程中肯定不出错,做为一个程序员,还是要加上上面这句cmp
|
|
|
double a = (y1 / x1 - y2 / x2) / (x1 - x2); // 推式子,见题解
|
|
|
double b = (y1 / x1) - a * x1; // 推式子,见题解
|
|
|
if (a >= 0) continue; // 抛物线开口需要向下
|
|
|
|
|
|
// 此抛物线可以覆盖掉哪些小猪
|
|
|
for (int k = 0; k < n; k++) {
|
|
|
double x = pig[k].x, y = pig[k].y;
|
|
|
if (cmp(a * x * x + b * x, y)) // 符合抛物线方程,代表此抛物线可以覆盖掉k点
|
|
|
path[i][j] += 1 << k; // 记录此抛物线方程覆盖掉k号小猪
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
/*
|
|
|
2、DP 解决用最少多少条抛物线才能完全覆盖掉所有小猪
|
|
|
起点:0000 终点:1111
|
|
|
办法:对于没有覆盖的小猪,引入经过它的抛物线,对比记录最小代价解
|
|
|
原来就是1的,不用管了,只考虑状态是0的就对了。
|
|
|
*/
|
|
|
memset(f, 0x3f, sizeof f); // 预求最小,先设最大,表示状态不合法或没有计算过
|
|
|
f[0] = 0; // 递推起点,一只小猪也不覆盖掉,需要0条抛物线,这在现实中是合理的解释。
|
|
|
|
|
|
for (int x = 0; x < (1 << n) - 1; x++) { // 枚举每个存在0的状态, (1<<n)-1类似于 111111这样的形式,因为现在的目标是找出含有数位是0的状态进行状态转移,所以全是1的不用考虑
|
|
|
if (f[x] == INF) continue; // 如果当前状态是一个未推导状态,那么也不指望它能帮助其它人提供信息
|
|
|
|
|
|
for (int i = 0; i < n; i++) { // 枚举状态x的每一个数位,也可以理解为每一只小猪,看看它是不是还没有被覆盖到
|
|
|
if ((x >> i & 1) == 0) { // i号小猪现在还没有被覆盖到
|
|
|
for (int j = 0; j < n; j++) { // 枚举i号小猪参与的所有抛物线
|
|
|
if (path[i][j]) {
|
|
|
int k = path[i][j] | x; // 引入此抛物线(i,j),可以在x状态基础上覆盖掉更多的小猪,到达k状态
|
|
|
f[k] = min(f[k], f[x] + 1); // 记录k状态是通过x状态在增加一条抛物线的代价下到达的
|
|
|
}
|
|
|
}
|
|
|
break;
|
|
|
/*
|
|
|
Q: 上面在枚举一个没有覆盖的点的时候为什么只需要枚举一个点就可以了?
|
|
|
A: 因为这里找出来的点i, 是为了进行下一步的状态转移,而不是为了把没有覆盖的点覆盖上。
|
|
|
因为我们枚举了所有的状态,每次找出一个未覆盖的,那最后也自然也能把所有状态都覆盖到,也就是所有状态都进行了转移尝试。
|
|
|
*/
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 递推的结果保存在所有状态都是1的结果数组中
|
|
|
printf("%d\n", f[(1 << n) - 1]);
|
|
|
}
|
|
|
return 0;
|
|
|
} |