P4040保龄球(9.26)

题面:戳这里

题意概括:

有一种叫做保龄球的运动,它有以下几种规则

①每一回合都分上下两轮,每轮都能投回球,每回都能打中一定数量的木瓶

②每一回合的得分为当前这个 回合的得分 + 特殊规则加成

加成如下

①若第一轮能投中全部十个木瓶成为全中,全中直接跳过第二轮,下一回合(包括两轮)得分翻倍

②若第一轮和第二轮一共击中十个木瓶称为补中,下一回合第一轮得分翻倍

③若最后一回合为全中则再加一回合

目标:改变顺序,但不能改变数量(若最后一回合后会有下一回合,则改变顺序后也应有下一回合)


思路:模拟退火(一道很裸的紫题)

模拟退火:乱搞算法
对于每种状态都有一种价值,求出所有状态中的最优价值(求最大值 或 最小值)。
(大部分都是对于 贪心 和 DP 进行乱搞)(状态一定是连续的)

简单而言模拟退火就是对上述图片进行模拟

进行一下简单的定义

我们称现在所处的状态为 当前状态 now

我们称图中红线所处的状态为 下一状态 new

我们称题目中要求的最优解所处的状态为 目标状态

初始温度 T0 ,表示最开始随机的范围大小。

终止温度 TE ,表示我们这次退火结束的温度。

衰减系数 k :初始温度以一定比例缩小为终止温度

一般情况下

T0 = 1e4;

TE = 1e-4;

k = 0.94 ~ 0.99(小数点后可以多加几位,加的越多,模拟次数越多,时间越长,答案更加准确)(系数不能太小否则很难达到最优状态)

下面以求最大值为例

若当前在C点 我们要找到最高点,先随机roll到E点,往下走,有可能roll到其他点

若roll到D点(D点比它矮,但可以看见他的右边有比他大的B点)
则以一定概率跳过去)

若roll到A点(A点比它高,可能为最优解,一定跳过去)

总结:求最大值是遇到比它大的一定跳过去,若是比它小的则以一定概率跳过去(若求最小值则相反即可)

实现:设概率为P

P=e^{Δ/t}(题目要求求最大值)

P=e^{-Δ/t}(题目要求求最小值)

其中 Δ=new - now e为常数2.718281828459

t 一般情况下会用来表示答案变化范围,普遍性取值为温度 T ,这样保证 0<P<1 ,并且 P 随着 Δ 的改变而改变,具体改变是正相关还是负相关,与题目求是最大值还是最小值有关

解释:(此处需要着重理解)

如果求最大值,目标状态比当前更优,即为Δ > 0 ,Δ/t > 0, 所以一定会跳过去

如果Δ < 0. Δ/t < 0, P为(0 , 1 )的一个数,则以一定概率跳归去

(若Δ < 0,且差的特别多,概率会特别小)


以上内容就是模拟退火的精髓

下面回到本题

我们来套用一下模拟退火的思想

本题在求什么呢?最大价值

当前状态是什么呢? 就是当前顺序下的价值

目标状态是什么呢?随机选取两个回合交换后的价值

那下面就很显然了,就是一道很裸的板子题

最后我们进行多次退火就可以了。

什么?你说你不知道该进行多少次退火?不会计算复杂度?

这里有一个很方便的函数,可以帮助你卡着时间过,让你放心且愉悦。

while ((double)clock()/CLOCKS_PER_SEC<0.8)

clock 是一个表示时间单位的函数,具体怎么表示,我也不清楚,每个编译器版本不同,这个函数表示的时间单位一般不同。但是我们有一个表示单位时间为 1s 的常量 CLOCKS_PER_SEC,我们用 clock() 相除就可以换算成以秒作为单位的数了。在题目时间限制为 1s 时,一般我们循环条件是小于 0.8 ,因为如果是 0.9 以上,或者直接为 1 ,是可能会超时的,因为最后一次模拟退火万一时间复杂度突然高起来,就会造成这种情况,所以为了保险,一般就是 0.8 。

如果你不喜欢用卡时,那么这个退火的次数一般进行 1000 ∼ 5000 次。

一般 k 越大 ,这个次数可以适当低,但最好让次数多一些。

经过测试,当 k = 0.95 时,次数需要等于2000,只有90分,3000就可以成功 AC 。别问我怎么知道的。

所以还是卡时安心一些。

对于每次的新的答案,我们用函数 calc() 求出,求得方式与题意相同。


#include<bits/stdc++.h>
#define x first 
#define y second 
using namespace std;
long long read()
{
	long long x = 0, f = 1;char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = x * 10 + ch - '0'; ch = getchar();}
	return x *f;
}
typedef pair<int,int>PII;
int n,m;
PII p[100];
int ans=0;
int calc()//计算当前状态下的价值 
{
    int cnt=0;
    for(int i = 1; i <= m; i++)
    {
        cnt += p[i].x+p[i].y;
        if(i <= n)
        {
            if(p[i].x == 10)//全中的情况 
			{
				cnt += p[i+1].x + p[i+1].y;
			}
            else
			{
				if(p[i].x + p[i].y == 10)//补中的情况 
				cnt += p[i+1].x;
			}    
        }
    }
    ans = max(ans, cnt);
    return cnt;
}
void simulate_anneal()//模拟退火 
{
	for(double t = 1e4; t > 1e-4; t *= 0.99)//温度 
	{
		int x = rand() % m + 1, y = rand() % m + 1;//随机选取两个回合 
		int cnt1 = calc();//记录当前的价值 
		swap(p[x],p[y]);//交换 
		if(n + (p[n].x == 10) == m)//回合数必须相等 
		{
			int cnt2 = calc();//交换后的目标价值 
			int dt=cnt2-cnt1;
			if(exp(dt / t) < (double)rand() / RAND_MAX) swap(p[x], p[y]);//套用公式 
		}	
		else swap(p[x], p[y]);//回合数不想等的话换回来 
	}

}
int main()
{
	n = read();
	for(int i = 1; i <= n; i++)
	{
		p[i].x = read(), p[i].y = read();
	}
	if(p[n].x == 10) m = n + 1, p[n+1].x = read(), p[n+1].y = read();//最后一轮为全中 
	else m = n;
	while((double)clock() / CLOCKS_PER_SEC < 0.8) simulate_anneal();//卡时 
	cout << ans << endl;
	return 0;
}


快去A了这道题吧(完结撒花~~~)

posted @ 2022-10-04 20:39  zhaozixu2006  阅读(44)  评论(0编辑  收藏  举报