2018-2019 ACM-ICPC, Asia Nanjing Regional Contest D - Country Meow(最小球覆盖——三分/模拟退火)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_37025443/article/details/84256474

题目链接

题意:

给你n个点,让你在平面中找一个点,使得这个点到各个点的距离中的最大值最小,然后输出这个最大距离

解析:

南京赛的一道题目,一看以为是最小圆覆盖,但是有3个坐标,以为原理差不多

结果就相差一个字,解法天差地别。

这个是最小球覆盖的模板题。

两种做法一种是三分,一种是模拟退火

三分一般可以使用的原理是答案函数一般是凸函数或凹函数,只有一个最大值/最小值,如下图

这道题他的做法就是直接三分坐标,递归地三分坐标分量x,y,z,这样就有3层。

每一层都是在给定的条件下(确定了几个坐标分量),在剩余未确定的坐标分量中找到最大距离最小的坐标,然后再返回该座标

至于每一层的坐标分量为什么独立是凸/凹函数,这个我也不知道...

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
using namespace std;
const int MAXN = 200;
const double eps=1e-9;
const double INF = 1e9+100;
typedef struct point
{
	double p[3];
}point;

point cm[MAXN];
int n;

double dist_max(point now)
{
	double maxx=-1;
	for(int i=1;i<=n;i++)
	{
		double tmp=0;
		for(int j=0;j<3;j++)
		{
			tmp+=(now.p[j]-cm[i].p[j])*(now.p[j]-cm[i].p[j]);
		}
		maxx=max(maxx,sqrt(tmp));
	}
	return maxx;
}


point cal(point now,int dep)  //0:x , 1:y , 2:z
{
	if(dep>=3) return now;
	double l=-100000,r=100000;
	point pl,pr;
	point ans;
	for(int i=0;i<dep;i++) pl.p[i]=pr.p[i]=now.p[i]; 
	while(l<r-eps)
	{
		double midl = (l+r) / 2;        
		double midr = (midl+r) / 2;        // 如果是求最小值的话这里判<=即可        
		pl.p[dep]=midl;
		pr.p[dep]=midr;
		pl=cal(pl,dep+1);
		pr=cal(pr,dep+1);
		if(dist_max(pl)<=dist_max(pr))
		{
			r=midr;
			ans=pl;
		}
		else
		{
			l=midl;
			ans=pr;
		}
	}

	return ans;
}

int main()
{
	
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%lf%lf%lf",&cm[i].p[0],&cm[i].p[1],&cm[i].p[2]);
	point res;
	res.p[0]=res.p[1]=res.p[2]=0;
	printf("%.15lf\n",dist_max(cal(res,0)));
	return 0;
}

另一个方法就是模拟退火。

这个其实跟三分有点类似,三分只能求答案函数是单峰的情况。而模拟退火可以求答案函数是多峰的情况。

做法也跟暴力和随机,基本是随机确定起点,然后随机确定下一步的增量,每走一步更新一遍答案

。基本的框架就是

具体实现就是一个  while  循环, 循环内有4步:

  1. 根据当前解找到下一个解
  2. 计算下一个解的 "能量" (也就是价值)
  3. 决定是否要接受这个新解
  4. 降温

找下一个解的时候有一个提高精度的小技巧: 根据当前温度决定差值的范围. 这样在降温即将结束接近最优解的时候可以有更大的概率更精确地命中最优解.

具体做法就是使用一个产生 [0,1][0,1] 随机实数的函数, 将随机区间转为 [−1,1][−1,1] 后乘上 TT 作为差值. (也就是生成一个 [−T,T][−T,T] 的随机值作为差值)

不过实际操作的时候我们较少直接输出最终解, 而是选择在模拟退火的过程中单独维护一个解, 只在遇到更优解的时候将其更新, 增加正确率.

还有洛谷版的

一、模拟退火步骤

选定一个初始状态(比如选定所有点坐标的平均数),选定一个初始温度T。

当温度大于一个边界值时:

{
  随机变化坐标,变化幅度为 T 。

  计算新解与当前解的差 DE。

  如果新解比当前解优(DE > 0),就用新解替换当前解。

  否则以 exp(DE / T) 的概率用新解替换当前解。

  温度乘上一个小于1的系数,即降温。
}

这样,随着温度不断降低,变化幅度也越来越小,接受一个更劣的解的概率也越来越小。

二、模拟退火注意事项

  1. 温度T的初始值设置问题。 初始温度高,则搜索到全局最优解的可能性大,但因此要花费大量的计算时间;反之,则可节约计算时间,但全局搜索性能可能受到影响。

  2. 退火速度问题。 模拟退火算法的全局搜索性能也与退火速度密切相关。同一温度下的“充分”搜索(退火)是相当必要的,但这需要计算时间。

  3. 温度管理问题 降温系数应为正的略小于1.00的常数。

这道题,有点不一样的是,他是根据离当前解最远的那个点的距离来推算下一个点,因为这样就可以保证把答案变得更小

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
using namespace std;
const int MAXN = 200;
const double eps=1e-9;
const double INF = 1e9+100;
typedef struct point
{
	double p[3];
}point;

point cm[MAXN];
int n;

double dist_max(point now)
{
	double maxx=-1;
	for(int i=1;i<=n;i++)
	{
		double tmp=0;
		for(int j=0;j<3;j++)
		{
			tmp+=(now.p[j]-cm[i].p[j])*(now.p[j]-cm[i].p[j]);
		}
		maxx=max(maxx,sqrt(tmp));
	}
	return maxx;
}


point cal(point now,int dep)  //0:x , 1:y , 2:z
{
	if(dep>=3) return now;
	double l=-100000,r=100000;
	point pl,pr;
	point ans;
	for(int i=0;i<dep;i++) pl.p[i]=pr.p[i]=now.p[i]; 
	while(l<r-eps)
	{
		double midl = (l+r) / 2;        
		double midr = (midl+r) / 2;        // 如果是求最小值的话这里判<=即可        
		pl.p[dep]=midl;
		pr.p[dep]=midr;
		pl=cal(pl,dep+1);
		pr=cal(pr,dep+1);
		if(dist_max(pl)<=dist_max(pr))
		{
			r=midr;
			ans=pl;
		}
		else
		{
			l=midl;
			ans=pr;
		}
	}

	return ans;
}

int main()
{
	
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%lf%lf%lf",&cm[i].p[0],&cm[i].p[1],&cm[i].p[2]);
	point res;
	res.p[0]=res.p[1]=res.p[2]=0;
	printf("%.15lf\n",dist_max(cal(res,0)));
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_37025443/article/details/84256474