题解:P2831 [NOIP 2016 提高组] 愤怒的小鸟
前置知识:
- 状态压缩 DP
- 二次函数知识
题意简述
给定
n 个平面直角坐标系上的点。你可以从原点引出解析式为y = ax^2+bx(a<0) 的抛物线,问最少引出多少条这样的抛物线,能使得所有给定的点都在这些抛物线上。
具体思路
注意到
dp 部分
设
这里我们可以先简化一下题目。我们可以假设所有可以覆盖到点的抛物线已经被求出,经过的点集(经过状压)存储在
那么我们可以顺推,枚举每一种可能的点被经过的状态
但是,有些时候一些点可能无法与原点和另一个点确定一条符合题意的抛物线,却可以直接用一条抛物线去经过这个点。所以,我们还要枚举每一个点(设此点为第
两者分开转移即可。
不过,如果基于这种做法,是要求出
预处理部分
众所周知,同一平面的三个点可以确定一条抛物线。(本题有了原点,就只用两个点了)\ 所以我们可以枚举每一对点,计算过原点和这两只猪的抛物线的解析式。
不过,我们总不能直接丢一个方程给程序去算吧(是计算器的话当我没说)。所以,我们需要推出
又到了喜闻乐见的推柿子环节。
这里直接给出结论,过程见云剪贴板:
如果
对于每条合法的抛物线,枚举给定的每个点
最后 dp 一下就可以了。回答单次询问的时间复杂度为
最后的最后,由于处理
代码
#include <iostream>
using namespace std;
using ll = long long;
//#define debug cout << "fnmdp"
int dp[262200]; // dp_s 表示点(猪猪)被穿过的情况为 s
int T, n, m, line[10010], cnt;
double xx[20], yy[20];
const double eps = 1e-6;
int main(){
cin.tie(0) −> sync_with_stdio(0);
cin >> T;
while(T −−){
memset(line, 0, sizeof line);
memset(dp, 0x66, sizeof dp); // 多测必初始化!!!
cin >> n >> m;
for(int i=1; i<=n; ++i){
cin >> xx[i] >> yy[i];
}
for(int i=1; i<=n; ++i){
// line_i 表示第 i 个函数图像覆盖点的情况
for(int j=i+1; j<=n; ++j){
if(xx[i] != xx[j]){
double a = (yy[i] − yy[j] * xx[i] / xx[j]) / xx[i] / (xx[i] − xx[j]);
double b = yy[j] / xx[j] − a * xx[j];
if(a < 0){
++ cnt;
for(int k=1; k<=n; ++k){ // 算出解析式后重新枚举
if(fabs(a * xx[k] * xx[k] + b * xx[k] − yy[k]) <= eps){
line[cnt] |= (1 << (k−1));
}
}
}
}
}
}
dp[0] = 0;
for(int i=0; i<(1<<n); ++i){ // dp_i 单调不降,从小到大枚举状态数字
for(int j=1; j<=cnt; ++j){
dp[i | (line[j])] = min(dp[i] + 1, dp[i | (line[j])]);
} // 用一条线覆盖 / 维持原样
for(int j=1; j<=n; ++j){
dp[i | (1 << j-1)] = min(dp[i] + 1, dp[i | (1 << j-1)]);
}
}
cout << dp[(1<<n) − 1] << '\n';
}
return 0;
}
已防抄袭,请勿直接 Copy。
感谢所有审核大大们!\ End.