1. 程式人生 > >基於貪心演算法求解TSP問題(JAVA)

基於貪心演算法求解TSP問題(JAVA)

前段時間在搞貪心演算法,為了舉例,故拿TSP來開刀,寫了段求解演算法程式碼以便有需之人,注意程式碼考慮可讀性從最容易理解角度寫,沒有優化,有需要可以自行優化!

一、TSP問題

TSP問題(Travelling Salesman Problem)即旅行商問題,又譯為旅行推銷員問題、貨郎擔問題,是數學領域中著名問題之一。假設有一個旅行商人要拜訪n個城市,他必須選擇所要走的路徑,路徑的限制是每個城市只能拜訪一次,而且最後要回到原來出發的城市。路徑的選擇目標是要求得的路徑路程為所有路徑之中的最小值。

TSP問題是一個組合優化問題。該問題可以被證明具有NPC計算複雜性。TSP問題可以分為兩類,一類是對稱TSP問題(Symmetric TSP),另一類是非對稱問題(Asymmetric TSP)。所有的TSP問題都可以用一個圖(Graph)來描述:

V={c1, c2, …, ci, …, cn},i = 1,2, …, n,是所有城市的集合.ci表示第i個城市,n為城市的數目;

E={(r, s): r,s∈ V}是所有城市之間連線的集合;

C = {crs: r,s∈ V}是所有城市之間連線的成本度量(一般為城市之間的距離);

如果crs = csr, 那麼該TSP問題為對稱的,否則為非對稱的。

一個TSP問題可以表達為:

求解遍歷圖G = (V, E, C),所有的節點一次並且回到起始節點,使得連線這些節點的路徑成本最低。

二、貪心演算法

貪心演算法,又名貪婪演算法(學校里老教授都喜歡叫貪婪演算法),是一種常用的求解最優化問題的簡單、迅速的演算法。貪心演算法總是做出在當前看來最好的選擇,它所做的每一個在當前狀態下某種意義上是最好的選擇即貪心選擇,並希望通過每次所作的貪心選擇導致最終得到問題最優解。必須注意的是,貪心演算法不是對所有問題都能得到整體最優解,選擇的貪心策略必須具備無後效性,即某個狀態以後的過程不會影響以前的狀態,只與當前狀態有關。

1、貪心演算法的基本思路

從問題的某一個初始解觸發逐步逼近給定的目標,以儘可能快地求得更好的解。當達到某演算法中的某一步不能再繼續前進時,演算法停止。大致步驟如下:

1)建立數學模型來描述問題;
2)把求解的問題分成若干個子問題
3)對每一個子問題求解,得到子問題的區域性最優解
4)把子問題的區域性最優解合成原問題的一個解

2、貪心演算法的實現框架

貪心演算法沒有固定的演算法框架,演算法設計的關鍵是貪心策略的選擇,而貪心策略適用的前提是:區域性最優策略能導致產生全域性最優解。

    從問題的某一初始解出發;
    while (能朝給定總目標前進一步)
    {
          利用可行的決策,求出可行解的一個解元素;
    }
    由所有解元素組合成問題的一個可行解;

3、貪心演算法存在的問題

1)不能保證求得的最後解是最佳的;
2)不能用來求最大最小解問題;
3)只能在某些特定條件約束的情況下使用,例如貪心策略必須具備無後效性等。

4、典型的貪心演算法使用領域

馬踏棋盤、揹包、裝箱等

三、貪心演算法求解TSP問題

貪心策略:在當前節點下遍歷所有能到達的下一節點,選擇距離最近的節點作為下一節點。基本思路是,從一節點出發遍歷所有能到達的下一節點,選擇距離最近的節點作為下一節點,然後把當前節點標記已走過,下一節點作為當前節點,重複貪心策略,以此類推直至所有節點都標記為已走節點結束。

我們使用TSP問題依然來自於tsplib上的att48,這是一個對稱TSP問題,城市規模為48,其最優值為10628.其距離計算方法下圖所示:

好,下面是具體程式碼:

package noah;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class TxTsp {

	private int cityNum; // 城市數量
	private int[][] distance; // 距離矩陣

	private int[] colable;//代表列,也表示是否走過,走過置0
	private int[] row;//代表行,選過置0

	public TxTsp(int n) {
		cityNum = n;
	}

	private void init(String filename) throws IOException {
		// 讀取資料
		int[] x;
		int[] y;
		String strbuff;
		BufferedReader data = new BufferedReader(new InputStreamReader(
				new FileInputStream(filename)));
		distance = new int[cityNum][cityNum];
		x = new int[cityNum];
		y = new int[cityNum];
		for (int i = 0; i < cityNum; i++) {
			// 讀取一行資料,資料格式1 6734 1453
			strbuff = data.readLine();
			// 字元分割
			String[] strcol = strbuff.split(" ");
			x[i] = Integer.valueOf(strcol[1]);// x座標
			y[i] = Integer.valueOf(strcol[2]);// y座標
		}
		data.close();

		// 計算距離矩陣
		// ,針對具體問題,距離計算方法也不一樣,此處用的是att48作為案例,它有48個城市,距離計算方法為偽歐氏距離,最優值為10628
		for (int i = 0; i < cityNum - 1; i++) {
			distance[i][i] = 0; // 對角線為0
			for (int j = i + 1; j < cityNum; j++) {
				double rij = Math
						.sqrt(((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j])
								* (y[i] - y[j])) / 10.0);
				// 四捨五入,取整
				int tij = (int) Math.round(rij);
				if (tij < rij) {
					distance[i][j] = tij + 1;
					distance[j][i] = distance[i][j];
				} else {
					distance[i][j] = tij;
					distance[j][i] = distance[i][j];
				}
			}
		}

		distance[cityNum - 1][cityNum - 1] = 0;

		colable = new int[cityNum];
		colable[0] = 0;
		for (int i = 1; i < cityNum; i++) {
			colable[i] = 1;
		}

		row = new int[cityNum];
		for (int i = 0; i < cityNum; i++) {
			row[i] = 1;
		}

	}
	
	public void solve(){
		
		int[] temp = new int[cityNum];
		String path="0";
		
		int s=0;//計算距離
		int i=0;//當前節點
		int j=0;//下一個節點
		//預設從0開始
		while(row[i]==1){
			//複製一行
			for (int k = 0; k < cityNum; k++) {
				temp[k] = distance[i][k];
				//System.out.print(temp[k]+" ");
			}
			//System.out.println();
			//選擇下一個節點,要求不是已經走過,並且與i不同
			j = selectmin(temp);
			//找出下一節點
			row[i] = 0;//行置0,表示已經選過
			colable[j] = 0;//列0,表示已經走過
			
			path+="-->" + j;
			//System.out.println(i + "-->" + j);
			//System.out.println(distance[i][j]);
			s = s + distance[i][j];
			i = j;//當前節點指向下一節點
		}
		System.out.println("路徑:" + path);
		System.out.println("總距離為:" + s);
		
	}
	
	public int selectmin(int[] p){
		int j = 0, m = p[0], k = 0;
		//尋找第一個可用節點,注意最後一次尋找,沒有可用節點
		while (colable[j] == 0) {
			j++;
			//System.out.print(j+" ");
			if(j>=cityNum){
				//沒有可用節點,說明已結束,最後一次為 *-->0
				m = p[0];
				break;
				//或者直接return 0;
			}
			else{
				m = p[j];
			}
		}
		//從可用節點J開始往後掃描,找出距離最小節點
		for (; j < cityNum; j++) {
			if (colable[j] == 1) {
				if (m >= p[j]) {
					m = p[j];
					k = j;
				}
			}
		}
		return k;
	}


	public void printinit() {
		System.out.println("print begin....");
		for (int i = 0; i < cityNum; i++) {
			for (int j = 0; j < cityNum; j++) {
				System.out.print(distance[i][j] + " ");
			}
			System.out.println();
		}
		System.out.println("print end....");
	}

	public static void main(String[] args) throws IOException {
		System.out.println("Start....");
		TxTsp ts = new TxTsp(48);
		ts.init("c://data.txt");
		//ts.printinit();
		ts.solve();
	}
}

求解結果截圖:

四、總結

單從求解結果來看,我個人其實還是能接受這個解,但仔細想想,實際上這個求解結果有太多運氣成分在裡面,貪心演算法畢竟是貪心演算法,只能緩一時,而不是長久之計,問題的模型、引數對貪心演算法求解結果具有決定性作用,這在某種程度上是不能接受的,於是聰明的人類就發明了各種智慧演算法(也叫啟發式演算法),但在我看來所謂的智慧演算法本質上就是貪心演算法和隨機化演算法結合,例如傳統遺傳演算法用的選擇策略就是典型的貪心選擇,正是這些貪心演算法和隨機演算法的結合,我們才看到今天各種各樣的智慧演算法。