BZOJ2428:[HAOI2006]均分数据

我对模拟退火的理解:https://www.cnblogs.com/AKMer/p/9580982.html

题目传送门:https://www.lydsy.com/JudgeOnline/problem.php?id=2428

遗传算法和爬山算法在\(OJ\)\(A\)题的可能性太小了,还是退火好,可以对我在\(BZOJ\)的账号做一点贡献。

对于这道题,我们首先得明白状态空间是啥。

这道题要求一个最优的分组方法,使得每一组的权值和的方差最小,那么状态就是\(n\)个数分别放在哪一个组里。

然后我们就可以用一个长度为\(n\)的数组来记录状态,寻找邻居节点的时候随机一个数字,将它放到另一组去就是一个新状态了。然后对于方差最小这个要求,我们可以灵性的贪心一波,比如每次都把这个数字移到权值和最小的那一组去。这样在很大几率上会直接减少方差。

每个状态对应的权值就是该状态分配下的方差。

然后退个六七十遍火就是了。

时间复杂度:\(O(能A)\)

空间复杂度:\(O(能A)\)

代码如下:

#include <cmath>
#include <ctime>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define sqr(a) ((a)*(a))

const int maxn=30;

const double T_0=1e-7,del_T=0.998;

int n,m;//n个数字,m组
double ave,ans;//ave存平均值,ans存历史最小方差
int a[maxn],d[maxn],b[maxn],c[maxn],e[maxn];//a存数字,b[i]存第i个数字当前在第几组,d[i]存第i个数字在历史最优状态下在第几组,c[i]存第i组当前权值,e[i]存第i组在历史最优状态下的权值

int read() {
	int x=0,f=1;char ch=getchar();
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=x*10+ch-'0';
	return x*f;
}//快读

double calc(int pos,int group) {//计算把pos号数字放到第group组后的方差
	double sum=0;
	for(int i=1;i<=m;i++) {
		double tmp=c[i]-ave;
		if(tmp==b[pos])tmp-=a[pos];
		if(tmp==group)tmp+=a[pos];
		sum+=sqr(tmp);//按照数学公式计算
	}
	return sqrt(sum/m);//记得返回double,不然会被卡精度
}

void Anneal() {
	memcpy(b,d,sizeof(d));
	memcpy(c,e,sizeof(e));//每次都从最优状态开始
	double T=1e7;//初始温度1e7,结束温度1e-7,del_T为0.998;
	while(T>=T_0) {
		int group=0,mn=1e9;
		for(int i=1;i<=m;i++)
			if(c[i]<mn)mn=c[i],group=i;//找最小的那一组编号
		int pos=rand()%n+1;
		double tmp=calc(pos,group);//计算新状态的答案
		if(tmp<ans) {//如果新状态更优
			c[b[pos]]-=a[pos];c[group]+=a[pos];
			b[pos]=group;ans=tmp;
			memcpy(d,b,sizeof(b));
			memcpy(e,c,sizeof(c));//直接更新当前状态以及历史最优状态
		}
		else if(exp((ans-tmp)/T)*RAND_MAX>rand()) {//如果在一定的概率内
			c[b[pos]]-=a[pos];c[group]+=a[pos];
			b[pos]=group;//直接更新当前状态
		}
		T*=del_T;//降温
	}
}

int main() {
	srand(time(0));
	n=read();m=read();
	for(int i=1;i<=n;i++) {
		a[i]=read();
		b[i]=d[i]=rand()%m+1;
		e[d[i]]+=a[i];
		c[d[i]]+=a[i];ave+=a[i];
	}ave/=m;ans=calc(0,0);//初始化一个状态
	for(int i=1;i<=60;i++)
		Anneal();//做60遍退火(好像这个代码稍微少一点点就A不了了)
	printf("%.2lf\n",ans);//输出答案
	return 0;
}

这题也可以用爬山写,和这个代码差不多,大家灵性脑补一波就好。

posted @ 2018-09-04 19:47  AKMer  阅读(174)  评论(0编辑  收藏  举报