ACO蟻群演算法解決TSP旅行商問題

Android路上的人發表於2015-04-30

前言

蟻群演算法也是一種利用了大自然規律的啟發式演算法,與之前學習過的GA遺傳演算法類似,遺傳演算法是用了生物進行理論,把更具適應性的基因傳給下一代,最後就能得到一個最優解,常常用來尋找問題的最優解。當然,本篇文章不會主講GA演算法的,想要了解的同學可以檢視,我的遺傳演算法學習遺傳演算法在走迷宮中的應。話題重新回到蟻群演算法,蟻群演算法是一個利用了螞蟻尋找食物的原理。不知道小時候有沒有發現,當一個螞蟻發現了地上的食物,然後非常迅速的,就有其他的螞蟻聚攏過來,最後把食物抬回家,這裡面其實有著非常多的道理的,在ACO中就用到了這個機理用於解決實際生活中的一些問題。

螞蟻找食物

首先我們要具體說說一個有意思的事情,就是螞蟻找食物的問題,理解了這個原理之後,對於理解ACO演算法就非常容易了。螞蟻作為那麼小的動物,在地上漫無目的的尋找食物,起初都是沒有目標的,他從螞蟻洞中走出,隨機的爬向各個方向,在這期間他會向外界播撒一種化學物質,姑且就叫做資訊素,所以這裡就可以得到的一個前提,越多螞蟻走過的路徑,資訊素濃度就會越高,那麼某條路徑資訊素濃度高了,自然就會有越多的螞蟻感覺到了,就會聚集過來了。所以當眾多螞蟻中的一個找到食物之後,他就會在走過的路徑中放出資訊素濃度,因此就會有很多的螞蟻趕來了。類似下面的場景:


至於螞蟻是如何感知這個資訊素,這個就得問生物學家了,我也沒做過研究。

演算法介紹

OK,有了上面這個自然生活中的生物場景之後,我們再來切入文章主題來學習一下蟻群演算法,百度百科中對應蟻群演算法是這麼介紹的:蟻群演算法是一種在圖中尋找優化路徑的機率型演算法。他的靈感就是來自於螞蟻發現食物的行為。蟻群演算法是一種新的模擬進化優化的演算法,與遺傳演算法有很多相似的地方。蟻群演算法在比較早的時候成功解決了TSP旅行商的問題(在後面的例子中也會以這個例子)。要用演算法去模擬螞蟻的這種行為,關鍵在於資訊素的在演算法中的設計,以及路徑中資訊素濃度越大的路徑,將會有更高的概率被螞蟻所選擇到。

演算法原理

要想實現上面的幾個模擬行為,需要藉助幾個公式,當然公式不是我自己定義的,主要有3個,如下圖:


上圖中所出現的alpha,beita,p等數字都是控制因子,所以可不必理會,Tij(n)的意思是在時間為n的時候,從城市i到城市j的路徑的資訊素濃度。類似於nij的字母是城市i到城市j距離的倒數。就是下面這個公式。


所以所有的公式都是為第一個公式服務的,第一個公式的意思是指第k只螞蟻選擇從城市i到城市j的概率,可以見得,這個受距離和資訊素濃度的雙重影響,距離越遠,去此城市的概率自然也低,所以nij會等於距離的倒數,而且在算資訊素濃度的時候,也考慮到了資訊素濃度衰減的問題,所以會在上次的濃度值上乘以一個衰減因子P。另外還要加上本輪搜尋增加的資訊素濃度(假如有螞蟻經過此路徑的話),所以這幾個公式的整體設計思想還是非常棒的。

演算法的程式碼實現

由於本身我這裡沒有什麼真實的測試資料,就隨便自己構造了一個簡單的資料,輸入如下,分為城市名稱和城市之間的距離,用#符號做區分標識,大家應該可以看得懂吧

# CityName
1
2
3
4
# Distance
1 2 1
1 3 1.4
1 4 1
2 3 1
2 4 1
3 4 1

螞蟻類Ant.java:

package DataMining_ACO;

import java.util.ArrayList;

/**
 * 螞蟻類,進行路徑搜尋的載體
 * 
 * @author lyq
 * 
 */
public class Ant implements Comparable<Ant> {
	// 螞蟻當前所在城市
	String currentPos;
	// 螞蟻遍歷完回到原點所用的總距離
	Double sumDistance;
	// 城市間的資訊素濃度矩陣,隨著時間的增多而減少
	double[][] pheromoneMatrix;
	// 螞蟻已經走過的城市集合
	ArrayList<String> visitedCitys;
	// 還未走過的城市集合
	ArrayList<String> nonVisitedCitys;
	// 螞蟻當前走過的路徑
	ArrayList<String> currentPath;

