题目传送门:[NOIP2016 提高组] 愤怒的小鸟 - 洛谷
思路构建
我们首先看这道题的数据规模,发现n≤18,那么很容易想到这道题可能用状压dp来做。
我们可以先思考一个问题,几个点可以确定一个二次函数。这显然是一个小学二年级初中生都知道的问题——3点确定一个二次函数。在这道题中,题目已经帮你找到了一个点(原点),所以我们其实只需要两个点就可以确定一个二次函数了。但题中还有一个限制,那就是a<0,所以我们任意选取的两点不一定符合题意,所以我们得特判a是否为负数。
基于上面的思考,我们可以得到一种记录基本状态的思路——任选两个点进行枚举,得到一个满足题意的解析式(即a<0),然后再枚举剩下的点,找到所有在该二次函数上的点并用一个二进制数记录这个状态。
接着考虑dp过程。我们知道达到所有基本状态的最小步数是1,所以我们就可以在记录基本状态的时候将该状态的dp值记为1。然后,可以发现状态的合并这一过程中,状态所对应的二进制数单调递增,所以从小到大依次枚举可以通过基本状态取并集得来的状态,再用它的dp值来递推后面状态的值,最后输出全集的dp值即可。
细节思考
在代码实现里有几个小细节需要注意:
1.函数解析式的求法:
其中只有a,b是未知量。所以我们可以将这个方程转化为
消元可得:
2.基本状态只能记录1次,且不能互相为子集,因为如1,2,3都在一条二次函数上,那么只有2,3这个状态是不合法的。所以我们枚举两个点的时候i:1—>n-1,j:i+1—>n,并且已经和i一起记录过的点不能再枚举。
3.关于精度的问题。个人实测,精度设为1e-6可过。
代码实现
#include<bits/stdc++.h>
using namespace std;
int n,T,g[20],zt[410],cnt=0,f[1<<19];
struct point{
double x,y;
}p[20];
inline void solve(double x11,double x12,double y1,double x21,double x22,double y2,double&a,double&b){
a=(y1*x22-y2*x12)/(x11*x22-x21*x12);
b=(y1*x21-y2*x11)/(x12*x21-x22*x11);
}//求解析式
int main(){
ios::sync_with_stdio(0);
cin>>T;
while(T--){
memset(g,0,sizeof(g));
memset(p,0,sizeof(p));
memset(zt,0,sizeof(zt));
memset(f,0x3f,sizeof(f));
cnt=0;
int m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>p[i].x>>p[i].y;
for(int i=1;i<n;i++){
for(int j=i+1;j<=n;j++){
int tmp=0;
if((1<<(j-1))&g[i])continue;//保证两个点只能共同记录一次
double a=0,b=0;
if(p[i].x!=p[j].x)solve(p[i].x*p[i].x,p[i].x,p[i].y,p[j].x*p[j].x,p[j].x,p[j].y,a,b);
if(a>=0)continue;
tmp|=1<<(i-1);tmp|=1<<(j-1);
for(int k=n;k>j;k--){
if(abs((p[k].x*p[k].x*a+p[k].x*b)-p[k].y)<=1e-6){
tmp|=1<<(k-1);
g[k]|=tmp;
}
}
g[j]|=tmp;g[i]|=tmp;
zt[++cnt]=tmp;
f[tmp]=1;
}
}
for(int i=1;i<=n;i++){
if(!g[i]){zt[++cnt]=1<<(i-1);f[zt[cnt]]=1;}
}
int s=(1<<n)-1;
for(int i=1;i<=s;i++){
if(f[i]>=1000)continue;
for(int j=1;j<=cnt;j++){
f[i|zt[j]]=min(f[i|zt[j]],f[i]+1);
}
}
cout<<f[s]<<'\n';
}
return 0;
}