题目地址:
https://www.acwing.com/problem/content/526/
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 i 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 T T个关卡,现在Kiana想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。由于她不会算,所以希望由你告诉她。
输入格式:
第一行包含一个正整数
T
T
T,表示游戏的关卡总数。下面依次输入这
T
T
T个关卡的信息。每个关卡第一行包含两个非负整数
n
,
m
n,m
n,m,分别表示该关卡中的小猪数量和Kiana输入的神秘指令类型。接下来的
n
n
n行中,第
i
i
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
1≤n≤18
1≤n≤18,
0
≤
m
≤
2
0≤m≤2
0≤m≤2,
0
<
x
i
,
y
i
<
10
0<x_i,y_i<10
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。
输出格式:
对每个关卡依次输出一行答案。输出的每一行包含一个正整数,表示相应的关卡中,消灭所有小猪最少需要的小鸟数量。
注:可以直接暴力求解,不需要用 m m m的条件。
法1:记忆化搜索。对于一组小猪的坐标,先预处理一下所有的穿过每两个小猪的抛物线(之所以是两个,因为题目中的抛物线可以由两点决定),能覆盖的小猪的状态,以
p
[
i
]
[
j
]
p[i][j]
p[i][j]来表示,其是一个整数,它的二进制位来描述覆盖状态,
1
1
1表示能覆盖该下标的小猪,
0
0
0表示不能覆盖。此处要注意几点:
1、给定两个小猪的坐标
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1)和
(
x
2
,
y
2
)
(x_2,y_2)
(x2,y2),相当于求解线性方程组:
{
x
1
2
a
+
x
1
b
=
y
1
x
2
2
a
+
x
2
b
=
y
2
\begin{cases}x_1^2a+x_1b=y_1\\ x_2^2a+x_2b=y_2\end{cases}
{x12a+x1b=y1x22a+x2b=y2可以解出
a
a
a和
b
b
b:
{
a
=
y
1
x
1
−
y
2
x
2
x
2
−
x
1
b
=
y
1
x
1
−
a
x
1
\begin{cases} a=\frac{\frac{y_1}{x_1}-\frac{y_2}{x_2}}{x_2-x_1} \\b=\frac{y_1}{x_1}-ax_1\end{cases}
{a=x2−x1x1y1−x2y2b=x1y1−ax1可以用Cramer法则,
a
a
a的值可以由下列行列式得到:
a
=
∣
y
1
x
1
y
2
x
2
∣
∣
x
1
2
x
1
x
2
2
x
2
∣
a=\frac{\left|\begin{array}{c} y_1 & x_1\\ y_2 & x_2 \end{array}\right|}{ \left|\begin{array}{c} x_1^2 & x_1\\ x_2^2 & x_2 \end{array}\right|}
a=∣∣∣∣x12x22x1x2∣∣∣∣∣∣∣∣y1y2x1x2∣∣∣∣解出
a
a
a之后,代入第一个方程即可得
b
b
b;
2、有的小猪可能无法与任意另一个小猪构成一个合法抛物线,比如这两个小猪位于同一条竖线(即
x
1
=
x
2
x_1=x_2
x1=x2),或者求出的抛物线开口向上或退化为直线(即
a
≥
0
a\ge 0
a≥0),这两种情况得舍弃;
3、如果某个小猪无法与任意其余的小猪构成一个合法抛物线,我们必须也分配一个抛物线给它,这种抛物线能经过的小猪状态存在
p
[
i
]
[
i
]
p[i][i]
p[i][i]里,也就是
p
[
i
]
[
i
]
=
1
<
<
i
p[i][i]=1<<i
p[i][i]=1<<i。这样的抛物线虽然不唯一,但是它完全是用来覆盖小猪
i
i
i用的。
4、为了节省时间,我们可以只计算
i
≤
j
i\le j
i≤j时候的
p
[
i
]
[
j
]
p[i][j]
p[i][j]。
预处理上述信息之后,接下来进行DFS,并用数组 f [ i ] f[i] f[i]来表示,当当前覆盖的状态为 i i i的时候,还需要至少多少个抛物线可以覆盖所有小猪。递归出口是当 i = 2 n − 1 i=2^n-1 i=2n−1的时候,此时所有小猪都被覆盖了,不需要再加抛物线了,所以返回 0 0 0;否则,找到还没被覆盖的小猪下标,枚举所有可以覆盖这个小猪的抛物线,然后进入下一层递归。 f f f的作用是做记忆,如果有记忆则调取记忆,不需要重复计算了。代码如下:
#include <iostream>
#include <cstring>
#include <cmath>
#define x first
#define y second
using namespace std;
const int N = 20, M = 1 << 20;
const double eps = 1e-8;
int n, m;
// path[i][j]存经过小猪i和j的抛物线能覆盖的小猪状态
int path[N][N], f[M];
// 存小猪的坐标
pair<double, double> q[20];
// s是已经覆盖了多少小猪的状态的二进制表示,返回的是至少还要多少个抛物线可以覆盖所有小猪
int dfs(int s) {
if (f[s] != -1) return f[s];
if (s == (1 << n) - 1) return f[s] = 0;
// 找到第一个未覆盖的小猪的下标,赋值给t
int t = 0;
for (int i = 0; i < n; i++)
if (!(s >> i & 1)) {
t = i;
break;
}
int res = 0x3f3f3f3f;
// 枚举与小猪t配对的另一个小猪的下标,构造抛物线覆盖之
for (int i = 0; i < n; i++) {
int c = 0;
// 只有i <= j的path[i][j]是有效的
if (i <= t) c = path[i][t];
else c = path[t][i];
// 只要覆盖小猪t和i的抛物线是存在的,那么它一定能覆盖t,
// 也就一定能使得s向递归出口更进一步),则枚举之
if (c) res = min(res, dfs(c | s) + 1);
}
return f[s] = res;
}
// 将path[i][j]初始化为能覆盖小猪i和j的抛物线能覆盖的所有小猪的状态;
// 因为path[i][j] = path[j][i],我们只计算i <= j的情况
void init_paths() {
// 枚举抛物线覆盖的第一只小猪
for (int i = 0; i < n; i++) {
// 有可能存在某个点,必须得单独由一条抛物线来覆盖它,
// 那么这种抛物线只覆盖小猪i,所以它的path值为1 << i
path[i][i] = 1 << i;
// 枚举覆盖的第二只小猪。j = i的情况已经在上一行枚举过
for (int j = i + 1; j < n; j++) {
double x1 = q[i].x, y1 = q[i].y, x2 = q[j].x, y2 = q[j].y;
// 同一竖线上的两个点无法被同一条抛物线穿过,略过此种情况
if (fabs(x1 - x2) < eps) continue;
double a = (y1 / x1 - y2 / x2) / (x1 - x2), b = y1 / x1 - a * x1;
// 略过开口向上的抛物线
if (a >= 0) continue;
// 计算此抛物线能覆盖的小猪的状态。首先它一定能覆盖小猪i和j
int s = (1 << i) | (1 << j);
for (int k = 0; k < n; k++) {
// 略过已经被覆盖的i和j
if (k == i || k == j) continue;
double x = q[k].x, y = q[k].y;
// 如果能覆盖小猪k,则计入状态
if (fabs(a * x * x + b * x - y) < eps)
s += 1 << k;
}
path[i][j] = s;
}
}
}
int main() {
int T;
cin >> T;
while (T--) {
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> q[i].x >> q[i].y;
memset(path, 0, sizeof path);
init_paths();
memset(f, -1, sizeof f);
// 初始状态没有小猪被覆盖
cout << dfs(0) << endl;
}
return 0;
}
每个case时间复杂度 O ( n 2 n ) O(n2^n) O(n2n),空间 O ( 2 n ) O(2^n) O(2n)。
法2:动态规划。思路和上面是一样的,只不过写成了递推的形式。代码如下:
#include <iostream>
#include <cstring>
#include <cmath>
#define x first
#define y second
using namespace std;
const int N = 20, M = 1 << 20;
const double eps = 1e-8;
int n, m;
pair<double, double> q[N];
int path[N][N], f[M];
// 与上面完全一样
void init_paths() {
for (int i = 0; i < n; i++) {
path[i][i] = 1 << i;
for (int j = i + 1; j < n; j++) {
double x1 = q[i].x, y1 = q[i].y, x2 = q[j].x, y2 = q[j].y;
if (fabs(x1 - x2) < eps) continue;
double a = (y1 / x1 - y2 / x2) / (x1 - x2), b = y1 / x1 - a * x1;
if (a >= 0) continue;
int s = (1 << i) | (1 << j);
for (int k = 0; k < n; k++) {
if (k == i || k == j) continue;
double x = q[k].x, y = q[k].y;
if (fabs(a * x * x + b * x - y) < eps)
s += 1 << k;
}
path[i][j] = s;
}
}
}
int main() {
int T;
cin >> T;
while (T--) {
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> q[i].x >> q[i].y;
memset(path, 0, sizeof path);
init_paths();
memset(f, 0x3f, sizeof f);
f[0] = 0;
// f[i]的i不用枚举到1 << n,因为在其之前已经被递推到了。
for (int i = 0; i < (1 << n) - 1; i++) {
// 找到还没被覆盖的小猪下标
int t = 0;
for (int j = 0; j < n; j++)
if (!(i >> j & 1)) {
t = j;
break;
}
// 枚举能覆盖t的抛物线
for (int j = 0; j < n; j++) {
int c = 0;
if (t <= j) c = path[t][j];
else c = path[j][t];
if (c) f[i | c] = min(f[i | c], f[i] + 1);
}
}
cout << f[(1 << n) - 1] << endl;
}
return 0;
}
每个case时空复杂度与上同。