	public Ant(double[][] pheromoneMatrix, ArrayList<String> nonVisitedCitys) {
		this.pheromoneMatrix = pheromoneMatrix;
		this.nonVisitedCitys = nonVisitedCitys;

		this.visitedCitys = new ArrayList<>();
		this.currentPath = new ArrayList<>();
	}

	/**
	 * 計算路徑的總成本(距離)
	 * 
	 * @return
	 */
	public double calSumDistance() {
		sumDistance = 0.0;
		String lastCity;
		String currentCity;

		for (int i = 0; i < currentPath.size() - 1; i++) {
			lastCity = currentPath.get(i);
			currentCity = currentPath.get(i + 1);

			// 通過距離矩陣進行計算
			sumDistance += ACOTool.disMatrix[Integer.parseInt(lastCity)][Integer
					.parseInt(currentCity)];
		}

		return sumDistance;
	}

	/**
	 * 螞蟻選擇前往下一個城市
	 * 
	 * @param city
	 *            所選的城市
	 */
	public void goToNextCity(String city) {
		this.currentPath.add(city);
		this.currentPos = city;
		this.nonVisitedCitys.remove(city);
		this.visitedCitys.add(city);
	}

	/**
	 * 判斷螞蟻是否已經又重新回到起點
	 * 
	 * @return
	 */
	public boolean isBack() {
		boolean isBack = false;
		String startPos;
		String endPos;

		if (currentPath.size() == 0) {
			return isBack;
		}

		startPos = currentPath.get(0);
		endPos = currentPath.get(currentPath.size() - 1);
		if (currentPath.size() > 1 && startPos.equals(endPos)) {
			isBack = true;
		}

		return isBack;
	}

	/**
	 * 判斷螞蟻在本次的走過的路徑中是否包含從城市i到城市j
	 * 
	 * @param cityI
	 *            城市I
	 * @param cityJ
	 *            城市J
	 * @return
	 */
	public boolean pathContained(String cityI, String cityJ) {
		String lastCity;
		String currentCity;
		boolean isContained = false;

		for (int i = 0; i < currentPath.size() - 1; i++) {
			lastCity = currentPath.get(i);
			currentCity = currentPath.get(i + 1);

			// 如果某一段路徑的始末位置一致,則認為有經過此城市
			if ((lastCity.equals(cityI) && currentCity.equals(cityJ))
					|| (lastCity.equals(cityJ) && currentCity.equals(cityI))) {
				isContained = true;
				break;
			}
		}

		return isContained;
	}

	@Override
	public int compareTo(Ant o) {
		// TODO Auto-generated method stub
		return this.sumDistance.compareTo(o.sumDistance);
	}
}

蟻群演算法工具類ACOTool.java:
package DataMining_ACO;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * 蟻群演算法工具類
 * 
 * @author lyq
 * 
 */
public class ACOTool {
	// 輸入資料型別
	public static final int INPUT_CITY_NAME = 1;
	public static final int INPUT_CITY_DIS = 2;

	// 城市間距離鄰接矩陣
	public static double[][] disMatrix;
	// 當前時間
	public static int currentTime;

	// 測試資料地址
	private String filePath;
	// 螞蟻數量
	private int antNum;
	// 控制引數
	private double alpha;
	private double beita;
	private double p;
	private double Q;
	// 隨機數產生器
	private Random random;
	// 城市名稱集合,這裡為了方便,將城市用數字表示
	private ArrayList<String> totalCitys;
	// 所有的螞蟻集合
	private ArrayList<Ant> totalAnts;
	// 城市間的資訊素濃度矩陣,隨著時間的增多而減少
	private double[][] pheromoneMatrix;
	// 目標的最短路徑,順序為從集合的前部往後挪動
	private ArrayList<String> bestPath;
	// 資訊素矩陣儲存圖,key採用的格式(i,j,t)->value
	private Map<String, Double> pheromoneTimeMap;

	public ACOTool(String filePath, int antNum, double alpha, double beita,
			double p, double Q) {
		this.filePath = filePath;
		this.antNum = antNum;
		this.alpha = alpha;
		this.beita = beita;
		this.p = p;
		this.Q = Q;
		this.currentTime = 0;

		readDataFile();
	}

