爬山算法(Hill Climbing)解決旅行商問題(TSP)

何為TSP問題?

旅行商問題 TSP(Travelling Salesman Problem)是數(shù)學(xué)領(lǐng)域中著名問題之一后众。

  • 場景:一個旅行商人需要拜訪n個城市
  • 條件:要選擇一個路徑能夠拜訪到所有的城市放闺,且每個城市只能被拜訪一次祟昭,最終回到起點
  • 目標(biāo):求得的路徑路程為所有路徑可能之中的最小值

TSP問題被證明是NP完全問題,這類問題不能用精確算法實現(xiàn)怖侦,而需要使用相似算法篡悟。

TSP問題分為兩類:對稱TSP(Symmetric TSP)以及非對稱TSP(Asymmetric TSP)

對稱與非對稱TSP區(qū)別

本文解決的是對稱TSP
假設(shè):A表示城市A,B表示城市B础钠,D(A->B)為城市A到城市B的距離恰力,同理D(B->A)為城市B到城市A的距離
對稱TSP中,D(A->B) = D(B->A)旗吁,城市間形成無向圖
非對稱TSP中踩萎,D(A->B) ≠ D(B->A),城市間形成有向圖

什么情況下會出現(xiàn)非對稱TSP呢很钓?

現(xiàn)實生活中香府,可能出現(xiàn)單行線董栽、交通事故、機票往返價格不同等情況企孩,均可以打破對稱性锭碳。

何為爬山算法(Hill Climbing)?

爬山算法是一種局部擇優(yōu)的方法勿璃,采用啟發(fā)式方法擒抛。直觀的解釋如下圖:

一系列山峰與峽谷的剖面圖

爬山算法,顧名思義就是爬山补疑,找到第一個山峰的時候就停止歧沪,作為算法的輸出結(jié)果。所以莲组,爬山算法容易把局部最優(yōu)解A作為算法的輸出诊胞,而我們的目的是找到全局最優(yōu)解B。

如何讓算法有更大的可能性搜索到全局最優(yōu)解B呢锹杈?

如下圖所示撵孤,盡管在這個圖中的許多局部極大值,仍然可以使用模擬退火算法(Simulated Annealing)發(fā)現(xiàn)全局最大值竭望。

圖片來源:wikipedia
圖片來源:wikipedia

對于模擬退火的內(nèi)容在此不予贅述邪码,有機會另寫一文詳述,如感興趣請訪問大白話解析模擬退火算法

Java實現(xiàn)爬山算法解決旅行商問題

必要解釋詳見注釋

/**
 * 城市實體類
 * 實現(xiàn)計算城市間距離方法
 * @author Frank CHEN
 *
 */
public class City {
    // 地球赤道半徑
    private static final double ERATH_EQUATORIAL_RADIUS = 6378.1370D;
    
    private static final double CONCVERT_DEGREES_TO_RADIANS = Math.PI / 180;
    // 經(jīng)度
    private double longitude;
    // 緯度
    private double latitude;
    // 城市名
    private String name;
    
    public City(double longitude, double latitude, String name) {
        super();
        this.longitude = longitude;
        this.latitude = latitude;
        this.name = name;
    }
    
    public double getLongitude() {
        return longitude;
    }
    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }
    public double getLatitude() {
        return latitude;
    }
    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return this.name;
    }
    
    /**
     * 計算傳入城市與當(dāng)前城市的實際距離
     * @param city
     * @return
     */
    public double measureDistance(City city) {
        double deltaLongitude = deg2rad(city.getLongitude() - this.getLongitude());
        double deltaLatitude = deg2rad(city.getLatitude() - this.getLatitude());
        double a = Math.pow(Math.sin(deltaLatitude / 2D), 2D) 
                + Math.cos(deg2rad(this.getLatitude())) 
                * Math.cos(deg2rad(city.getLatitude())) 
                * Math.pow(Math.sin(deltaLongitude / 2D), 2D);
        return  ERATH_EQUATORIAL_RADIUS * 2D * Math.atan2(Math.sqrt(a), Math.sqrt(1D - a));
    }
    
    // 轉(zhuǎn)化為弧度
    private double deg2rad(double deg) {
          return deg * CONCVERT_DEGREES_TO_RADIANS;
    }
}

