形式化题目

在平面上给定第一象限内的 nn 个点 (xi,yi)(x_i, y_i)(0<xi,yi<100 < x_i, y_i < 10,互不相同)。每次操作选择一条形如 y=ax2+bxy = ax^2 + bx(a<0a < 0,a,ba,b 为实数)的抛物线,所有恰好落在这条曲线上的点被"消灭"。求消灭全部 nn 个点所需的最少操作次数。共 TT 组数据,1⩽n⩽181 \leqslant n \leqslant 18。

正解

思路

朴素做法。 最直接的想法是:每只小猪单独发射一只鸟去打,答案上界是 nn。要减少次数,就要让一只鸟顺路多打几只——问题变成"哪些小猪能被同一条抛物线一起带走"。

第一步:小猪集合可以化成 bitmask。 一条轨迹 y=ax2+bxy = ax^2 + bx 必须过原点。任取两只不在同一竖直线上的小猪 (x1,y1)(x_1, y_1)、(x2,y2)(x_2, y_2),把它们代入方程得到线性方程组

{ax12+bx1=y1ax22+bx2=y2⟹a=y1/x1−y2/x2x1−x2,b=y1x1−ax1\begin{cases} a x_1^2 + b x_1 = y_1 \\ a x_2^2 + b x_2 = y_2 \end{cases} \quad\Longrightarrow\quad a = \frac{y_1/x_1 - y_2/x_2}{x_1 - x_2},\qquad b = \frac{y_1}{x_1} - a x_1

也就是说:一条合法轨迹最多由两只小猪唯一确定。反过来,若某次发射打中了 ⩾2\geqslant 2 只小猪,其中任意两只确定出的就是这条轨迹本身。所以值得考虑的抛物线只有

LINE[i][j]=过原点与小猪 i,j 的抛物线经过的小猪集合(bitmask)\text{LINE}[i][j] = \text{过原点与小猪 } i, j \text{ 的抛物线经过的小猪集合(bitmask)}

这 O(n2)O(n^2) 条。两种特殊情况退化成"只打 ii 自己":xi=xjx_i = x_j(两点同列,方程组无解)以及解出的 a⩾0a \geqslant 0(开口向上或水平,不能从原点射进第一象限再落回地面)。坐标只有两位小数,“经过"用 ε=10−6\varepsilon = 10^{-6} 判定:∣axk2+bxk−yk∣<ε|a x_k^2 + b x_k - y_k| < \varepsilon。LINE[i][i]={i}\text{LINE}[i][i] = \{i\},代表"故意单打一只”。

第二步:集合覆盖 → 状压 DP。 现在每个候选抛物线对应一个"一次发射能消灭的小猪集合",问题变成:用最少的集合把全集 {0,1,…,n−1}\{0, 1, \dots, n-1\} 覆盖。n⩽18n \leqslant 18 正是 bitmask DP 的信号:

dp[S]=已消灭的小猪集合包含 S 时,需要的最少小鸟数dp[S] = \text{已消灭的小猪集合包含 } S \text{ 时,需要的最少小鸟数}

初值 dp[0]=0dp[0] = 0,答案是 dp[全满]dp[\text{全满}]。若直接从每个状态枚举所有 O(n2)O(n^2) 条线,会有大量重复计数;标准技巧是给转移定序:设 lowlow 是当前还活着的编号最小的小猪,它迟早要被某只鸟"第一次"消灭——固定当前这只鸟就是解决它的那只,于是只需枚举 LINE[low][j]\text{LINE}[low][j](jj 遍历所有小猪,含 j=lowj = low),从 SS 转到 S∪LINE[low][j]S \cup \text{LINE}[low][j]。每只活猪的"第一次"都被恰好转移一次覆盖,不重不漏。

题面的 mm 指令呢? m=1m = 1、m=2m = 2 保证的是最优解的形状(有 ⌊n/3⌋\lfloor n/3 \rfloor 连杀、⌈n/3+1⌉\lceil n/3 + 1 \rceil 只内全灭),源自"任意三只小猪中必有两只确定的抛物线合法"的组合性质,用于引导搜索 + 剪枝的写法;状压 DP 不依赖这些性质,读入后直接忽略 mm。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-10 11:30
 * update_at: 2026-10-10 11:30
 */

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const double EPS=1e-6; const int INF=1<<30;
struct Point{double x,y;};
int line_of(vector<Point>&pts,int i,int j){ if(i==j) return 1<<i; double x1=pts[i].x,y1=pts[i].y,x2=pts[j].x,y2=pts[j].y; if(fabs(x1-x2)<EPS) return 1<<i; double a=(y1/x1-y2/x2)/(x1-x2); if(a>=-EPS) return 1<<i; double b=y1/x1-a*x1; int mask=0; for(int k=0;k<(int)pts.size();k++) if(fabs(a*pts[k].x*pts[k].x+b*pts[k].x-pts[k].y)<EPS) mask|=1<<k; return mask; }
int solve_level(vector<Point>&pts){int n=pts.size(), full=(1<<n)-1; vector<vector<int> > line(n, vector<int>(n)); for(int i=0;i<n;i++) for(int j=0;j<n;j++) line[i][j]=line_of(pts,i,j); vector<int> dp(1<<n,INF); dp[0]=0; for(int mask=0;mask<(1<<n);mask++){int rest=full^mask; if(dp[mask]==INF||rest==0) continue; int low=0; while(((rest>>low)&1)==0) low++; int nxt=dp[mask]+1; for(int j=0;j<n;j++){int nm=mask|line[low][j]; if(nxt<dp[nm]) dp[nm]=nxt;}} return dp[full];}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); int T; if(!(cin>>T)) return 0; while(T--){int n; string m; cin>>n>>m; vector<Point> pts(n); for(int i=0;i<n;i++) cin>>pts[i].x>>pts[i].y; cout<<solve_level(pts)<<'\n';} return 0;}
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 12:20
# update_at: 2026-10-02 12:20

