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了这道题吧(完结撒花~~~)