[Luogu P2503] [HAOI2006] 均分数据

洛谷传送门

题目描述

已知N个正整数:A1、A2、……、An 。今要将它们分成M组,使得各组数据的数值和最平均,即各组的均方差最小。均方差公式如下:

σ = ∑ i = 1 M ( x i − x ‾ ) M \sigma= \sqrt{\frac{\sum_{i=1}^M(x_{i}-\overline{x})}{M}} σ=Mi=1M(xix)

x ‾ = ∑ i = 1 M x i M \overline{x}=\frac{\sum_{i=1}^M x_{i}}{M} x=Mi=1Mxi

,其中σ为均方差, x ‾ \overline{x} x是各组数据和的平均值, x i x_i xi为第i组数据的数值和。

输入输出格式

输入格式:

输入文件data.in包括:

第一行是两个整数,表示N,M的值(N是整数个数,M是要分成的组数)

第二行有N个整数,表示A1、A2、……、An。整数的范围是1–50。

(同一行的整数间用空格分开)

输出格式:

输出文件data.out包括一行,这一行只包含一个数,表示最小均方差的值(保留小数点后两位数字)。

输入输出样例

#输入样例1

6 3
1 2 3 4 5 6

#输出样例1

0.00

博主的第一反应:这不是贪心吗?于是写了一个玄学暴力程序:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define R register
#define IN inline
#define W while
#define db double
#define gc getchar()
#define EPS 1e-8
using namespace std;
multiset <int> st;
multiset <int> :: iterator it, itt;
int data[100];
int empty = 0;
db va[100], tott, ave;
int main()
{
    db minn = 1e60;
    srand(time(0));
    int num, div;
    scanf("%d%d", &num, &div);
    for (R int i = 1; i <= num; ++i) scanf("%d", &data[i]), ave += data[i];
    ave /= div;
    int time = 150000;
    R int value, cnt;
    W (time--)
    {
        st.clear();
        for (R int i = 1; i <= div; ++i) st.insert(empty);
        random_shuffle(data + 1, data + 1 + num);
        for (R int i = 1; i <= num; ++i)
        {
            it = st.begin();
            value = *it ;
            value += data[i];
            st.erase(it);
            st.insert(value);
        }
        int top = 0;
        for (it = st.begin(); it != st.end(); ++it)
        {
            top += (*it - ave) * (*it - ave);
        }
        if(top < minn) minn = top;
    }
    printf("%.2lf", sqrt(minn / div));
    return 0;
}

然后成功地苟到了20分…

[Luogu P2503] [HAOI2006] 均分数据_第1张图片

反过来思考一下, 如此暴力用到了set, 使得每次操作都带了一个log, 不利于大范围枚举, 并且只靠random_shuffle进行重新排列每次需要O(N)的时间, 十分浪费时间, 并且不可能枚举到所有情况。于是我们用到了更优的算法——退火算法。

退火算法也是一种玄学的随机算法。先将数据随机分配到每一组中, 再随时间随机调整。 开始时允许向局部次优解移动, 随时间的推移逐渐接近最优解, 此时将判定条件逐渐变得严格, 使解逐渐趋于最优解。这道题中我们通过调整移动数据的条件来限制解的移动, 具体见代码…

# include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define R register
#define W while
#define IN inline
#define gc getchar()
#define db double
#define MX 105
int data[MX], tot[MX], id[MX];
db ans, minn = 1e60, now, bound, tim, aver;
int dot, dv, pos, belong, tar;
int main()
{
  srand(20020220);
  scanf("%d%d", &dot, &dv);
  for (R int i = 1; i <= dot; ++i)
  {
  	scanf("%d", &data[i]);
  	aver += data[i];
  }
  aver /= (double) dv;
  tim = 10000;
  W (tim--)
  {
  	memset(tot, 0, sizeof(tot));
  	ans = 0;
  	for (R int i = 1; i <= dot; ++i)
  	{
  		id[i] = rand() % dv + 1;
  		tot[id[i]] += data[i];
  	}
  	for (R int i = 1; i <= dv; ++i)
  	{ans += (aver - tot[i]) * (aver - tot[i]);}
  	bound = 10000;
  	W (bound > 0.01)
  	{
  		bound *= 0.9;
  		pos = rand() % dot + 1;
  		belong = id[pos];
  		if(bound > 500) tar = min_element(tot + 1, tot + 1 + dv) - tot;
  		now = ans;
  		now -= (tot[belong] - aver) * (tot[belong] - aver);
  		now -= (tot[tar] - aver) * (tot[tar] - aver);
  		tot[belong] -= data[pos];
  		tot[tar] += data[pos];
  		now += (tot[belong] - aver) * (tot[belong] - aver);
  		now += (tot[tar] - aver) * (tot[tar] - aver);
  		if(now < ans) ans = now, id[pos] = tar;
  		else if (rand() % 10000 > bound)
  		{
  			tot[belong] += data[pos];
  			tot[tar] -= data[pos];
  		}
  		else
  		{
  			ans = now, id[pos] = tar;
  		}
  	}
  	minn = min(ans, minn);
  }
  printf("%.2lf", sqrt(minn / dv));
  return 0;
}

[Luogu P2503] [HAOI2006] 均分数据_第2张图片
这样还是拿不到满分…不过将退火时间变长即可AC233

退火算法坑点:

  1. 尽量加大退火时间或起始温度, 通过更多次数的交换比较加大达到最优解的几率,时间若不够可掐表。
  2. 一个好的随机种子是成功的一半!可选取玄学数字例如19260817(实测效果并不优秀), 19491001, 233 …

你可能感兴趣的:(模拟退火,数学)