写在开头:
最近师妹的结课作业问我,关于使用粒子群求解TSP问题的思路。我想了想,自己去年的作业用的是遗传算法,貌似有些关联,索性给看了看代码。重新学习了一遍粒子群算法,在这里记录一下,算是对知识的总结,巩固一下。
本文主要是使用粒子群来求解旅行商问题,即TSP问题,这里主要讲解代码和实现思路,原理会简单带过。详细的具体原理,请读者移步参考链接。
本文将从如下几个方面进行描述:
粒子群算法简称PSO,它的基本思想是模拟鸟群的捕食行为。设想这样一个场景:一群鸟在随机搜索食物。在这个区域里只有一块食物。所有的鸟都不知道食物在那里。但是他们知道当前的位置离食物还有多远。那么找到食物的最优策略是什么呢。最简单有效的就是搜寻目前离食物最近的鸟的周围区域。
PSO从这种模型中得到启示并用于解决优化问题。PSO中,每个优化问题的解都是搜索空间中的一只鸟。我们称之为“粒子”。所有的粒子都有一个由被优化的函数决定的适应值(fitness value),每个粒子还有一个速度决定他们飞翔的方向和距离。然后粒子们就追随当前的最优粒子在解空间中搜索。
PSO 初始化为一群随机粒子(随机解)。然后通过迭代找到最优解。在每一次迭代中,粒子通过跟踪两个”极值”来更新自己。第一个就是粒子本身所找到的最优解,这个解叫做个体极值pBest。另一个极值是整个种群目前找到的最优解,这个极值是全局极值gBest。另外也可以不用整个种群而只是用其中一部分作为粒子的邻居,那么在所有邻居中的极值就是局部极值。
TSP问题(Travelling Salesman Problem)即旅行商问题,又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。
TSP问题是一个组合优化问题。该问题可以被证明具有NPC计算复杂性。TSP问题可以分为两类,一类是对称TSP问题(Symmetric TSP),另一类是非对称问题(Asymmetric TSP)。
首先整理一下具体实现逻辑,具体流程如下:
进化——其实就是交换
交换序列对由3部分组成:自身交换序列、自身最优解的交换序列、全局最优解的交换序列。
打印结果
使用面向对象的思想,首先定义一个city.java城市实体类,有id、name、经度和纬度属性。
public class City {
private int mId; //城市编号
private String mName; //城市名称
private float mLongitude; //经度
private float mLatitude; //经度
public City(int id, String name, float longitude, float latitude) {
mId = id;
mName = name;
mLongitude = longitude;
mLatitude = latitude;
}
定义一个粒子实体类Unit.java,包括行走城市的路径path和适应度fitness。并添加两个方法,输出城市序列和计算自己的适应度。
private int[] mPath; //行走的城市路径,存储城市编号
private int mFitness; //适应度值,为当前个体走这个路径的总距离。越小越好。
public Unit(int[] path) {
mPath = path;
mFitness = calculateFitness();
}
public void printPath() {
if (mPath == null) {
System.out.println("mPath为null,当前个体的路径为空");
} else {
for (int i = 0; i < mPath.length - 1; i++) {
System.out.print(CityLab.getInstance().getmCities().get(mPath[i]).getName() + "——》");
}
System.out.println(CityLab.getInstance().getmCities().get(mPath[mPath.length - 1]).getName());
}
}
public void upDateFitness() {
this.mFitness = calculateFitness();
}
/**
* 计算当前路径的适应值,即为路径长度
*/
public int calculateFitness() {
//根据经纬度计算距离
//近似计算:0.00001度,距离相差约1米;0.01,距离相差1000米.1度,距离相差100km
int distance = 0; //单位千米(km)
int n = mPath.length;
for (int i = 1; i < n; i++) {
City c1 = CityLab.getInstance().getmCities().get(mPath[i - 1]);
City c2 = CityLab.getInstance().getmCities().get(mPath[i]);
distance += Math.sqrt(Math.pow(100 * (c1.getLatitude() - c2.getLatitude()), 2) + Math.pow(100 * (c1.getLongitude() - c2.getLongitude()), 2));
}
distance += Math.sqrt(Math.pow(100 * (CityLab.getInstance().getmCities().get(mPath[0]).getLatitude() - CityLab.getInstance().getmCities().get(mPath[n - 1]).getLatitude()), 2) + Math.pow(100 * (CityLab.getInstance().getmCities().get(mPath[0]).getLongitude() - CityLab.getInstance().getmCities().get(mPath[n - 1]).getLongitude()), 2));
return distance;
}
接下来,定义交换对实体类,SO.java,包含两个属性x和y,用来做交换的。
public class SO {
private int x;
private int y;
public SO(int x, int y) {
this.x = x;
this.y = y;
}
最后使用单例模式来管理所有的城市列表CityLab.java
/**
* 城市单例
*/
public class CityLab {
private static CityLab mCityLab = null;
private ArrayList mCities = new ArrayList<>();
private CityLab() {
//读取文件,添加城市信息到mcities中
String filename = "d://city.csv";
String strbuff;
BufferedReader data = null;
try {
data = new BufferedReader(new InputStreamReader(
new FileInputStream(filename)));
int id = 1;
while (true) {
//读取一行数据:北京,116.41667,39.91667
strbuff = data.readLine();
if (strbuff == null) {
break;
}
String[] arr = strbuff.split(",");
City c = new City(id, arr[1], valueOf(arr[2]), valueOf(arr[3]));
id++;
mCities.add(c);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static CityLab getInstance() {
if (mCityLab == null) {
mCityLab = new CityLab();
}
return mCityLab;
}
public ArrayList getmCities() {
return mCities;
}
}
实体类我们定义好了,接下来看具体实现粒子群求解TSP问题的过程。
具体PSO.java类如下:
/**
* 粒子群求解TSP旅行商问题
* 更新公式:Vii=wVi+ra(Pid-Xid)+rb(Pgd-Xid)
*/
public class PSO {
private int scale; //种群规模
private ArrayList mUnits = new ArrayList<>(); //粒子群
private int MAX_GEN;// 迭代次数
private float w; //惯性权重
private int cityNum = 25; //城市数量
private HashMap Pd = new HashMap<>(); //一颗粒子历代中出现最好的解
private Unit Pgd; // 整个粒子群经历过的的最好的解,每个粒子都能记住自己搜索到的最好解
private int bestT;// 最佳出现代数
private ArrayList> listV = new ArrayList<>(); //自身交换序列,即所谓的惯性因子
Random random = new Random();
/**
* 构造方法
*
* @param scale
* @param MAX_GEN
* @param w
*/
public PSO(int scale, int MAX_GEN, float w) {
this.scale = scale;
this.MAX_GEN = MAX_GEN;
this.w = w;
}
/**
* 初始化参数配置
*/
private void init() {
cityNum = CityLab.getInstance().getmCities().size();
random = new Random(System.currentTimeMillis());
}
/**
* 初始化种群
*/
private void initGroup() {
for (int k = 0; k < scale; k++) {
int[] path = new int[cityNum];
for (int i = 0; i < cityNum; ) {
//随机生成一个城市路径
//int s = random.nextInt(max)%(max-min+1) + min;
int s = random.nextInt(65535) % cityNum;
int j;
for (j = 0; j < i; j++) {
if (s == path[j]) {
break;
}
}
if (i == j) {
path[i] = s;
i++;
}
}
Unit unit = new Unit(path);
mUnits.add(unit);
}
}
/**
* 初始化自身的交换序列即惯性因子
*/
private void initListV() {
for (int i = 0; i < scale; i++) {
ArrayList list = new ArrayList<>();
int n = random.nextInt(cityNum - 1) % (cityNum); //随机生成一个数,表示当前粒子需要交换的对数
for (int j = 0; j < n; j++) {
//生成两个不相等的城市编号x,y
int x = random.nextInt(cityNum - 1) % (cityNum);
int y = random.nextInt(cityNum - 1) % (cityNum);
while (x == y) {
y = random.nextInt(cityNum - 1) % (cityNum);
}
//x不等于y
SO so = new SO(x, y);
list.add(so);
}
listV.add(list);
}
}
public void solve() {
initGroup();
initListV();
//挑选最好的个体
for (int i = 0; i < scale; i++) {
Pd.put(i, mUnits.get(i));
}
Pgd = Pd.get(0);
for (int i = 0; i < scale; i++) {
if (Pgd.getFitness() > Pd.get(i).getFitness()) {
Pgd = Pd.get(i);
}
}
System.out.println("初始化最好结果为:" + Pgd.getFitness());
Pgd.printPath();
// 进化
evolution();
// 打印
System.out.println("==================最后粒子群=====================");
System.out.println("最佳长度出现代数:");
System.out.println(bestT);
System.out.println("最佳长度");
System.out.println(Pgd.getFitness());
System.out.println("最佳路径:");
Pgd.printPath();
}
/**
* 进化
*/
private void evolution() {
for (int t = 0; t < MAX_GEN; t++) {
for (int k = 0; k < scale; k++) {
ArrayList vii = new ArrayList<>();
//更新公式:Vii=wVi+ra(Pid-Xid)+rb(Pgd-Xid)
//第一部分,自身交换对
int len = (int) (w * listV.get(k).size());
for (int i = 0; i < len; i++) {
vii.add(listV.get(k).get(i));
}
//第二部分,和当前粒子中出现最好的结果比较,得出交换序列
//ra(Pid-Xid)
ArrayList a = minus(mUnits.get(k).getPath(), Pd.get(k).getPath());
float ra = random.nextFloat();
len = (int) (ra * a.size());
for (int i = 0; i < len; i++) {
vii.add(a.get(i));
}
//第三部分,和全局最优的结果比较,得出交换序列
//rb(Pgd-Xid)
ArrayList b = minus(mUnits.get(k).getPath(), Pgd.getPath());
float rb = random.nextFloat();
len = (int) (rb * b.size());
for (int i = 0; i < len; i++) {
vii.add(b.get(i));
}
listV.remove(0); //移除当前,加入新的
listV.add(vii);
//执行交换,生成下一个粒子
exchange(mUnits.get(k).getPath(), vii);
}
//更新适应度的值,并挑选最好的个体
for (int i = 0; i < scale; i++) {
mUnits.get(i).upDateFitness();
if (Pd.get(i).getFitness() > mUnits.get(i).getFitness()) {
Pd.put(i, mUnits.get(i));
}
if (Pgd.getFitness() > Pd.get(i).getFitness()) {
Pgd = Pd.get(i);
bestT = t;
}
}
//打印当前代的结果
if (t % 100 == 0) {
// 打印
System.out.println("--------第"+t+"代的最佳结果为-----------");
System.out.println(Pgd.getFitness());
System.out.println("最佳路径:");
Pgd.printPath();
}
}
}
/**
* 执行交换,更新粒子
*
* @param path
* @param vii 存储的是需要交换的下标对
*/
private void exchange(int[] path, ArrayList vii) {
int tmp;
for (SO so : vii) {
tmp = path[so.getX()];
path[so.getX()] = path[so.getY()];
path[so.getY()] = tmp;
}
}
/**
* 生成交换对,把a变成和b一样,返回需要交换的下标对列表
*
* @param a
* @param b
* @return
*/
private ArrayList minus(int[] a, int[] b) {
int[] tmp = a.clone();
ArrayList list = new ArrayList<>();
int index = 0;
for (int i = 0; i < b.length; i++) {
if (tmp[i] != b[i]) {
//在tmp中找到和b[i]相等的值,将下标存储起来
for (int j = i + 1; j < tmp.length; j++) {
if (tmp[j] == b[i]) {
index = j;
break;
}
}
SO so = new SO(i, index);
list.add(so);
}
}
return list;
}
public static void main(String[] args) {
PSO pso = new PSO(30, 5000, 0.5f);
pso.init();
pso.solve();
}
}
注释写得很详细,这里不再赘述,各位可以直接运行代码,通过debug的方式,这样我觉得很容易理解的。看源码是学习编程最快的方式
最后感谢大家的观看,如果有问题请评论区留言,本人将尽力解答。
附上源码下载链接:
https://download.csdn.net/download/u012324136/10513646
声明
在参考下面链接的基础上,对代码进行重构,使用java面向对象的思想,是代码可读性大大增强
参考链接:
https://blog.csdn.net/zuochao_2013/article/details/53431767?ref=myread
https://blog.csdn.net/wangqiuyun/article/details/12515203
https://blog.csdn.net/guognib/article/details/30034821