现代优化算法探究 模拟退火算法

相较于完全贪心的单点爬山算法,模拟退火用概率接受新解的方式,对贪心法容易陷入局部最优解的缺陷进行了改进,是一种常用的在大的搜索空间中逼近全局最优解的元启发式方法。这里先大致描述算法。然后先用一个简单的求函数最小值例子,解释我写的c++模拟退火算法模板的基本使用,之后用其解决15节点的TSP问题,并与动态规划得到的全局最优解进行比较,解释模拟退火算法解决一般问题的方法和效果。

算法描述:

随机从一个合法解s开始,从s扩展出足够的状态,对于新的状态,若新状态优于原始状态,则接受新解,否则,以一定的概率接受新解(这保证了算法有机会跳出局部最优解),而该概率随算法运行时间下降,类似于粒子退火的过程(这也是合理的,因为随着算法的进行,当前解就逐步逼近于最优解,那么接受新解的概率自然要下降)。

其实模拟退火就是不停的基于当前最优解,试图进行改良,并且以概率接受不是那么好的解来避免陷入局部最优解,并不复杂。

算法伪代码:

模拟退火模板题: 求函数(x - 2)*(x + 3)*(x + 8)*(x - 9)的最小值

分析:

直接使用模板即可。在定义问题时,关键要素有如下几个:

1. 状态的定义

2. 对应于一个状态的效用值的计算

3. 产生新解的方式

然后还要注意的是,参数的调整,比如时间到温度如何转化,迭代次数等等。参数调整也是所有这些非经典算法都要考虑的问题。

代码:

#include<iostream>
#include<algorithm>
#include<map>
#include<set>
#include<queue>
#include<sstream>
#include<cmath>
#include<iterator>
#include<bitset>
#include<stdio.h>
#include<unordered_set>
#include<ctime>
using namespace std;
#define _for(i,a,b) for(int i=(a);i<(b);++i)
#define _rep(i,a,b) for(int i=(a);i<=(b);++i)
typedef long long LL;
const int INF = 1 << 30;
const int maxn = 100005;
const double eps = 1e-6;

template< typename T = double>
class State {    //状态的定义
public:
    T x, val;
    T getval(State<T> & s) {   //效用值得计算
        auto x = s.val;
        return (x - 2)*(x + 3)*(x + 8)*(x - 9);
    }
    T getval(T & x) { return (x - 2)*(x + 3)*(x + 8)*(x - 9); }

    State(T x) :x(x), val(getval(x)) {}
    State() {};
    State(const State<T> & rhs) :x(rhs.x), val(rhs.val) {}

};


template<typename T = double>
class SA {             //Simulated Annealing  
private:
    static double schedule(int t,double tem) {
        return tem*temrate;
    }
    static State<T> randomNe(State<T> & s) {          //产生新解的方式
        double y = s.x + 2 * ((double)(rand() % 1000)) / 1000 - 1;
        return State<T>(y);
    }
    static const int maxtry = 100;

public:
    static const double temrate;
    static const double inittem;
    State<T> minans(State<T> s) {
        State<T> current = s, ans = s;
        double tem = SA::inittem;
        for (int t = 1;; t++) {
            tem=schedule(t,tem);
            if (tem <= eps)return ans;
            for (int i = 0; i<maxtry; ++i) {
                State<T> ne = randomNe(current);
                double detaE = current.val - ne.val;
                if (detaE > 0)ans = current = ne;
                else if (rand()<exp(detaE / tem) * 0x7fff) current = ne;
            }
            //cout << current.val << endl;
        }
    }
};

const double SA<double>::inittem = 1000;
const double SA<double>::temrate = 0.99;

int main()
{
   // freopen("C:\\Users\\admin\\Desktop\\in.txt", "r", stdin);
   // freopen("C:\\Users\\admin\\Desktop\\out.txt", "w", stdout);

    srand((unsigned)time(NULL));

    double s0 = rand() % 100 / 10;

    SA<double> solver;
    State<double> ans = solver.minans(State<double>(s0));
    cout << ans.x << " " << ans.val << endl;


    return 0;
}

最后结果可以大致稳定在x=6.5左右,某次运行结果为x=6.492 y=-1549.72.

在求解过程中,当前解的演变过程如下:

可以看到随着迭代的次数增加,当前解逐步趋向于最小值,过程类似于粒子退火,初始时能量大,无序性大,最后趋于稳定。

该函数的图形如下:

 

可以看出得出的结果接近于全局最优值。

15节点的TSP问题:

问题描述:

有15个城市, 给出每个城市的坐标,每两个城市之间相互有路径可走,需要消耗一定的费用。问一个商人,想从某个起点出发经过每个城市一次且仅仅一次最后回到起点所需的最小费用是多少。本题直接把两点的距离作为费用。

数据:

53.7121   15.3046    51.1758    0.0322    46.3253   28.2753    30.3313    6.9348
56.5432   21.4188    10.8198   16.2529    22.7891   23.1045    10.1584   12.4819
20.1050   15.4562    1.9451    0.2057    26.4951   22.1221    31.4847    8.9640
26.2418   18.1760    44.0356   13.5401    28.9836   25.9879   

从上到下,从左往右,每两个数代表一个城市的x 和y 坐标。

分析:

仍然使用模拟退火算法。同样需要考虑三个问题:状态的定义,效用值得计算,产生新解的方式。 具体看代码。

代码:

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<vector>
#include<stack>
#include<algorithm>
#include<map>
#include<set>
#include<queue>
#include<sstream>
#include<cmath>
#include<iterator>
#include<bitset>
#include<stdio.h>
#include<unordered_set>
#include<ctime>
#include<assert.h>
using namespace std;
#define _for(i,a,b) for(int i=(a);i<(b);++i)
#define _rep(i,a,b) for(int i=(a);i<=(b);++i)
typedef long long LL;
const int INF = 1 << 30;
const int maxn = 15;

struct Node {
	double x, y;
};
Node a[maxn];
int order[maxn];
double dist[maxn][maxn];

template< typename T = double>
class State {       //状态定义
public:
	T val;
	int path[maxn];

	T getval() {       //效用值定义
		double x = 0;
		for (int i = 1; i <= maxn; ++i) x += dist[path[i%maxn]][path[i - 1]];
		return val=x;
	}
	//double getval(double & x) { return (x - 2)*(x + 3)*(x + 8)*(x - 9); }

	State() {
		memcpy(path, order, sizeof(path));
		random_shuffle(path, path + maxn);
		val = getval();
	};
	State(const State<T> & rhs) :val(rhs.val) { memcpy(path, rhs.path, sizeof(path)); }
};


template<typename T = double>
class SA {                  //Simulated Annealing  
private:
	static double schedule(int t) {
		return 100- 0.05*t;
	}
	static State<T> randomNe(State<T> & s) {             //产生新解
		State<T> ne = s;
		int head = max(1, rand() % maxn), tail = max(1, rand() % maxn);
		if (head > tail)swap(head, tail);
		reverse(ne.path+head, ne.path + tail);
		ne.val=ne.getval();
		return ne;
	}
	static const int maxtry = 1000;

public:

	State<T> minans(State<T> & s) {
		State<T> current = s, ans = s;
		for (int t = 1;; t++) {
			double tem = schedule(t);
			if (tem <= 0)return ans;
			for (int i = 0; i<maxtry; ++i) {
				State<T> ne = randomNe(current);
				double detaE = current.val - ne.val;
				if (detaE > 0)ans = current = ne;
				else if (rand()<exp(detaE / tem) * 0x7fff) current = ne;
			}
			//printf("%lf\n", ans.val);
		}
	}
};

int main()
{
	//freopen("C:\\Users\\admin\\Desktop\\in.txt", "r", stdin);
	//freopen("C:\\Users\\admin\\Desktop\\out.txt", "w", stdout);

	srand((unsigned)time(NULL));

	for (int i = 0; i < maxn; ++i) {
		scanf("%lf%lf", &a[i].x, &a[i].y);
	}
	for (int i = 0; i < maxn; ++i) order[i] = i;
	for (int i = 0; i < maxn; ++i)
		for (int j = 0; j < maxn; ++j)
			dist[i][j] = sqrt((a[i].x - a[j].x)*(a[i].x - a[j].x) + (a[i].y - a[j].y)*(a[i].y - a[j].y));


	SA<double> solver;
	State<double> s;
	State<double> ans = solver.minans(s);


	printf("%lf\n", ans.val);
	for (int i = 0; i < maxn; ++i) {
		printf("%d ", ans.path[i]);
	}

	return 0;
}

