P4044题解

题意简述

一场保龄球比赛一共有 $n(1\le n \le 50)$ 个轮次,每一轮都会有十个木瓶放置在木板道的另一端。每一轮中,选手都有两次投球的机会来尝试击倒全部的十个木瓶。对于每一次投球机会,选手投球的得分等于这一次投球所击倒的木瓶数量。选手每一轮的得分是他两次机会击倒全部木瓶的数量。对于每一个轮次,有如下三种情况:

  1. “全中”:第一次击倒了全部十个木瓶,此时不需要进行第二次,同时计算总分时,下一轮的得分将会被乘 $2$ 计入总分。

  2. “补中”:经过两次才击倒了十个木瓶,计算总分时,选手在下一轮中的第一次的得分将会以双倍计入总分。

  3. “失误”:两次尝试后仍没有击倒全部的木瓶,计算总分时,下一轮的得分不翻倍直接计入总分。

此外,如果第 $n$ 轮是“全中”,那么选手可以进行一次附加轮(也就是一共进行 $n+1$ 轮比赛)。注意附加轮的规则只执行一次,即使第 $n+1$ 轮又打出“全中”,也不会进行第 $n+2$ 轮比赛(也就是说附加轮的成绩不会使得其他轮的分数翻番)。

现在进行了 $n$ 个轮次的保龄球比赛,求将打出的所有轮次的顺序重新排列后,能得到的最高总分数(当然重排后所需要进行的轮数和重排前所进行的轮数是一致)。

题目分析

这个题第一眼看上去是一道状压动规,但至少我没想到状态该怎么设计(如果是将排列作为状态,$O(n!)$ 级别的复杂度无论是空间还是时间都接受不了)。而朴素暴搜的时间复杂度也是 $O(n!)$,仍然无法接受。所以我们可以采用复杂度相对低的非完美算法——模拟退火

模拟退火名称中的“退火”是物理概念,笼统地说是将金属缓慢加热到一定温度,保持足够时间,然后以适宜速度冷却,因为本人不知道对于算法本身帮助不大,所以不多加赘述,有兴趣的同学可以自行查阅资料。但是具体的算法中,需要用到的三个参数的名称和物理有点关系:初始温度 $T_0$、终止温度 $T_E$、衰减系数 $\alpha$

模拟退火算法一般用来求解最优解问题。形式大致是这样的:$f_{min/max}(state)$。注意状态 $state$ 只是一个笼统的概念,可以包括多个参数,表示内容也是多样的,比如平面/空间中的一个点、许多选与不选的状态决策、以及排列等。其中,本题就可以直接认为状态是 $n$ 个轮次的一个排列。另外最优解的标准也是多样的,$f$ 函数的值同样可以包含多种信息,本题的 $f$ 值可以看作某个排列下的得分。这些体现了模拟退火算法的解决问题的广泛性与一般性。

在介绍具体的算法过程之前,先来解释下刚刚的三个参数 和别的概念初始温度 $T_0$、终止温度 $T_E$、衰减系数 $\alpha$ 的具体含义:$T_0$ 在算法中指初始搜索步长(相对较大),$T$ 指终止搜索步长(为保证答案精确,应尽量趋于 $0$,但会增加时间),$\alpha$($\in[0,1)$) 是搜索步长的衰减率(为保证答案精确,应尽量趋于 $1$,但也会增加时间)。(注意状态没有“步长”这个概念(比如本题)时,三个参数并不一定是这个意思,而应该具体情况具体处理,下设问题是求最小值)。

具体实现是这样的:

  1. 将当前搜索步长“温度” $t$ 设置为 $T_0$,并设定一个随机状态 $state_0$。

  2. 在当前决策 $state$ 的当前步长(即温度 $t$)之内再次选择一个新的随机决策 $state'$。

  3. 如果 $state'$ 更优,则一定保留;但如果更劣,也不应直接选择不保留,因为 $state'$ 也有可能拓展到全局最优解,所以我们应有一定几率保留该决策。设 $\Delta=f(state')-f(state)$,一般保留决策的概率取 $e^{-\frac{\Delta}{t}}$(求最大值时取 $e^{\frac{\Delta}{t}}$,下同),这样就能保证 $f(state')$ 越接近最优解,保留的概率最大;越不接近最优解,保留的概率越小。具体实现时则是将 $e^{-\frac{\Delta}{t}}$ 与一个随机实数 $x\in [0,1]$ 相比较,如果 $e^{-\frac{\Delta}{t}}>x$ 则保留,反之不保留。

  4. 将 $t$ 乘上 $\alpha$,并重复第 $2$~$3$ 步,如此一直到 $t\leq T_E$ 为止。

  5. 将 $1$~$4$ 步总体重复 $m$ 次($m$ 的值由答案精度以及数据步长、时限等具体定)。

  6. 注意答案的选择不一定是最终保留的决策,而是应该取过程中所计算出的最优解。

然后我们算一下时间复杂度,设计算 $f(state)$ 的时间复杂度为 $O(q)$,那么总时间复杂度为 $O(mq\log_{\alpha} \frac{T_E}{T_0})$。

回到本题,本题中的状态应是 $n$ 个轮次的排列,所以也就没有“步长”这一概念,但是模拟退火的整体框架是可以应用的:我们只需要每次随机挑选两个轮次并交换,检查合法性后计算得分并比较,然后选择保留/不保留该决策即可。

代码实现

#include<bits/stdc++.h>
using namespace std;
struct nd
{
    int x,y;
}q[55];
int n,m,ans;
int f()//计算得分
{
    int res=0;
    for(int i=1;i<=m;i++)
    {
        res+=q[i].x+q[i].y;
        if(i<=n)
        {
            if(q[i].x==10)
                res+=q[i+1].x+q[i+1].y;
            else if(q[i].x+q[i].y==10)
                res+=q[i+1].x;
        }
    }
    ans=max(ans,res);//用计算出的答案更新
    return res;
}
void SA()//模拟退火
{
    for(double t=1e4;t>1e-4;t*=0.99)//当前步长(温度),本代码中T0 = 1e4,TE = 1e-4,α = 0.99
    {
        int a=rand()%m+1,b=rand()%m+1;//随机挑选两个轮次
        int x=f();//f(state)
        swap(q[a],q[b]);//交换轮次
        if(n+(q[n].x==10)==m)//合法
        {
            int y=f();//f(state')
            int delta=y-x;//Δ
            if(exp(delta/t)<(double)rand()/RAND_MAX)
                swap(q[a],q[b]);//如果更劣,一定概率保留;如果更优,直接保留
        }
        else 
            swap(q[a],q[b]);//不合法,换回来
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d%d",&q[i].x,&q[i].y);
    if(q[n].x==10)
        m=n+1,scanf("%d%d",&q[n+1].x,&q[n+1].y);
    else 
        m=n;
    for(int i=1;i<=100;i++)//重复模拟退火过程,这里 m = 100
        SA();
    printf("%d",ans);
    return 0;
}
posted @ 2022-08-26 12:46  Hadtsti  阅读(1)  评论(0编辑  收藏  举报  来源