预处理过原点的 O(n²) 条候选抛物线各自打中的小猪 bitmask,n ≤ 18 状压 DP,按编号最小的活猪定序转移求最少小鸟数。
OJ: roj
题目 ID: 3643
难度:提高
标签:状态压缩动态规划位运算python
创建: 2026-10-02 12:20
更新: 2026-10-10 11:30
形式化题目
在平面上给定第一象限内的
正解
思路
朴素做法。 最直接的想法是:每只小猪单独发射一只鸟去打,答案上界是
第一步:小猪集合可以化成 bitmask。 一条轨迹
也就是说:一条合法轨迹最多由两只小猪唯一确定。反过来,若某次发射打中了
这
第二步:集合覆盖 → 状压 DP。 现在每个候选抛物线对应一个"一次发射能消灭的小猪集合",问题变成:用最少的集合把全集
初值
题面的
代码
/**
* 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;}#!/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()复杂度
- 预处理候选线:
条 × 每条 次判定 = , 。 - DP:
个状态 × 每状态 次转移 = ,空间 。 - 这与标准 C++ 正解同阶;Python 实现在本仓库 20 组真实数据上每组不超过
、峰值约 ,远低于限制。
图示解析
以样例 2 的第 3 关为例:5 只小猪
| 候选线 | 决定它的猪 | 打中的 bitmask | 含义 |
|---|---|---|---|
| 0 与 3(对角) | {0, 3}( |
顺路带走对角两只 | |
| 1 与 2(对角) | {1, 2}( |
顺路带走反对角两只 | |
| 1 与 4 | {1, 4} |
顺路带走第 5 只 | |
| 单打 | {i} |
保底方案 |
注意同列的 0 与 1(
DP 从
答案 3。读者应观察两点:一条抛物线天然对应一个 bitmask 集合;"最小活猪定序"让每步枚举从
总结
- 把"发射一只鸟"抽象成
条候选抛物线上的 bitmask 集合,是本题唯一的建模跳跃;两只猪定一条抛物线( 、不同列才合法)是核心观察。 提示状压 DP;"编号最小活猪定序"消除排列重复,转移 。 - 实现细节:同列与
特判成"单打"、 判经过、 指令直接忽略。 - 验证:三个官方样例一致;data 仓 20 组真实测试数据全部 PASS。