import sys

EPS = 1e-6   # 坐标只保留两位小数,抛物线"经过"用这个精度判定
INF = 1 << 30


def line_of(pts: list[tuple[float, float]], i: int, j: int) -> int:
    """过原点与猪 i、猪 j 的抛物线顺路能打死的猪集合(bitmask)。

    i == j 时退化为"只打死 i 自己";两点同列或开口向上(a ≥ 0)时
    不存在合法轨迹,同样只打死 i 自己。
    """
    (x1, y1), (x2, y2) = pts[i], pts[j]
    if abs(x1 - x2) < EPS:                          # 两点同列,抛物线不存在
        return 1 << i
    a = (y1 / x1 - y2 / x2) / (x1 - x2)             # 解 y=ax²+bx 过两点的方程组
    if a >= -EPS:                                   # 必须 a < 0 才能从原点射进第一象限
        return 1 << i
    b = y1 / x1 - a * x1
    return sum(1 << k for k, (x, y) in enumerate(pts) if abs(a * x * x + b * x - y) < EPS)


def solve_level(pts: list[tuple[float, float]]) -> int:
    """状压 DP:dp[已消灭的猪集合] = 最少小鸟数。"""
    n = len(pts)
    full = (1 << n) - 1
    LINE = [[line_of(pts, i, j) for j in range(n)] for i in range(n)]

    dp = [INF] * (1 << n)
    dp[0] = 0
    for mask in range(1 << n):
        rest = full ^ mask
        if dp[mask] == INF or rest == 0:            # 不可达 / 已经全灭
            continue
        low = (rest & -rest).bit_length() - 1       # 编号最小的活猪,必须由下一只鸟解决
        nxt = dp[mask] + 1
        for line in LINE[low]:
            nm = mask | line
            if nxt < dp[nm]:
                dp[nm] = nxt
    return dp[full]


