[BZOJ 2428][HAOI 2006]均分数据(模拟退火)

题目链接

http://www.lydsy.com/JudgeOnline/problem.php?id=2428

思路

模拟退火乱搞
首先随机设定每个元素所在的组,然后求出初始的方差,然后开始退火,每次退火时首先随机选一个元素 t ,并得到它所在的组 x ,然后需要选一组 y ,把 t 放入组 y 中,选这个组 y 也要讲究技巧,若温度高的话,此时状态不稳定,很难使最终的搜索结果尽量更优,那么就需要贪心地选当前元素值之和最小的组。若温度低的话,此时状态比较稳定,而且各个组的元素和基本上差不多,此时就需要随机选个 y ,然后尝试将 t 放入组 y 中,若新解更优,则把旧解更新为新解,否则在[0,10000)随机一个数,若随机的数比T小,那么就把旧解更新为新解(T越小,在这种情况下更新解的概率也就越小,T的大小相当于是解的可靠性,T越小,说明此时解越可靠,在新解并不优的情况下改变原有解的概率也就越小),否则保持原有的解不变

显然这样的概率性算法,只做1次是不够的,因为开始时就是在随机地确定每个元素所在的组,因此要做很多次,经我实验,最保险是做[10000,20000]次,不过速度比较慢,大概1000ms~1500ms不等,一般的种子做5000次也可以,最快的话可以用一个1e6~1e7的大质数充当随机种子,最少要做2000次,加上内联优化只需200+ms,直接刷到这个题的榜的第一面23333

代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <cmath>

#define MAXN 10000

using namespace std;

double minans=1e30,avenue=0; //avenue=平均值
int n,m,a[MAXN];
int sum[MAXN]; //sum[i]=第i组的(元素-所有数据平均值)^2和
int belong[MAXN]; //belong[i]=元素i所属的组的编号

void SA()
{
    memset(sum,0,sizeof(sum));
    for(int i=1;i<=n;i++)
    {
        belong[i]=rand()%m+1;
        sum[belong[i]]+=a[i];
    }
    double ans=0; //ans=当前的每组(元素-平均值)^2之和
    for(int i=1;i<=m;i++)
        ans+=(sum[i]-avenue)*(sum[i]-avenue);
    double T=10000; //退火温度
    while(T>0.1)
    {
        T*=0.9;
        int t=rand()%n+1,x=belong[t],y; //被换的元素是t,t在组x中,换到组y去
        if(T>500) //温度很高时,此时波动很大,y取当前元素之和最小的那一组,这样准确一些
            y=min_element(sum+1,sum+m+1)-sum;
        else //温度低时,y随机选,让答案渐渐更优
            y=rand()%m+1;
        if(x==y) continue; //RP不好时x可能和y相同,那么跳过本次操作,重新选x和y
        double tmp=ans; //tmp=之前的ans值
        //更新ans值
        ans-=(sum[x]-avenue)*(sum[x]-avenue);
        ans-=(sum[y]-avenue)*(sum[y]-avenue);
        sum[x]-=a[t],sum[y]+=a[t];
        ans+=(sum[x]-avenue)*(sum[x]-avenue);
        ans+=(sum[y]-avenue)*(sum[y]-avenue);
        if(ans<=tmp) //新的解更优,就选择新的解
            belong[t]=y; //标记t现在在y组里
        else if(rand()%10000>T) //否则如果随机后决定移动到新解处,就移动,但是保持原来的解不变
        {
            sum[x]+=a[t],sum[y]-=a[t]; //随机后决定不移动
            ans=tmp;
        }
        else belong[t]=y; //随机后决定移动
    }
    if(ans<minans) minans=ans; //更新最小答案
}

int main()
{
    srand(19990720);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),avenue+=a[i];
    avenue/=(double)m;
    for(int i=1;i<=10000;i++) SA();
    printf("%.2lf\n",sqrt(minans/m));
    return 0;
}

你可能感兴趣的:([BZOJ 2428][HAOI 2006]均分数据(模拟退火))