此處根據(jù)經(jīng)緯度計算城市間距離的公式市框,請參考Calculate distance between two latitude-longitude points? (Haversine formula)

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;

/**
 * 路徑實體類
 * @author Frank CHEN
 *
 */
public class Route {
    private ArrayList<City> cities = new ArrayList<>();
    
    /**
     * 構(gòu)造方法
     * 隨機打亂cities排序
     * @param cities
     */
    public Route(ArrayList<City> cities) {
        this.cities.addAll(cities);
        Collections.shuffle(this.cities);
    }
    
    public Route(Route route) {
        for(City city : route.getCities()) {
            this.cities.add(city);
        }
    }
    
    /**
     * 計算城市間總距離
     * @return
     */
    public double getTotalDistance() {
        int citiesSize = this.cities.size();
        double sum = 0D;
        int index = 0;
        while(index < citiesSize - 1) {
            sum += this.cities.get(index).measureDistance(this.cities.get(index + 1));
            index++;
        }
        sum += this.cities.get(citiesSize - 1).measureDistance(this.cities.get(0));
        return sum;
    }
    
    public String getTotalStringDistance() {
        String returnString = String.format("%.2f", this.getTotalDistance());
        return returnString;
    }
    
    public ArrayList<City> getCities() {
        return cities;
    }
    
    public void setCities(ArrayList<City> cities) {
        this.cities = cities;
    }

    @Override
    public String toString() {
        return Arrays.toString(cities.toArray());
    }
    
} 


/**
 * 實現(xiàn)爬山算法
 * @author Frank CHEN
 *
 */
public class HillClimbing {
    // 最大迭代次數(shù)
    public static final int ITERATIONS_BEFORE_MAXIMUM = 1500;
    
    /**
     * 尋找最短路徑
     * @param currentRoute
     * @return
     */
    public Route findShortestRoute(Route currentRoute) {
        Route adjacentRoute;
        int iterToMaximumCounter = 0;
        String compareRoutes = null;
        while(iterToMaximumCounter < ITERATIONS_BEFORE_MAXIMUM) {
            adjacentRoute = obtainAdjacentRoute(new Route(currentRoute));
            if(adjacentRoute.getTotalDistance() <= currentRoute.getTotalDistance()) {
                compareRoutes = "<= (更新)";
                iterToMaximumCounter = 0;
                currentRoute = new Route(adjacentRoute);
            } else {
                compareRoutes = "> (保持) - 迭代次數(shù) # " + iterToMaximumCounter;
                iterToMaximumCounter++;
            }
            System.out.println("       | " + compareRoutes);
            System.out.print(currentRoute + " |      " + currentRoute.getTotalStringDistance());
        }
        
        System.out.println("       | 可能的最優(yōu)解");
        
        return currentRoute;
    }
    
    /**
     * 隨機交換兩個城市位置
     * @param route
     * @return
     */
    public Route obtainAdjacentRoute(Route route) {
        int x1 = 0, x2 = 0;
        while(x1 == x2) {
            x1 = (int) (route.getCities().size() * Math.random());
            x2 = (int) (route.getCities().size() * Math.random());
        }
        
        City city1 = route.getCities().get(x1);
        City city2 = route.getCities().get(x2);
        
        // swap two stochastic cities
        route.getCities().set(x2, city1);
        route.getCities().set(x1, city2);
        return route;
    }
}

import java.util.ArrayList;
import java.util.Arrays;

/**
 * 初始化城市數(shù)據(jù)
 * main方法
 * @author Frank CHEN
 *
 */
