愤怒的小鸟
题目来源
NOIP 2016 D2T3
题目描述
在一个二维平面上,有一只小鸟在
(
0
,
0
)
\ (0,\ 0)
(0, 0) 处,有
n
\ n
n 只小猪在
(
x
i
,
y
i
)
\ (x_i,\ y_i)
(xi, yi) 处。小鸟可以以
y
=
a
x
2
+
b
x
(
a
<
0
)
\ y=ax^2+bx\ (a<0)
y=ax2+bx (a<0) 的抛物线的飞出,并消灭在它飞行路径上的所有小猪。求小鸟至少要飞几次才可以消灭所有的小猪。包含多组测试数据。
n
≤
18
,
T
≤
30
,
x
i
,
y
i
∈
d
o
u
b
l
e
\ n\leq 18,\ T\leq 30,\ x_i,y_i\in double
n≤18, T≤30, xi,yi∈double
解题方法
方法一 (朴素版)
还是状压DP,可以直接一维,定义
f
s
\ f_s
fs 表示消灭状态为
s
\ s
s 时小鸟飞的最少次数。
如果直接暴力状压的话时间复杂度为
O
(
T
n
3
2
n
)
\ O(Tn^32^n)
O(Tn32n) ,因为还要枚举
a
,
b
\ a,\ b
a, b ,显然是不可以接受的。
方法二 (AC版)
推式子
假设现在有两点
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
\ (x_1,\ y_1),\ (x_2,\ y_2)
(x1, y1), (x2, y2) ,那么代入
y
=
a
x
2
+
b
x
\ y=ax^2+bx
y=ax2+bx 实在是做不了,下面给出推导为
a
,
b
\ a,\ b
a, b 的式子:
{
x
1
2
a
+
x
1
b
=
y
1
x
2
2
a
+
x
2
b
=
y
2
→
{
x
1
2
x
2
a
+
x
1
x
2
b
=
x
2
y
1
x
1
x
2
2
a
+
x
1
x
2
b
=
x
1
y
2
→
a
=
x
2
y
1
−
x
1
y
2
x
1
2
x
2
−
x
1
x
2
2
→
a
=
y
1
x
1
−
y
2
x
2
x
1
−
x
2
,
b
=
y
1
x
1
−
a
x
1
\left\{\begin{matrix} x_1^2a+x_1b=y_1\\ x_2^2a+x_2b=y_2 \end{matrix}\right. \to \left\{\begin{matrix} x_1^2x_2a+x_1x_2b=x_2y_1\\ x_1x_2^2a+x_1x_2b=x_1y_2 \end{matrix}\right. \to a=\frac{x_2y_1-x_1y_2}{x_1^2x_2-x_1x_2^2} \to a=\frac{\frac{y_1}{x_1}-\frac{y_2}{x_2}}{x_1-x_2} ,\ b=\frac{y_1}{x_1}-ax_1
{x12a+x1b=y1x22a+x2b=y2→{x12x2a+x1x2b=x2y1x1x22a+x1x2b=x1y2→a=x12x2−x1x22x2y1−x1y2→a=x1−x2x1y1−x2y2, b=x1y1−ax1
但是,并不是所有的
x
1
,
x
2
,
y
1
,
y
2
\ x_1,\ x_2,\ y_1,\ y_2
x1, x2, y1, y2 都可以形成二次函数,
x
1
≠
x
2
\ x_1\neq x_2
x1=x2 才可以,因为没有两点在一条抛物线上绝对垂直。
题目要求
a
<
0
\ a<0
a<0 ,要特判。
定义
和朴素做法一样,定义
f
s
\ f_s
fs 表示消灭状态为
s
\ s
s 时最少需要发射的小鸟数。
但是,不同处在于,可以预处理
f
l
y
i
,
j
\ fly_{i,\ j}
flyi, j 。
定义
f
l
y
i
,
j
\ fly_{i,\ j}
flyi, j 为小猪
i
\ i
i 和小猪
j
\ j
j 能组成的抛物线所能经过的其它小猪,需要注意的是
f
l
y
\ fly
fly 存的是一个状态。
举个例子:
如果
i
,
j
\ i,\ j
i, j 的抛物线经过
k
\ k
k ,则
f
l
y
i
,
j
=
f
l
y
i
,
j
∣
2
k
−
1
\ fly_{i,\ j}=fly_{i,\ j}|2^{k-1}
flyi, j=flyi, j∣2k−1
状态转移
先给出循环框架:
for (int s = 0; s < (1 << n); s++)
for (int i = 1; i <= n; i++)
if (!(s & (1 << (i - 1)))) //抛物线上的点必须没有被消灭过
for (int j = 1; j <= n; j++) {
...
}
然后要分情况讨论:
-
i
=
j
\ i=j
则f
s
∣
2
i
−
1
=
m
i
n
(
f
s
∣
2
i
−
1
,
f
s
+
1
)
\ f_{s|2^{i-1}}=min(f_{s|2^{i-1}},\ f_s+1)
要么利用已经计算过的最小值,要么新开一条抛物线。 -
i
≠
j
\ i\neq j
则f
s
∣
f
l
y
i
,
j
=
m
i
n
(
f
s
∣
f
l
y
i
,
j
,
f
s
+
1
)
\ f_{s|fly_{i,\ j}}=min(f_{s|fly_{i,\ j}},\ f_s+1)
以下为核心代码:
for (int s = 0; s < (1 << n); s++)
for (int i = 1; i <= n; i++)
if (!(s & (1 << i - 1))) {
for (int j = 1; j <= n; j++) {
if (i == j) f[s | 1 << (i - 1)] = min(f[s | 1 << (i - 1)], f[s] + 1);
if (fabs(x[i] - x[j]) < eps) continue;
f[s | fly[i][j]] = min(f[s | fly[i][j]], f[s] + 1);
}
break; //剪枝,两点的先后顺序不影响结果
}
这样的话,通过预处理,时间复杂度降至
O
(
T
(
n
3
+
n
2
2
n
)
)
\ O(T(n^3+n^22^n))
O(T(n3+n22n)) ,约等于
T
n
2
2
n
\ Tn^22^n
Tn22n ,但是容易被卡常,但是在
n
2
2
n
\ n^22^n
n22n 中有许多状态在集合中,所以已经删掉了许多状态,总体时间可以接受。
代码实现
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
const double eps = 1e-7;
int T;
int f[1 << N];
int fly[N][N];
double x[N], y[N];
inline int read()
{
register int sum = 0, w = 1; register char ch = getchar();
while (ch < '0' or ch > '9') { if (ch == '-') w = -1; ch = getchar(); }
while (ch >= '0' and ch <= '9') { sum = sum * 10 + ch - '0'; ch = getchar(); }
return sum * w;
}
int main()
{
T = read();
while (T--) {
memset(fly, 0, sizeof fly);
memset(f, 0x3f, sizeof f);
int n = read(), m = read();
for (int i = 1; i <= n; i++)
scanf("%lf%lf", &x[i], &y[i]);
for (int i = 1; i < n; i++)
for (int j = i + 1; j <= n; j++) {
if (fabs(x[i] - x[j]) < eps) continue; //x1, x2坐标不能相同
double a = (y[i] / x[i] - y[j] / x[j]) / (x[i] - x[j]);
if (a >= 0) continue;
double b = y[i] / x[i] - a * x[i];
for (int k = 1; k <= n; k++)
if (fabs(y[k] / x[k] - (a * x[k] + b)) < eps) fly[i][j] |= 1 << k - 1;
}
f[0] = 0;
for (int s = 0; s < (1 << n); s++)
for (int i = 1; i <= n; i++)
if (!(s & (1 << i - 1))) {
for (int j = 1; j <= n; j++) {
if (i == j) f[s | 1 << (i - 1)] = min(f[s | 1 << (i - 1)], f[s] + 1);
if (fabs(x[i] - x[j]) < eps) continue;
f[s | fly[i][j]] = min(f[s | fly[i][j]], f[s] + 1);
}
break;
}
printf("%d\n", f[(1 << n) - 1]);
}
return 0;
}