当然是退火啦!QwQ
题目描述
现在有n枚金币,它们可能会有不同的价值,现在要把它们分成两部分,要求这两部分金币数目之差不超过1,问这样分成的两部分金币的价值之差最小是多少?
输入输出格式
输入格式:
每个输入文件中包含多组测试数据,输入文件的第一行是一个正整数T(T≤20)T(T≤20)T(T≤20),用来说明文件中有多少组测试数据。接下来将依次给出所有测试数据的描述,每组测试数据的第一行是一个正整数n(n≤20)n(n≤20)n(n≤20),表示共有nnn枚金币。第二行有nnn个正整数viv_ivi,分别给出每一枚金币的价值。
输出格式:
对每一组输入数据,输出一个非负整数,表示分成的两部分金币的价值差别的最小值。
分析:
搜索当然是可以的,但是也要考虑到省选的进程(大雾),毕竟考场上写dfs这东西,万一写错了点什么,又不知道是写挂了死循环还是真的TLE掉了。而且,此题dfs剪枝并不是十分理想,虽然可以做,但是效果不好。
对于最优化问题,我们当然可以立刻想到一个方法,即随机的分配这些金币,统计两组的差值并加以修改,但我们可以意识到这种修改得出的答案往往不令人满意。
为了平衡时间消耗巨大和答案不准确的两种算法,我们考虑采用随机化搜索的一种——模拟退火算法。这种算法模仿了金属退火降温的规律,能够在给定的时间内得出一组较优的解。并且,大部分情况下能够给出确切地最优解。
模拟退火算法:
基本实现步骤:
- 生成一组随机的解AnsiAns_iAnsi
- 对于新生成的解,求出与当前解差值δ=Ansi−Ansnow\delta=Ans_i-Ans_{now}δ=Ansi−Ansnow
- 如果δ<0,\delta<0,δ<0,表示这组解更加优秀,我们无条件地接受。否则我们以一个固定概率接受,这个概率P=e−δkTP=e^{-\frac{\delta}{kT}}P=e−kTδ,T 是当前的温度,k是玻尔兹曼常数。不难看出,T减少(降温)后,我们要让接受非最优解的概率降低,因为系统需要趋于稳定和能量最低
- 降温,将T缩小固定的大小。
在本题中我们按照这个套路走下去,首先随机生成一个金币序列,将前[n2][\frac{n}{2}][2n]个金币分派给组A,n−[n2]n-[\frac{n}{2}]n−[2n]个金币分配给组B。每次随机交换两组中各一个金币,统计这两组金币价值的差δ\deltaδ。然后我们按照上述准则选择是否接受这个答案。
- 在程序实现时,为了调控这个概率,我们省去了玻尔兹曼常数,在左边乘上INT_MAX,在右边用一个随机生成的数字表示概率。如果随机数大于左边的参数,那么我们接受,否则,我们拒绝。
- 可以看出,随着T的减小左边参数越来越大,接受非最优解的概率也越来越小。
最后不要忘记降温。而降温也是个玄学过程。我们推荐使用等比方式降温,设置降温速度e(e<1)e(e<1)e(e<1).每次取舍后让T=eTT=eTT=eT.至于e,我们要根据具体题目选择。e比较大,答案精确,耗时较大;e比较小,答案未必精确,但运行速度快。
本题中我们选择T=2048,e=0.998,末温eps=1e−7T=2048,e=0.998,末温eps=1e-7T=2048,e=0.998,末温eps=1e−7,并对每组数据分别退火14次,即可通过本题。当然,退火次数可能更少些,或者其他参数调整也可以通过。
- 参数的调整本身也是模拟退火的重要部分。当你Wa掉的时候不一定是算法不对,很可能只是参数不优秀。因此,可以修改参数多次提交代码以获得高分。当然,模拟退火在IOI和ACM中用武之地可能更大些,至于OI,如果真的走投无路并且相信自己的人品,那么,就放心地退火吧!
代码
// luogu-judger-enable-o2
#include<bits/stdc++.h>
#define maxn 50
#define eps 1e-7
#define ll long long
using namespace std;
inline ll read()
{
ll n=0,k=1;
char c=getchar();
while(c>'9'||c<'0') {if(c=='-') k=-1; c=getchar();}
while(c>='0'&&c<='9') n=(n<<1)+(n<<3)+(c^48),c=getchar();
return n*k;
}
int n,m;
int T;
ll arr[maxn];
ll ans=99999999999;
inline int count()
{
int res1=0,res2=0;
for (register int i=1;i<=n;++i)
if (i<=(n+1)/2) res1+=arr[i];
else res2+=arr[i];
return abs(res1-res2);
}
inline void SA()
{
double Tmp =2048.0;
while(Tmp>eps)
{
int s1=rand()%((n+1)/2)+1,s2=rand()%((n+1)/2)+((n+1)/2);
if (s1<=0||s1>n||s2<=0||s2>n) continue;
swap(arr[s1],arr[s2]);
int newans=count();
int del=ans-newans;
if (del>0) ans=newans;
else if (exp((double)((double)del/Tmp))*RAND_MAX<=rand()) swap(arr[s1],arr[s2]);
Tmp*=0.998;
}
}
int main()
{
srand(time(NULL));
T=read();
while(T--)
{
ans=99999999999;
n=read();
for(register int i=1;i<=n;++i)
arr[i]=read();
random_shuffle(arr+1,arr+n+1);
for(register int i=1;i<=2;++i)
SA(),SA(),SA(),SA(),SA(),SA(),SA();
printf("%lld\n",ans);
}
return 0;
}