public class Driver {
    private ArrayList<City> initialCities = new ArrayList<City>(Arrays.asList(
            new City(116.41667, 39.91667, "北京"),
            new City(121.43333, 34.50000, "上海"),
            new City(113.00000, 28.21667, "長沙"),
            new City(106.26667, 38.46667, "銀川"),
            new City(109.50000, 18.20000, "三亞"),
            new City(112.53333, 37.86667, "太原"),
            new City(91.00000, 29.60000, "拉薩"),
            new City(102.73333,  25.05000, "昆明"),
            new City(126.63333, 45.75000, "哈爾濱"),
            new City(113.65000, 34.76667, "鄭州"),
            new City(113.50000, 22.20000, "澳門")));
    
    public static void main(String[] args) {
        Driver driver = new Driver();
        Route route = new Route(driver.initialCities);
        new HillClimbing().findShortestRoute(route);
    }
}

此處初始化數(shù)據(jù)源可以使用TSPLIB中所提供的數(shù)據(jù)霞扬,此程序大致闡述爬山算法的實現(xiàn)。

運行result

編寫于一個失眠夜


菜鳥一枚枫振,歡迎評論區(qū)相互交流喻圃,加速你我成長???。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末粪滤,一起剝皮案震驚了整個濱河市斧拍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杖小,老刑警劉巖肆汹,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異予权,居然都是意外死亡昂勉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門扫腺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來岗照,“玉大人,你說我怎么就攤上這事≡苤粒” “怎么了厚者?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長迫吐。 經(jīng)常有香客問我库菲,道長,這世上最難降的妖魔是什么志膀? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任熙宇,我火速辦了婚禮,結(jié)果婚禮上溉浙,老公的妹妹穿的比我還像新娘奇颠。我一直安慰自己,他們只是感情好放航,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著圆裕,像睡著了一般广鳍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吓妆,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天赊时,我揣著相機與錄音,去河邊找鬼行拢。 笑死祖秒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的舟奠。 我是一名探鬼主播竭缝,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼沼瘫!你這毒婦竟也來了抬纸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤耿戚,失蹤者是張志新(化名)和其女友劉穎湿故,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體膜蛔,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡坛猪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了皂股。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墅茉。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出躁锁,到底是詐尸還是另有隱情纷铣,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布战转,位于F島的核電站搜立,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏槐秧。R本人自食惡果不足惜啄踊,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望刁标。 院中可真熱鬧颠通,春花似錦、人聲如沸膀懈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽启搂。三九已至硼控,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間胳赌,已是汗流浹背牢撼。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留疑苫,地道東北人熏版。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像捍掺,于是被迫代替她去往敵國和親撼短。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 高級鉗工應(yīng)知鑒定題庫(858題) ***單選題*** 1. 000003難易程度:較難知識范圍:相關(guān)4 01答案:...
    開源時代閱讀 5,700評論 1 9
  • 王濤的這本書其實好多年前都看了很多遍乡小,去年這個時候?qū)W了專業(yè)營養(yǎng)學(xué)阔加,收獲滿滿,知道很多慢性病的原理满钟,家里人也改掉...
    步步嬌閱讀 167評論 0 0
  • 我負責(zé)醫(yī)院護理實習(xí)工作時湃番,每次碰到護士長交上來那些違規(guī)醫(yī)院規(guī)章制度的實習(xí)生(主要是曠工和遲到)的情況夭织,是我最頭疼的...
    森林樹閱讀 753評論 6 2
  • CSS簡介: CSS(Cascading Style Sheets):層疊樣式表 樣式定義如何顯示 HTML 元素...
    別讓我一個人醉_1fa7閱讀 254評論 0 0
  • 許的愿望逐漸實現(xiàn),但是實現(xiàn)以后換來的卻是歸零的存款吠撮。 這樣是不是一件好事尊惰? 新年要怎么辦?
    賤賤小姐閱讀 177評論 0 0