模拟退火求解书店购书问题——欧皇算法
模拟退火算法是类似于物体热力学平衡的现象,这基于高温物体退火方式和组合优化问题求解方式的种种共同特性。当一个物体进行退火处理时,首先对物体进行加热至熔化,此时物体粒子会产生剧烈的运动,随之对物体逐渐的降温,同时粒子热运动减弱。随着温度逐渐降低,粒子运动逐渐有序,当温度降至结晶温度后,物体结晶形成稳定的状态。在退火过程中物体从高能量状态到低能量状态,以及偶尔由低至高能量态波动。类比组合优化问题,整个计算执行过程类似退火过程,候选结果是物体状态,目标函数看作状态指标值,温度变化就是状态指标值得变化,当温度下降到结晶点时,目标函数收敛,此时是局部极值点。
模拟退火算法是一种启发式算法,所谓启发式算法可以简单的理解为数据前后有联系并以此利用中间信息进行计算的算法。模拟退火算法与爬山算法极为类似,前者会计算限定的整个范围的数据,而后者当遇到一个极值的时候便会结束,因此后者的局限性非常大,虽然非常的容易得到一个极值,但是计算的极值并不是我们希望得到的那个极值。无论哪种算法都是贪婪算法的思想,都具有一定的狭隘性。
模拟退化算法的求解步骤为:
1.定义初始温度
2.计算当前数据及其相邻数据的解
3.计算一个接受值,根据相邻解的大小进行判断是否接受这个新解
4.温度系数到达一定的阈值结束迭代并得出最小值
回到题目上,我们要从15个书店里购进20本不同的书,每个书店都有不同的运费,在同一个书店购买不同的书运费不会累计,也就是说我们可以在一个书店购买不同的书而运费只需一次即可。如何选择才能使得我们求出的花费总和最小。首先,如果用蒙特卡罗算法算法或者是穷举,其时间复杂度已经高达15的20次方了,估计算到死也不一定算的出最小的那个值。这时我们可以用模拟退火来求解。
先定义一个解空间,该解空间的长度为要购书数量的数组,将数组随机赋值,每一个值都表示将要在哪一个书店进行购书,如果我们要计算相邻数据的值,只需要随机给一个位置随机赋值,就可以计算出一个新的值。然后比较相邻解的大小和接受概率判断是否接受这个解。并通过控制温度系数和迭代次数控制程序运行的次数就能求出一个最优解(由于给定初始条件不一样会导致算出来的解并不是全局最优解,而是一个局部最优解),以下给出完整代码:
import java.util.*;
public class Main {
static Scanner scanner=new Scanner(System.in);
static Random r=new Random();
static int index[]=new int[15];//表示购买序列是在哪家书店买的书
static int count;//计算花费
static int moneysum(int way[],int freight[],int book[][]) {//计算总花费
for(int i=0;i<index.length;i++) {//初始化为0
index[i]=0;
}
count=0;
for(int i=0;i<way.length;i++) {//对应书店计数
index[way[i]-1]++;
}
for(int i=0;i<index.length;i++) {//计算运费
if(index[i]!=0) {
count=count+freight[i];
}
}
for(int i=0;i<way.length;i++) {//计算运费加书费
count=count+book[way[i]-1][i];
}
return count;
}
static void wayrandom(int way1[],int way2[]) {//将购买序列随机化更新
for(int i=0;i<way1.length;i++) {
way2[i]=way1[i];
}
int index1=r.nextInt(16);
int index2=r.nextInt(16);
if(index1==15) {
index1--;
}
if(index2==0) {
index2++;
}
way2[index1]=index2;
}
public static void main(String[] args) {
int book[][]=new int[15][20];//每本书在该书店的价格
int freight[]=new int[15];//运费
int way1[]=new int[20];//初始购买序号
int way2[]=new int[20];//随机跟新后的序号
List<Integer> list=new LinkedList<Integer>();//存放每次较小的结果
for(int i=0;i<15;i++) {
for(int j=0;j<20;j++) {
book[i][j]=scanner.nextInt();
}
}
for(int i=0;i<15;i++) {
freight[i]=scanner.nextInt();
}
for(int i=0;i<20;i++) {
way1[i]=scanner.nextInt();
}
int T0=1000;//初始温度
double T=T0;//迭代中温度会发生改变,第一次迭代时温度就是T0
int maxd=5000;//最大迭代次数
int maxdd=200;//每个温度下的迭代次数
double coefficient=0.95;//温度衰减系数
int money1;//初始序列的花费
int money2;//更新后的花费
for(int i=0;i<maxd;i++) {
for(int j=0;j<maxdd;j++) {//开始爆搜,我很擅长,哈哈哈
money1=moneysum(way1,freight,book);
wayrandom(way1,way2);
money2=moneysum(way2,freight,book);
if(money2<money1) {//如果更新后的总花费更小
for(int k=0;k<way1.length;k++) {//初始数组改变为更新后的数组
way1[k]=way2[k];
}
list.add(money2);//结果添加进容器
}else {//如果更新后的总花费更大
double exp=Math.E;
double x=-((money2-money1)/T);
double p=Math.pow(exp,x);//计算接受概率,一个e的负x次方的函数
double q=r.nextDouble();//生成一个0到1之间的随机数
if(q<p) {//若接受概率更大
for(int k=0;k<way1.length;k++) {
way1[k]=way2[k];//初始数组改变为更新后的数组
}
}
}
}
T=T*coefficient;//改变温度系数
}
int ans=Integer.MAX_VALUE;
for(int i=0;i<list.size();i++) {
ans=Math.min(ans, list.get(i));//遍历容器求出最小值
}
System.out.print(ans);
}
}