洛谷 P2831 愤怒的小鸟

一看就是道毒瘤题

两位数的数据范围,一看不是暴搜枚举就是状压了
我们考虑状压啦
预处理出 O ( n 2 ) O(n^2) O(n2)条抛物线各自串起来哪几只猪,然后位运算暴算一通
先推个抛物线公式
{ x 1 2 a + x 1 b = y 1 x 2 2 a + x 2 b = y 2 \begin{aligned}\begin{cases} {x_1}^2a+x_1b=y_1\\ {x_2}^2a+x_2b=y_2\\ \end{cases}\end{aligned} {x12a+x1b=y1x22a+x2b=y2
一 顿 乱 推 之 后 … … 一顿乱推之后……
{ a = y 1 − x 1 y 2 x 2 x 1 2 − x 1 x 2 b = y 1 x 1 − x 1 2 a \begin{aligned}\begin{cases} a=\dfrac{y_1-\dfrac{x_1y_2}{x_2}}{{x_1}^2-x_1x_2}\\\\ b=\dfrac{y_1}{x_1}-{x_1}^2a\\ \end{cases}\end{aligned} a=x12x1x2y1x2x1y2b=x1y1x12a

然后挨个算每只猪能不能被串,可以就把这条抛物线一位改为1
还要注意抛物线的 a a a不能大于0,要不然你的鸟就上天了
然后就状压DP

#include
#include
using namespace std;
const double eps=1e-8;
inline double abs(const double x)
{return x<0?-x:x;}
inline int min(const int a,const int b)
{return a<b?a:b;}
int f[1<<18];
struct point
{
	double x,y;
}a[19];
int para[510],len;//para[i]:第i条抛物线可以串起来的猪的状压
void reset()
{
	memset(para,0,sizeof(para));
	len=0;
}
void Main()
{
	int n,m;scanf("%d%d",&n,&m);
	for(int i=0;i<1<<n;i++)//递推求popcount(二进制下1的个数)
		f[i]=f[i>>1]+(i&1);//最多情况下就一只鸟一只猪呗
	double A,B;//抛物线中的a和b
	for(int i=1;i<=n;i++)
		scanf("%lf%lf",&a[i].x,&a[i].y);
	for(int i=1;i<=n;i++)
	{
		#define x1 a[i].x//为了让代码更好看
		#define y1 a[i].y
		#define x2 a[j].x
		#define y2 a[j].y
		for(int j=1;j<i;j++)
			if(abs(x1-x2)>eps)//横坐标相同会除以零的
			{
				A=(y1-x1*y2/x2)/(x1*(x1-x2));
				if(A>-eps)continue;//你鸟上天了
				B=(y1-x1*x1*A)/x1;
				len++;
				#define x a[k].x
				#define y a[k].y
				for(int k=1;k<=n;k++)
					if(abs(A*x*x+B*x-y)<=eps)
						para[len]|=1<<k-1;//para
				#undef x
				#undef y
			}
		#undef x1
		#undef y1
		#undef x2
		#undef y2
	}
	for(int i=0;i<1<<n;i++)
	{
		for(int j=1;j<=len;j++)
			f[i|para[j]]=min(f[i|para[j]],f[i]+1);//多射一次或者用原来的方案
	}
	printf("%d\n",f[(1<<n)-1]);//把n只猪串起来
}
int main()
{
	int T;scanf("%d",&T);
	while(T--)
		reset(),Main();
	return 0;
}

当然,这不是最优的做法
我们很容易发现,para数组中存的很多线都是重复的,一条有 n n n个点的线会出现 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n1)
例如:抛物线 { i , j , k } \{i,j,k\} {i,j,k}会以 { i , j } , { i , k } , { j , k } \{i,j\},\{i,k\},\{j,k\} {i,j},{i,k},{j,k}的形式出现三次
这就很浪费了
于是我们改进一下:
既然最后要将每个位都填满,那我们不如从左到右填吧?
于是预处理出每个数最低位的0,每次就选取经过最低位0的 O ( n ) O(n) O(n)条抛物线进行转移。
这样复杂度就从 O ( 2 n n 2 ) O(2^nn^2) O(2nn2)降到了 O ( 2 n n ) O(2^nn) O(2nn)

#include
#include
using namespace std;
const double eps=1e-8;
inline double abs(const double x)
{return x<0?-x:x;}
inline int min(const int a,const int b)
{return a<b?a:b;}
int low0[1<<18];
void init()
{
	for(int i=1;i<1<<18;i++)
		low0[i]=i&1?low0[i>>1]+1:0;//递推求最低一个零
}
int f[1<<18];
struct point
{
	double x,y;
}a[19];
int para[19][19],len;
void reset()
{
	memset(para,0,sizeof(para));
	len=0;
}
void Main()
{
	int n,m;scanf("%d%d",&n,&m);
	for(int i=0;i<1<<n;i++)
		f[i]=f[i>>1]+(i&1);
	double A,B;
	for(int i=0;i<n;i++)
		scanf("%lf%lf",&a[i].x,&a[i].y);
	for(int i=0;i<n;i++)
	{
		#define x1 a[i].x
		#define y1 a[i].y
		#define x2 a[j].x
		#define y2 a[j].y
		for(int j=0;j<i;j++)
			if(abs(x1-x2)>eps)
			{
				A=(y1-x1*y2/x2)/(x1*(x1-x2));
				if(A>-eps)continue;
				B=(y1-x1*x1*A)/x1;
				#define x a[k].x
				#define y a[k].y
				for(int k=0;k<n;k++)
					if(abs(A*x*x+B*x-y)<=eps)
						para[j][i]|=1<<k;
				#undef x
				#undef y
			}
		#undef x1
		#undef y1
		#undef x2
		#undef y2
	}
	for(int i=0;i<1<<n;i++)
	{
		int j=low0[i];//就是这里!一定要经过它!
		for(int k=0;k<n;k++)
			f[i|para[j][k]]=min(f[i|para[j][k]],f[i]+1);
	}
	printf("%d\n",f[(1<<n)-1]);
}
int main()
{
	init();
	int T;scanf("%d",&T);
	while(T--)
		reset(),Main();
	return 0;
}

你可能感兴趣的:(做题笔记)