def solve() -> None:
    data = iter(sys.stdin.buffer.read().split())
    out: list[str] = []
    T = int(next(data))

    for _ in range(T):
        n = int(next(data))
        m = next(data)  # 神秘指令类型,只提示解的结构,不影响最坏情况下的最优解
        # 依次读入 x_i、y_i(float(next) 从左到右求值,先 x 后 y)
        pts = [(float(next(data)), float(next(data))) for _ in range(n)]
        out.append(str(solve_level(pts)))

    print('\n'.join(out))


if __name__ == "__main__":
    solve()

复杂度

  • 预处理候选线:O(n2)O(n^2) 条 × 每条 O(n)O(n) 次判定 = O(n3)O(n^3),n⩽18n \leqslant 18。
  • DP:2n2^n 个状态 × 每状态 nn 次转移 = O(2n⋅n)≈4.9×107O(2^n \cdot n) \approx 4.9 \times 10^7,空间 O(2n)O(2^n)。
  • 这与标准 C++ 正解同阶;Python 实现在本仓库 20 组真实数据上每组不超过 0.02 s0.02\,\mathrm{s}、峰值约 15 MB15\,\mathrm{MB},远低于限制。

图示解析

以样例 2 的第 3 关为例:5 只小猪 (2.72,2.72)(2.72,2.72)、(2.72,3.14)(2.72,3.14)、(3.14,2.72)(3.14,2.72)、(3.14,3.14)(3.14,3.14)、(5.00,5.00)(5.00,5.00)。其中前四只恰好构成关于直线 x=yx = y 的对称方阵,第 5 只在对角线方向更远处。第一步预处理后,几条有代表性的候选抛物线如下(猪从 0 开始编号):

候选线 决定它的猪 打中的 bitmask 含义
y=−x2+5.44xy = -x^2 + 5.44x 0 与 3(对角) {0, 3}(00110011) 顺路带走对角两只
y=−x2+5.86xy = -x^2 + 5.86x 1 与 2(对角) {1, 2}(11001100) 顺路带走反对角两只
y=−x2+10xy = -x^2 + 10x 1 与 4 {1, 4} 顺路带走第 5 只
单打 ii 自身 {i} 保底方案

注意同列的 0 与 1(xx 都是 2.722.72)、2 与 3(xx 都是 3.143.14)之间没有合法候选线——这正是 xi=xjx_i = x_j 需要特判的原因。

DP 从 dp[0]=0dp[0] = 0 出发,每次转移固定编号最小的活猪 lowlow,用"解决 lowlow 的那条线"扩展集合。最优路线为:

dp[∅]=0→ LINE[0][3] dp[{0,3}]=1→ LINE[1][2] dp[{0,1,2,3}]=2→ 单打 4 dp[{0..4}]=3dp[\varnothing]=0 \xrightarrow{\ \text{LINE}[0][3]\ } dp[\{0,3\}]=1 \xrightarrow{\ \text{LINE}[1][2]\ } dp[\{0,1,2,3\}]=2 \xrightarrow{\ \text{单打 } 4\ } dp[\{0..4\}]=3

答案 3。读者应观察两点:一条抛物线天然对应一个 bitmask 集合;"最小活猪定序"让每步枚举从 O(n2)O(n^2) 条线缩到 O(n)O(n) 条,把指数里的 n2n^2 因子压成 nn。

总结

  • 把"发射一只鸟"抽象成 O(n2)O(n^2) 条候选抛物线上的 bitmask 集合,是本题唯一的建模跳跃;两只猪定一条抛物线(a<0a<0、不同列才合法)是核心观察。
  • n⩽18n \leqslant 18 提示状压 DP;"编号最小活猪定序"消除排列重复,转移 O(2n⋅n)O(2^n \cdot n)。
  • 实现细节:同列与 a⩾0a \geqslant 0 特判成"单打"、ε\varepsilon 判经过、mm 指令直接忽略。
  • 验证:三个官方样例一致;data 仓 20 组真实测试数据全部 PASS。