	/**
	 * 從檔案中讀取資料
	 */
	private void readDataFile() {
		File file = new File(filePath);
		ArrayList<String[]> dataArray = new ArrayList<String[]>();

		try {
			BufferedReader in = new BufferedReader(new FileReader(file));
			String str;
			String[] tempArray;
			while ((str = in.readLine()) != null) {
				tempArray = str.split(" ");
				dataArray.add(tempArray);
			}
			in.close();
		} catch (IOException e) {
			e.getStackTrace();
		}

		int flag = -1;
		int src = 0;
		int des = 0;
		int size = 0;
		// 進行城市名稱種數的統計
		this.totalCitys = new ArrayList<>();
		for (String[] array : dataArray) {
			if (array[0].equals("#") && totalCitys.size() == 0) {
				flag = INPUT_CITY_NAME;

				continue;
			} else if (array[0].equals("#") && totalCitys.size() > 0) {
				size = totalCitys.size();
				// 初始化距離矩陣
				this.disMatrix = new double[size + 1][size + 1];
				this.pheromoneMatrix = new double[size + 1][size + 1];

				// 初始值-1代表此對應位置無值
				for (int i = 0; i < size; i++) {
					for (int j = 0; j < size; j++) {
						this.disMatrix[i][j] = -1;
						this.pheromoneMatrix[i][j] = -1;
					}
				}

				flag = INPUT_CITY_DIS;
				continue;
			}

			if (flag == INPUT_CITY_NAME) {
				this.totalCitys.add(array[0]);
			} else {
				src = Integer.parseInt(array[0]);
				des = Integer.parseInt(array[1]);

				this.disMatrix[src][des] = Double.parseDouble(array[2]);
				this.disMatrix[des][src] = Double.parseDouble(array[2]);
			}
		}
	}

	/**
	 * 計算從螞蟻城市i到j的概率
	 * 
	 * @param cityI
	 *            城市I
	 * @param cityJ
	 *            城市J
	 * @param currentTime
	 *            當前時間
	 * @return
	 */
	private double calIToJProbably(String cityI, String cityJ, int currentTime) {
		double pro = 0;
		double n = 0;
		double pheromone;
		int i;
		int j;

		i = Integer.parseInt(cityI);
		j = Integer.parseInt(cityJ);

		pheromone = getPheromone(currentTime, cityI, cityJ);
		n = 1.0 / disMatrix[i][j];

		if (pheromone == 0) {
			pheromone = 1;
		}

		pro = Math.pow(n, alpha) * Math.pow(pheromone, beita);

		return pro;
	}

	/**
	 * 計算綜合概率螞蟻從I城市走到J城市的概率
	 * 
	 * @return
	 */
	public String selectAntNextCity(Ant ant, int currentTime) {
		double randomNum;
		double tempPro;
		// 總概率指數
		double proTotal;
		String nextCity = null;
		ArrayList<String> allowedCitys;
		// 各城市概率集
		double[] proArray;

		// 如果是剛剛開始的時候,沒有路過任何城市,則隨機返回一個城市
		if (ant.currentPath.size() == 0) {
			nextCity = String.valueOf(random.nextInt(totalCitys.size()) + 1);

			return nextCity;
		} else if (ant.nonVisitedCitys.isEmpty()) {
			// 如果全部遍歷完畢,則再次回到起點
			nextCity = ant.currentPath.get(0);

			return nextCity;
		}

		proTotal = 0;
		allowedCitys = ant.nonVisitedCitys;
		proArray = new double[allowedCitys.size()];

		for (int i = 0; i < allowedCitys.size(); i++) {
			nextCity = allowedCitys.get(i);
			proArray[i] = calIToJProbably(ant.currentPos, nextCity, currentTime);
			proTotal += proArray[i];
		}

		for (int i = 0; i < allowedCitys.size(); i++) {
			// 歸一化處理
			proArray[i] /= proTotal;
		}

		// 用隨機數選擇下一個城市
		randomNum = random.nextInt(100) + 1;
		randomNum = randomNum / 100;
		// 因為1.0是無法判斷到的,,總和會無限接近1.0取為0.99做判斷
		if (randomNum == 1) {
			randomNum = randomNum - 0.01;
		}

		tempPro = 0;
		// 確定區間
		for (int j = 0; j < allowedCitys.size(); j++) {
			if (randomNum > tempPro && randomNum <= tempPro + proArray[j]) {
				// 採用拷貝的方式避免引用重複
				nextCity = allowedCitys.get(j);
				break;
			} else {
				tempPro += proArray[j];
			}
		}

		return nextCity;
	}

	/**
	 * 獲取給定時間點上從城市i到城市j的資訊素濃度
	 * 
	 * @param t
	 * @param cityI
	 * @param cityJ
	 * @return
	 */
	private double getPheromone(int t, String cityI, String cityJ) {
		double pheromone = 0;
		String key;

		// 上一週期需將時間倒回一週期
		key = MessageFormat.format("{0},{1},{2}", cityI, cityJ, t);

		if (pheromoneTimeMap.containsKey(key)) {
			pheromone = pheromoneTimeMap.get(key);
		}

		return pheromone;
	}

