题解:P13846 [CERC 2023] Ball Passing

· · 题解

P13846 [CERC 2023] Ball Passing

注:这是一篇模拟退火题解。

题目大意

N 个学生,其中男生和女生都是偶数个。

要求将男生两两配对,女生两两配对。以坐标形式给出每个学生的位置,则每对配对的学生之间会有一个欧几里得距离。

目标是最大化所有配对的距离之和。

思路

读题可知,我们只需要对于男生和女生,各自求出所有配对的距离之和的最大值即可。

那么我们先只考虑对于其中一种性别,如何最大化答案。

而对于两两配对这种问题,我们可以考虑对这种性别的学生进行重新排列,让奇数位的学生与他之后最近的偶数位的学生配对即可,即排列中的第 1 的学生与第 2 个学生配对、第 3 的学生与第 4 个学生配对、第 5 的学生与第 6 个学生配对 \dots \dots

于是,我们只需要找到对这种性别的学生的一种排列,使得按上述方式配对后,所有配对的距离之和最大即可。

那么,在观察到数据范围 2 \le N \le 50,并且发现题目要求我们找到一种最优排列的时候,我们不妨大胆地使用模拟退火算法

具体做法

  1. 先用 std::random_shuffle() 把一开始给的排列打乱,防止被卡()。

  2. 设定一个初始温度 T,然后进入漫长的降温环节。

  3. 每次降温时:随机选取一种性别,在这种性别的学生中随机选择两个学生交换位置,然后求出换位置后得到的排列对应的新答案。这样子求答案是很好求的:从前往后两个两个取学生,然后将这两个学生的距离累加到新答案中即可。

  4. 比较新答案 newans 和旧答案 ans

    • 如果新答案大于旧答案,则直接用新答案代替旧答案,且采用这个新的排列继续进行下一步的降温。

    • 否则,以一定的概率采用新排列(注意这里不用新答案代替旧答案),根据算法特色,这个概率为:

      \Large e^\frac{newans-ans}{T}

      这样选取概率可以保证:随着 T 的降低,排列的变化逐渐趋于稳定;新答案比旧答案小太多时,不采用新的排列。

  5. 降温,重复执行步骤 2\~5 直到温度降低到一定程度为止。

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 51;
string S;
int n,bc=1,gc=1;//bc为男生人数,gc为女生人数
double ans;

struct obj
{
    double x,y;
}b[MAXN],g[MAXN];
//分两个数组,用结构体存学生的信息,方便操作排列顺序

double getd(obj u,obj v)
{
    return sqrt((u.x-v.x)*(u.x-v.x) + (u.y-v.y)*(u.y-v.y));
}
//求两个学生之间的距离

int getr(int l,int r)
{
    return rand()%(r-l+1)+l;
}
//获取[l,r]的随机整数

double getp()
{
    return (double)rand()/RAND_MAX;
}
//获取一个[0,1]的随机浮点数,用于概率相关计算

double getans()
{
    double res = 0;
    for (int i=2;i<=bc;i+=2) res += getd(b[i-1],b[i]);
    for (int i=2;i<=gc;i+=2) res += getd(g[i-1],g[i]);
    return res;
}
//获取当前排列状况下的答案

int main()
{
    srand(time(NULL));
    cin >> n >> S;
    for (int i=1;i<=n;i++)
    {
        if (S[i-1] == 'B')
        {
            cin >> b[bc].x >> b[bc].y;
            bc ++;
        } else if (S[i-1] == 'G')
        {
            cin >> g[gc].x >> g[gc].y;
            gc ++;
        }
    }
    bc--,gc--;
    //把男生和女生的信息分别输入到两个数组里,并统计男生和女生各自的人数
    //注意字符串下标是从0开始的www

    if (bc > 0) random_shuffle(b+1,b+bc+1);
    if (gc > 0) random_shuffle(g+1,g+gc+1);
    //对应步骤1,重排数组,注意特判某个性别学生人数为0的情况,不然可能会RE

    double T = 100.0;
    //对应步骤2,设定初始温度
    while (T >= 0.01) //在温度低于一定数值时跳出循环
    {
        int s = getr(0,1),swp1,swp2;
        //s表示当前选取的性别,0是女生,1是男生;swp1和swp2表示随机选取的两个学生在数组中对应的下标
        if (s && bc > 0)
        {
            swp1 = getr(1,bc),swp2 = getr(1,bc);
            swap(b[swp1],b[swp2]);
        } else if (gc > 0)
        {
            swp1 = getr(1,gc),swp2 = getr(1,gc);
            swap(g[swp1],g[swp2]);
        } else continue;
        //交换学生在排列中的位置,得到新的排列
        //同样注意特判某个性别学生人数为0的情况

        double newans = getans();
        //获取新答案
        if (newans <= ans && getp() > exp((newans-ans)/T))
        {
            if (s) swap(b[swp1],b[swp2]);
            else swap(g[swp1],g[swp2]);
            //这里先判断了不采用新排列的情况,不采用新排列其实就是把原来交换位置的两个学生再换回来
        }
        else
        {
            ans = max(newans,ans);
            //采用新排列,且如果新答案比旧答案更大则取新答案
        }
        T *= 0.99999;//降温
    }

    cout << fixed << setprecision(6) << ans << endl;
    //输出答案,精确到小数点后6位

    return 0;
}

Tips:N 较小时,我们可以设置初始温度较低,结束温度较高,而让降温较慢一些,这样貌似可以让更多的排列被考虑到,也更容易随机过。

退火万岁!