一种模拟退火的优化
前言
在使用模拟退火抢 P10447 的最优解时,发现常规的退火在接受一个更劣解进行下一步搜索时,很难再找回原先的更优解。
于是就有了这篇文章。
算法思想
在接受更劣解时,标记当前的更优解。接下来的运行中在超过一个时间阈值时进行回溯。如果找到了比原更优解更优的解,则取消回溯计时。
算法流程:(无跳转则顺序执行)
- 生成一个邻域解并计算其价值。
- 判断其是否优于当前解,若不优,跳
6 。 - 判断其是否优于回溯点的解,若优于,则取消回溯计时。
- 更新当前解。
- 降温,跳
1 。 - 根据一个概率判断是否接受,若接受,且没有回溯点,添加回溯点,接受就跳
4 ,不接受则跳5 。
时间复杂度证明
模拟退火要什么时间复杂度。
示例
这里以 P10447 为例。
题意简述
给出一张图,有
n 个点\frac{n(n-1)}{2} 条边,边无向有权,求不重不漏经过每个点的以0 号点为起始,n-1 号点为终点的最短路径。
思路
这里先实现常规模拟退火。
见到这种路径经过点数确定的题目(TSP)(数据范围还很小qwq),可以想到模拟退火。
模拟退火的主要步骤是
- 先初始化温度,当前解和当前答案。
- 如果温度小于最终温度,跳
6 ,否则跳3 。 - 由当前解生成一个临时的新解,并计算新的答案。
- 判断是否接受该临时解,接受则更新解和答案,不接受则回退到上个解。
- 降温,跳
2 。 - 输出答案,结束。
模拟退火的核心是 Metropolis 准则,它决定了如何接受当前解的邻域解。在温度
T 下,对于一个新解与当前解的目标函数差值\varDelta E=E_{new}-E_{old} ,Metropolis 准则的接受概率为:1 &\text{}\varDelta E \le 0\\ e^{-\frac{\varDelta E}{T}} &\text{}\varDelta E \gt 0 \end{cases} 其中:
- 当
\varDelta E \le 0 时,接受新解,因为新解比当前解更优。- 当
\varDelta E \gt 0 时,接受新解的概率是e^{-\frac{\varDelta E}{T}} ,即接受差解的概率随温度降低而减小。在高温时,算法允许接受较差的解,从而探索更广泛的解空间。随着温度降低,算法更倾向于只接受更优的解,减少“跳出局部最优”的概率。
将解定义为一条从
具体实现
请自行卡时和调参(我给的代码极大概率能过,但并不优)。
存图直接用邻接矩阵:
int dist[21][21];
求邻域解:
vector<int> get_solve(vector<int> a) {
int i = rand() %(a.size()-2)+1, j = rand() % (a.size()-2)+1;//防止交换起点和终点
while (i == j) j = rand() % (a.size()-2)+1;//防止交换同样的点
swap(a[i], a[j]);
return a;
}
求代价:
int get_dist(vector<int> a) {
int ret =0;
for (int i = 1; i < a.size(); i++) {
ret += dist[a[i - 1]][a[i]];//顺序求解,n只有20,不用考虑什么奇怪O(1)
}
return ret;
}
模拟退火主要代码:
vector<int> SA(int n) {
vector<int> a;
double t = 10000, alpha =1-1e-3, e = 1e-4;//参数分别为初始温度,衰减因子,温度下限
for (int i = 0; i < n; i++) {
a.push_back(i);
}
vector<int> best_path = a;
int best_dist = get_dist(a);
while (t > e) {
for(int i=0;i<200;i++){
vector<int> neighbor = get_solve(a);//创建邻域解并计算代价
int distn = get_dist(neighbor);
int disto = get_dist(a);
if (distn<disto/*如果更优直接接受*/||exp((disto-distn)/t)>(rand()/(RAND_MAX + 1.0))/*如果不更优则根据一个概率(取决于与当前解代价的差距)接受*/) {
a = neighbor;
if(distn<best_dist){//如果比最优解还优就更新
best_dist = distn;
best_path = a;
}
}
}
t *= alpha;//降温
}
return best_path;
}
主函数部分就输入图然后跑退火,跑完退火根据返回路径求出代价输出。
就不贴代码了。
优化
考虑到在极限卡时时模拟退火的正确性会大打折扣。
所以我们可以用这篇文章的思想来优化,大体流程在上文已给出。
具体的,定义一个布尔变量来表示是否开启回溯计时器。以及一个计时器变量和回溯解和回溯代价。
仅需修改模拟退火主要代码:
vector<int> SA(int n) {
vector<int> a,mark;
double t = 10000, alpha =1-1e-3, e = 1e-4;
for (int i = 0; i < n; i++)a.push_back(i);
vector<int> best_path = a;
int best_dist = get_dist(a),mark_dist = get_dist(a),cnt;
bool flag=false;
while (t > e) {
for(int i=0;i<200;i++){
if(flag){
cnt--;//计时器
if(cnt<=0){//归零,回溯
flag=false;
a=mark;
}
}
vector<int> neighbor = get_solve(a);
int distn = get_dist(neighbor);
int disto = get_dist(a);
if (distn < disto) {
if(distn < markdist)flag = false;
a = neighbor;
if(distn<best_dist){
best_dist = distn;
best_path = a;
}
}else if(exp((disto-distn)/t)>(rand()/(RAND_MAX + 1.0))){
mark_dist = disto;
mark = a;
a = neighbor;
flag = true;
cnt = 5 * sqrt(t);//可以温度越小回溯计时越小。卡时
}
}
t *= alpha;
}
return best_path;
}
那么这道题就愉快地做完了。
例题
P10447,P1433,P1171