题解:洛谷 P2831 [NOIP 2016 提高组] 愤怒的小鸟

【题目来源】

洛谷:P2831 [NOIP 2016 提高组] 愤怒的小鸟 - 洛谷 (luogu.com.cn)

【题目描述】

Kiana 最近沉迷于一款神奇的游戏无法自拔。

简单来说,这款游戏是在一个平面上进行的。

有一架弹弓位于 \((0,0)\) 处,每次 Kiana 可以用它向第一象限发射一只红色的小鸟, 小鸟们的飞行轨迹均为形如 \(y=ax^2+bx\) 的曲线,其中 \(a,b\) 是 Kiana 指定的参数,且必须满足 \(a<0\)\(a,b\) 都是实数。

当小鸟落回地面(即 \(x\) 轴)时,它就会瞬间消失。

在游戏的某个关卡里,平面的第一象限中有 \(n\) 只绿色的小猪,其中第 \(i\) 只小猪所在的坐标为 \((x_i,y_i)\)

如果某只小鸟的飞行轨迹经过了 \((x_i,y_i)\),那么第 \(i\) 只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;

如果一只小鸟的飞行轨迹没有经过 \((x_i,y_i)\),那么这只小鸟飞行的全过程就不会对第 \(i\) 只小猪产生任何影响。

例如,若两只小猪分别位于 \((1,3)\) 和 \((3,3)\),Kiana 可以选择发射一只飞行轨迹为 \(y=-x^2+4x\) 的小鸟,这样两只小猪就会被这只小鸟一起消灭。

而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。

这款神奇游戏的每个关卡对 Kiana 来说都很难,所以 Kiana 还输入了一些神秘的指令,使得自己能更轻松地完成这个这个游戏。

这些指令将在【输入格式】中详述。

假设这款游戏一共有 \(T\) 个关卡,现在 Kiana 想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。

由于她不会算,所以希望由你告诉她。

【输入】

第一行包含一个正整数 \(T\),表示游戏的关卡总数。

下面依次输入这 \(T\) 个关卡的信息。每个关卡第一行包含两个非负整数 \(n,m\),分别表示该关卡中的小猪数量和 Kiana 输入的神秘指令类型。接下来的 \(n\) 行中,第 \(i\) 行包含两个正实数 \((x_i,y_i)\),表示第 \(i\) 只小猪坐标为 \((x_i,y_i)\),数据保证同一个关卡中不存在两只坐标完全相同的小猪。

如果 \(m=0\),表示 Kiana 输入了一个没有任何作用的指令。

如果 \(m=1\),则这个关卡将会满足:至多用 \(\lceil n/3+1\rceil\) 只小鸟即可消灭所有小猪。

如果 \(m=2\),则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 \(\lfloor n/3\rfloor\) 只小猪。

保证 \(1\le n\le 18, 0\le m\le 2,0\lt x_i,y_i\lt 10\),输入中的实数均保留到小数点后两位。

上文中,符号 \(\lceil c\rceil\)\(\lfloor c\rfloor\) 分别表示对 \(c\) 向上取整和向下取整,例如 :\(\lceil 2.1\rceil = \lceil 2.9\rceil= \lceil 3.0\rceil = \lfloor 3.0\rfloor = \lfloor 3.1\rfloor =\lfloor 3.9\rfloor= 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

【算法标签】

《洛谷 P2831 愤怒的小鸟》 #搜索# #状态压缩DP# #NOIP提高组# #2016#

【代码详解】

#include <bits/stdc++.h>
using namespace std;

#define x first  // 定义宏,简化pair的访问
#define y second
typedef pair<double, double> PDD;  // 存储点的坐标
const int N = 18, M = 1 << 18;     // 最大点数N和状态数M
const double eps = 1e-8;           // 浮点数比较的误差范围
int n, m;                          // 点数n,抛物线数量m
PDD q[N];                          // 存储所有点的坐标
int path[N][N];                    // path[i][j]表示通过点i和j的抛物线能覆盖的点集
int f[M];                          // f[state]表示覆盖状态state所需的最少抛物线数

// 浮点数比较函数
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].x >> q[i].y;  // 输入每个点的坐标

        memset(path, 0, sizeof path);  // 初始化path数组

        // 预处理所有可能的抛物线及其覆盖的点集
        for (int i = 0; i < n; i++) {
            path[i][i] = 1 << i;  // 单独覆盖自己的状态
            for (int j = 0; j < n; j++) {
                double x1 = q[i].x, y1 = q[i].y;
                double x2 = q[j].x, y2 = q[j].y;
                if (!cmp(x1, x2)) continue;  // x坐标相同无法构成抛物线

                // 计算抛物线参数a和b(y = ax^2 + bx)
                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].x, y = q[k].y;
                    if (!cmp(a * x * x + b * x, y))  // 点在抛物线上
                        state += 1 << k;
                }
                path[i][j] = state;  // 记录通过点i和j的抛物线覆盖的点集
            }
        }

        memset(f, 0x3f, sizeof f);  // 初始化DP数组为无穷大
        f[0] = 0;  // 初始状态(没有点被覆盖)需要0条抛物线

        // 动态规划求解
        for (int i = 0; i + 1 < (1 << n); i++) {
            int x = 0;
            // 找到第一个未被覆盖的点
            for (int j = 0; j < n; j++)
                if (!(i >> j & 1)) {
                    x = j;
                    break;
                }

            // 尝试用通过x点的所有抛物线进行状态转移
            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;  // 输出覆盖所有点所需的最少抛物线数
    }
    return 0;
}

【运行结果】

2
2 0
1.00 3.00
3.00 3.00
1
5 2
1.00 5.00
2.00 8.00
3.00 9.00
4.00 8.00
5.00 5.00
1
posted @ 2026-02-20 19:55  团爸讲算法  阅读(2)  评论(0)    收藏  举报