	/**
	 * 每輪結束,重新整理資訊素濃度矩陣
	 * 
	 * @param t
	 */
	private void refreshPheromone(int t) {
		double pheromone = 0;
		// 上一輪週期結束後的資訊素濃度,叢資訊素濃度圖中查詢
		double lastTimeP = 0;
		// 本輪資訊素濃度增加量
		double addPheromone;
		String key;

		for (String i : totalCitys) {
			for (String j : totalCitys) {
				if (!i.equals(j)) {
					// 上一週期需將時間倒回一週期
					key = MessageFormat.format("{0},{1},{2}", i, j, t - 1);

					if (pheromoneTimeMap.containsKey(key)) {
						lastTimeP = pheromoneTimeMap.get(key);
					} else {
						lastTimeP = 0;
					}

					addPheromone = 0;
					for (Ant ant : totalAnts) {
						if(ant.pathContained(i, j)){
							// 每隻螞蟻傳播的資訊素為控制因子除以距離總成本
							addPheromone += Q / ant.calSumDistance();
						}
					}

					// 將上次的結果值加上遞增的量,並存入圖中
					pheromone = p * lastTimeP + addPheromone;
					key = MessageFormat.format("{0},{1},{2}", i, j, t);
					pheromoneTimeMap.put(key, pheromone);
				}
			}
		}

	}

	/**
	 * 蟻群演算法迭代次數
	 * @param loopCount
	 * 具體遍歷次數
	 */
	public void antStartSearching(int loopCount) {
		// 蟻群尋找的總次數
		int count = 0;
		// 選中的下一個城市
		String selectedCity = "";

		pheromoneTimeMap = new HashMap<String, Double>();
		totalAnts = new ArrayList<>();
		random = new Random();

		while (count < loopCount) {
			initAnts();

			while (true) {
				for (Ant ant : totalAnts) {
					selectedCity = selectAntNextCity(ant, currentTime);
					ant.goToNextCity(selectedCity);
				}

				// 如果已經遍歷完所有城市,則跳出此輪迴圈
				if (totalAnts.get(0).isBack()) {
					break;
				}
			}

			// 週期時間疊加
			currentTime++;
			refreshPheromone(currentTime);
			count++;
		}

		// 根據距離成本,選出所花距離最短的一個路徑
		Collections.sort(totalAnts);
		bestPath = totalAnts.get(0).currentPath;
		System.out.println(MessageFormat.format("經過{0}次迴圈遍歷,最終得出的最佳路徑:", count));
		System.out.print("entrance");
		for (String cityName : bestPath) {
			System.out.print(MessageFormat.format("-->{0}", cityName));
		}
	}

	/**
	 * 初始化蟻群操作
	 */
	private void initAnts() {
		Ant tempAnt;
		ArrayList<String> nonVisitedCitys;
		totalAnts.clear();

		// 初始化蟻群
		for (int i = 0; i < antNum; i++) {
			nonVisitedCitys = (ArrayList<String>) totalCitys.clone();
			tempAnt = new Ant(pheromoneMatrix, nonVisitedCitys);

			totalAnts.add(tempAnt);
		}
	}
}

場景測試類Client.java:

package DataMining_ACO;

/**
 * 蟻群演算法測試類
 * @author lyq
 *
 */
public class Client {
	public static void main(String[] args){
		//測試資料
		String filePath = "C:\\Users\\lyq\\Desktop\\icon\\input.txt";
		//螞蟻數量
		int antNum;
		//蟻群演算法迭代次數
		int loopCount;
		//控制引數
		double alpha;
		double beita;
		double p;
		double Q;
		
		antNum = 3;
		alpha = 0.5;
		beita = 1;
		p = 0.5;
		Q = 5;
		loopCount = 5;
		
		ACOTool tool = new ACOTool(filePath, antNum, alpha, beita, p, Q);
		tool.antStartSearching(loopCount);
	}
}

演算法的輸出,就是在多次搜尋之後,找到的路徑中最短的一個路徑:

經過5次迴圈遍歷,最終得出的最佳路徑:
entrance-->4-->1-->2-->3-->4

因為資料量比較小,並不能看出蟻群演算法在這方面的優勢,博友們可以再次基礎上自行改造,並用大一點的資料做測試,其中的4個控制因子也可以調控。蟻群演算法作為一種啟發式演算法,還可以和遺傳演算法結合,創造出更優的演算法。蟻群演算法可以解決許多這樣的連通圖路徑優化問題。但是有的時候也會出現搜尋時間過長的問題。


參考文獻:百度百科.蟻群演算法

我的資料探勘演算法庫:https://github.com/linyiqun/DataMiningAlgorithm

我的演算法庫:https://github.com/linyiqun/lyq-algorithms-lib


相關文章