结果能稳定在170上下,某次运行结果如下:

161.241317 (最小总费用)

2 14 10 6 12 8 5 7 9 3 11 13 1 0 4  (路径,城市下标从0开始)

解的演变过程如下:

动态规划验证:

由于这里的重点只在模拟退火算法,这里只给出动态规划的状态转移方程,不做更多的解释了。

dp(S,i) 表示当前处在城市i上,已经走过bitset集合S中的城市,所耗费的最小费用。

所以状态转移方程如下:

其中 dist(i,j) 表示城市i和j之间的距离(或是说费用),初始条件dp(1,0)=0,时间复杂度大致为

代码:

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<vector>
#include<stack>
#include<algorithm>
#include<map>
#include<set>
#include<queue>
#include<sstream>
#include<cmath>
#include<iterator>
#include<bitset>
#include<stdio.h>
#include<unordered_set>
#include<ctime>
#include<assert.h>
using namespace std;
#define _for(i,a,b) for(int i=(a);i<(b);++i)
#define _rep(i,a,b) for(int i=(a);i<=(b);++i)
typedef long long LL;
const int INF = 1 << 30;
const int maxn = 15;

struct Node {
	double x, y;
};
Node a[maxn];
int order[maxn];
double dist[maxn][maxn];

double d[1 << maxn][maxn];
int p[1 << maxn][maxn];

void print(int S, int u, double & tot) {
	if (u == 0) {
		cout << u << " ";
		//cout << endl << tot;
		return;
	}

	tot += dist[u][p[(S)][u]];
	cout << u << " ";
	print(S ^ (1 << u), p[S][u], tot);
}

int main()
{
	//freopen("C:\\Users\\admin\\Desktop\\in.txt", "r", stdin);
	//freopen("C:\\Users\\admin\\Desktop\\out.txt", "w", stdout);

	srand((unsigned)time(NULL));

	for (int i = 0; i < maxn; ++i) {
		scanf("%lf%lf", &a[i].x, &a[i].y);
	}
	for (int i = 0; i < maxn; ++i) order[i] = i;
	for (int i = 0; i < maxn; ++i)
		for (int j = 0; j < maxn; ++j)
			dist[i][j] = sqrt((a[i].x - a[j].x)*(a[i].x - a[j].x) + (a[i].y - a[j].y)*(a[i].y - a[j].y));

	int m = 1 << maxn;
	for (int i = 0; i < m; ++i)
		for (int j = 0; j < maxn; ++j)
			d[i][j] = INF;
	d[1][0] = 0;
	for (int S = 1; S < m; ++S) {
		if (!S & 1) continue;
		for (int i = 0; i < maxn; ++i) {
			if (S&(1 << i)) {
				for (int j = 0; j < maxn; ++j)if (S&(1 << j)) {
					if (d[S][i] > d[S ^ (1 << i)][j] + dist[i][j]) {
						d[S][i] = min(d[S][i], d[S ^ (1 << i)][j] + dist[i][j]);
						p[S][i] = j;
					}
				}
			}
		}
	}
	double ans = INF;
	int ind;
	for (int i = 0; i < maxn; ++i) {
		if (ans > d[m - 1][i] + dist[i][0]) {
			ans = min(ans, d[m - 1][i] + dist[i][0]);
			ind = i;
		}
	}

	cout << ans << endl;
	double tot = 0;
	print((1 << maxn) - 1, ind, tot);

	return 0;
}

所得结果如下:

161.241

1 13 11 3 9 7 5 8 12 6 10 14 2 4 0

与以上模拟退火算法结果一致。

现在将maxn调至50,模拟退火算法很快给出结果:

300.458339

4 37 33 2 30 27 43 0 23 48 1 32 31 13 11 17 3 35 36 8 25 12 34 28 15 45 18 16 14 10 6 40 41 21 44 38 47 5 22 7 49 24 46 9 29 42 39 26 19 20

而因动态规划的时空复杂度过高,此时动态规划无法求解。

结论:

模拟退火算法能有效求解一般优化问题,并且能通过调整参数在较短的时间内得到很好的结果。是一个在找不到多项式复杂度算法的情况下求解一般较大规模优化问题的较好选择。

猜你喜欢

转载自blog.csdn.net/tomandjake_/article/details/81037548
今日推荐