1. 程式人生 > >簡單易懂,蟻群演算法解決旅行商問題

簡單易懂,蟻群演算法解決旅行商問題

轉載宣告:

原文把蟻群解決旅行商問題寫的很清楚,只不過本人認為原文中有一些小錯誤,特此更改(文中紅色加粗字型為改正處),程式碼中出現的一些演算法的小問題也進行了更正(比如程式碼中的貪心演算法),程式碼也附在下面,謝謝博主的分享。

1.關於旅行商(TSP)問題及衍化

  旅行商問題(Traveling Saleman Problem,TSP)是車輛路徑排程問題(VRP)的特例,由於數學家已證明TSP問題是NP難題,因此,VRP也屬於NP難題。旅行商問題(TSP)又譯為旅行推銷員問題、貨郎擔問題,簡稱為TSP問題,是最基本的路線問題,該問題是在尋求單一旅行者由起點出發,通過所有給定的需求點之後,最後再回到原點的最小路徑成本。——

旅行商問題百科

  很明顯,當節點數很少時,大多數人都會想到,問題很簡單,直接窮舉就OK了,但實際問題中,節點數往往很大,變得不可能。例如:對於一個僅有16個城市的旅行商問題,如果用窮舉法來求問題的最優解,需比較的可行解有:15!/2=653,837,184,000個。在1993年,使用當時的工作站用窮舉法求解此問題需時92小時。即使現在計算機速度快,但是面對複雜的問題,仍然不夠。這就是所謂的“組合爆炸”,指數級的增長,所以科學家逐步尋找近似演算法或者啟發式演算法,目的是在合理的時間範圍內找到可接受的最優解。

  TSP問題解決演算法的發展可以分為3個部分:

1).經典精確演算法:窮舉法、線性規劃演算法、動態規劃演算法、分支定界演算法等運籌學中的傳統演算法,這些演算法複雜度一般都很大,只適用於求解小規模問題。

2).近似演算法:當問題規模較大時,其所需的時間成級數增長,這是我們無法接受的,演算法求解問題的規模受到了很大的限制,一個很自然的想法就是犧牲精確解法中的最優性,去尋找一個好的時間複雜度我們可以容忍的,同時解的質量我們可以接受的演算法.基於這一思想所設計出的演算法統稱為近似演算法。如插入演算法,最鄰近演算法等。

3).智慧演算法:隨著科學技術和生產的不斷髮展,許多實際問題不可能在合理的時間範圍內找到全域性最優解,這就促使了近代最優化問題求解方法的產生。隨著各種不同搜尋機制的啟發式演算法相繼出現,如禁忌搜尋、遺傳演算法、模擬退火演算法、人工神經網路、進化策略、進化程式設計、粒子群優化演算法、蟻群優化演算法和免疫計算等,掀起了研究啟發式演算法的高潮。

  具體每一種演算法不再詳細描述,大家可以針對性的尋找相應資料進行了解。

  TSP問題在實際的生產生活中,更加實際環境不同,有很多衍生的經典問題。車輛路徑排程(VRP)擴充套件問題是經典VRP加入各種約束條件後而形成的。例如需求約束形成的需求隨機的車輛路徑問題(SVRP);加入時間約束得到的帶時間窗的車輛路徑題(VRPTW);加入距離約束的距離約束車輛路徑問題(DVRP);根據其它條件的不同,還有多配送中心車輛路徑問題(MDVRP)、可切分的車輛路徑問題(SDVRP);先配送再收集車輛路徑問題(VRPB)、配送收集車輛路徑問題(VRPPD);資訊不完全的模糊車輛路徑問題(FVRP)[3]。

2.群蟻演算法基本原理

2.1 演算法綜述

  對於VRP問題,求解演算法大致可分為精確演算法和人工智慧演算法兩大類。精確性演算法基於嚴格的數學手段,在可以求解的情況下,解的質量較好。但是由於演算法嚴格,運算量大,特別是大規模的問題幾乎無法求解。所以其應用只能是小規模的確定性問題,面對中小規模問題,人工智慧演算法在精度上不佔優勢。但規模變大時,人工智慧方法基本能在可接受時間裡,找到可接受的滿意解,這是精確演算法難以做到的。由於的實際問題,各種約束錯綜複雜,人工智慧演算法顯示出了巨大的優越性,也正因為如此,實際應用中,人工智慧演算法要更廣泛。求解車輛路徑排程問題的精確演算法有動態規劃法、分枝定界法等。並開始尋求所得結果可接受的啟發式演算法,以處理大規模實際問題,一些其他學科的新一代優化演算法相繼出現,如禁忌搜尋演算法,遺傳演算法,人工神經網路演算法,以及現在研究較多的蟻群演算法等。

2.2 群蟻演算法的原理

  蟻群演算法是受到對真實螞蟻群覓食行為研究的啟發而提出。生物學研究表明:一群相互協作的螞蟻能夠找到食物和巢穴之間的最短路徑,而單隻螞蟻則不能。生物學家經過大量細緻觀察研究發現,螞蟻個體之間的行為是相互作用相互影響的。螞蟻在運動過程中,能夠在它所經過的路徑上留下一種稱之為資訊素的物質,而此物質恰恰是螞蟻個體之間資訊傳遞交流的載體。螞蟻在運動時能夠感知這種物質,並且習慣於追蹤此物質爬行,當然爬行過程中還會釋放資訊素。一條路上的資訊素蹤跡越濃,其它螞蟻將以越高的概率跟隨爬行此路徑,從而該路徑上的資訊素蹤跡會被加強,因此,由大量螞蟻組成的蟻群的集體行為便表現出一種資訊正反饋現象。某一路徑上走過的螞蟻越多,則後來者選擇該路徑的可能性就越大。螞蟻個體之間就是通過這種間接的通訊機制實現協同搜尋最短路徑的目標的。我們舉例簡單說明螞蟻覓食行為:

    

    如上圖a,b,c的示意圖:

    a圖是原始狀態,螞蟻起始點為A,要到達E,中途有障礙物,要繞過才能到達。BC和BH是繞過障礙物的2條路徑(假設只有2條)。各個路徑的距離d已經標定。

    b圖是t=0時刻螞蟻狀態,各個邊上有相等的資訊素濃度,假設為15;

    c圖是t=1時刻螞蟻經過後的狀態,各個邊的資訊素濃度,有變化;因為大量螞蟻的選擇概率會不一樣,而選擇概率是和路徑長度相關的。所以越短路徑的濃度會越來越大,經過此短路徑達到目的地的螞蟻也會比其他路徑多。這樣大量的螞蟻實踐之後就找到了最短路徑。所以這個過程本質可以概括為以下幾點:

    1.路徑概率選擇機制資訊素蹤跡越濃的路徑,被選中的概率越大

    2.資訊素更新機制路徑越短,路徑上的資訊素蹤跡增長得越快

    3.協同工作機制螞蟻個體通過資訊素進行資訊交流。

    從螞蟻覓食的原理可見,單個個體的行為非常簡單螞蟻只知道跟蹤資訊素爬行並釋放資訊素,但組合後的群體智慧又非常高螞蟻群能在複雜的地理分佈的清況下,輕鬆找到蟻穴與食物源之間的最短路徑。這種特點恰恰與元啟發演算法的特點相一致,蟻群優化演算法正是受到這種生態學現象的啟發後加以模仿並改進而來,覓食的螞蟻由人工蟻替代,螞蟻釋放的資訊素變成了人工資訊素,螞蟻爬行和資訊素的蒸發不再是連續不斷的,而是在離散的時空中進行。

  上述例子如果不好理解,我在這裡貼幾張PPT,個人感覺非常不錯,也是我找了很多資料覺得最好理解的【來源是大連理工大學谷俊峰】,點選這裡提供下載:蟻群演算法基本知識.rar

    從深層意義上來講,蟻群演算法作為優化的方法之一,屬於人工群集智慧領域。人工群集智慧,大都受自然群集智慧如昆蟲群和動物群等的啟發而來。除了具有獨特的強有力的合作搜尋能力外,還可以利用一系列的計算代理對問題進行分散式處理,從而大大提高搜尋效率。

3.群蟻演算法的基本流程

  我們還是採用大連理工大學谷俊峰的PPT來說明問題,重要公式進行截圖計算和解釋,對PPT難以理解的地方進行單獨解釋:

3.1 基本數學模型

  首先看看基本TSP問題的基本數學模型:

  問題其實很簡單,目標函式就是各個走過路徑的總長度,注意的就是距離矩陣根據實際的問題不一樣,長度是不一樣的。

3.2 群蟻演算法說明

  在說明群蟻演算法流程之前,我們對演算法原理和幾個注意點進行描述:

1.TSP問題的人工蟻群演算法中,假設m只螞蟻在圖的相鄰節點間移動,從而協作非同步地得到問題的解。每隻螞蟻的一步轉移概率由圖中的每條邊上的兩類引數決定:1. 資訊素值也稱資訊素痕跡。2.可見度,即先驗值。
2.資訊素的更新方式有2種,一是揮發,也就是所有路徑上的資訊素以一定的比率進行減少,模擬自然蟻群的資訊素隨時間揮發的過程;二是增強,給評價值“好”(有螞蟻走過)的邊增加資訊素。
3.螞蟻向下一個目標的運動是通過一個隨機原則來實現的,也就是運用當前所在節點儲存的資訊,計算出下一步可達節點的概率,並按此概率實現一步移動,逐此往復,越來越接近最優解。
4.螞蟻在尋找過程中,或者找到一個解後,會評估該解或解的一部分的優化程度,並把評價資訊儲存在相關連線的資訊素中。 

3.3 群蟻演算法核心步驟

  更加我們前面的原理和上述說明,群蟻演算法的2個核心步驟是 路徑構建 和 資訊素更新。我們將重點對這2個步驟進行說明。

3.3.1 路徑構建

  每個螞蟻都隨機選擇一個城市作為其出發城市,並維護一個路徑記憶向量,用來存放該螞蟻依次經過的城市。螞蟻在構建路徑的每一步中,按照一個隨機比例規則選 擇下一個要到達的城市。隨機概率是按照下列公式來進行計算的:

  上述公式就是計算 當前點 到 每一個可能的下一個節點 的概率。分子是 資訊素強度 和 能見度 的冪乘積,而分母則是所有 分子的和值。這個剛開始是很不容易理解的,我們在最後例項計算的時候,可以看得很清楚,再反過來理解公式。注意每次選擇好節點後,就要從可用節點中移除選擇的節點。

3.3.2 資訊素更新

  資訊素更新是群蟻演算法的核心。也是整個演算法的核心所在。演算法在初始期間有一個固定的濃度值,在每一次迭代完成之後,所有出去的螞蟻回來後,會對所走過的路線進行計算,然後更新相應的邊的資訊素濃度。很明顯,這個數值肯定是和螞蟻所走的長度有關係的,經過一次次的迭代, 近距離的線路的濃度會很高,從而得到近似最優解。那我們看看資訊素更新的過程。

  初始化資訊素濃度C(0),如果太小,演算法容易早熟,螞蟻會很快集中到一條區域性最優路徑上來,因為可以想想,太小C值,使得和每次揮發和增強的值都差不多,那麼 隨機情況下,一些小概率的事件發生就會增加非最優路徑的資訊素濃度;如果C太大,資訊素對搜尋方向的指導性作用減低,影響演算法效能。一般情況下,我們可以使用貪婪演算法獲取一個路徑值Cnn,然後根據螞蟻個數來計算C(0) = m/Cnn ,m為螞蟻個數

  每一輪過後,問題空間中的所有路徑上的資訊素都會發生蒸發,然後,所有的螞蟻根據自己構建的路徑長度在它們本輪經過的邊上釋放資訊素,公式如下: 

  資訊素更新的作用:
1.資訊素揮發(evaporation)資訊素痕跡的揮發過程是每個連線上的 資訊素痕跡的濃度自動逐漸減弱的過程,這個揮發過程主要用於避 免演算法過快地向區域性最優區域集中,有助於搜尋區域的擴充套件。
2.資訊素增強(reinforcement)增強過程是蟻群優化演算法中可選的部 分,稱為離線更新方式(還有線上更新方式)。這種方式可以實現 由單個螞蟻無法實現的集中行動。基本蟻群演算法的離線更新方式是 在蟻群中的m只螞蟻全部完成n城市的訪問後,統一對殘留資訊進行 更新處理。

3.3.3 迭代與停止

  迭代停止的條件可以選擇合適的迭代次數後停止,輸出最優路徑,也可以看是否滿足指定最優條件,找到滿足的解後停止。最重要的是,我剛開始理解這個演算法的時候,以為每一隻螞蟻走一條邊就是一次迭代,其實是錯的。這裡演算法每一次迭代的意義是:每次迭代的m只螞蟻都完成了自己的路徑過程,回到原點後的整個過程。

4.群蟻演算法計算例項

  使用PPT中的一個案例,非常直觀,對幾個符號錯誤進行了修改,主要是計算概率的乘號,結果沒有錯誤:

這裡螞蟻2的應該選擇城市A。

步驟四中的τAC 計算應該等於0.233

  過程總體還是比較簡單的,注意理解公式,然後把公式和例項結合起來看,最好是拿筆自己手動畫一畫,容易理解。下面我們來看看如何程式設計實現TSP問題的群蟻演算法程式碼。

5.TSP問題的群蟻演算法C#程式碼實現

  百度搜索相關群蟻演算法的程式碼,基本都是matlab的,在CSDN有一個asp.net + C#版本的實現,不過我看了之後果斷決定重寫,封裝不夠完善,同時思路也不清楚。所以自己寫的過程,理解也更清楚了。經過我的簡單更改,目前還說得過去吧,當然後續我還打算繼續進行研究,所以先把基本程式的過程寫下來,當然是利用了C# 的面向物件特性,看了別人寫的 完全面向過程,理解真的很費勁。簡單說說實現過程和程式碼吧。

5.1 群蟻算法系統基類

  我們封裝了一個基礎的BaseTspAntSystem類,包括了一些基本屬性和計算過程,後續相關改進版本可以進行直接繼承。當然設計可能有缺陷,先這樣進行,碰到需求再改吧。 BaseTspAntSystem類的主要屬性如下:

/// <summary>城市數量,N</summary>
public Int32 NCity { get; set; }
/// <summary>螞蟻數量,M</summary>
public Int32 AntCount { get; set; }
/// <summary>資訊啟發式因子a,表徵資訊素重要程度的引數</summary>
public Double Alpha { get; set; }
/// <summary>期望啟發式因子b,表徵啟發式因子重要程度的引數</summary>
public Double Beta { get; set; }
/// <summary>資訊素蒸發的引數p</summary>
public Double Rho { get; set; }
/// <summary>距離資料,不一定是對稱矩陣,D=d(ij)</summary>
public Double[,] Distance { get; set; }
/// <summary>最大迭代次數NC</summary>
public Int32 NcMax { get; set; }
/// <summary>最好的解個數,取最優解列表中個數的數目,可以作為備用方案</summary>
public Int32 BetterPlanCount { get; set; }
private List<Ant> planList;
/// <summary>資訊素濃度</summary>
public double[,] InfoT { get; set; }

  基類有一個建構函式,對系統的初始化就是傳入基本的引數,並對相關列表進行初始化,程式碼如下:

/// <summary>建構函式</summary>
/// <param name="m">螞蟻數量</param>
/// <param name="a">資訊啟發式因子</param>
/// <param name="b">期望啟發式因子</param>
/// <param name="p">資訊素蒸發的引數</param>
/// <param name="distance">距離資料</param>
/// <param name="NCMax">最大迭代次數</param>
/// <param name="planCount">最優解的個數</param>
public BaseTspAntSystem(double[,]distance,Int32 m,double a,double b,double p,int NCMax,int planCount=10)
{
    this.AntCount = m;
    this.Alpha = a;
    this.Beta = b;
    this.Rho = p;
    this.Distance = distance;
    this.NcMax = NCMax;
    this.BetterPlanCount = planCount;
    planList = new List<Ant>();
    NCity = Distance.GetLength(0);//所有的城市個數
    InfoT = new double[NCity, NCity];//初始化資訊素矩陣
}

   核心的是求解過程,完全按照迭代次數要求進行迭代進行,過程就是概率選擇和資訊素更新,我們輔助的用到了Ant螞蟻類,目的就是讓程式更加獨立和容易理解。Ant類裡面有螞蟻路徑尋找過程的所有資訊。下一節將進行介紹。求解過程程式碼如下面,看看註釋和對比演算法進行:

public void TspSolution()
{
    #region 初始化計算
    //計算初始資訊素的值,可直接指定,或者貪婪演算法計算
    double Cnn = GreedyAlgorithm();
    double t0 = (double)AntCount / Cnn;//資訊素初始化
    for (int i = 0; i < NCity; i++)
    {
        for (int j = 0; j < NCity; j++)
        {
            //每條可行的路徑的資訊素初始值
            if (Distance[i, j] != 0) InfoT[i, j] = t0;
        }
    }
    //為每個螞蟻隨機選擇出發城市,List的長度為螞蟻個數,內部List長度為路徑    
    List<Int32> allNodes = new List<int>();
    for (int i = 0; i < NCity; i++) allNodes.Add(i);//所有路徑節點
    //迭代次數
    Int32 NC = 0;
    #endregion

    while (NC<NcMax)
    {
        //生成螞蟻及初始訪問城市,並設定對應禁忌表和路徑列表
        List<Ant> antList = new List<Ant>();
        for (int i = 0; i < AntCount; i++)
            antList.Add(new Ant(i, allNodes.DeepCopy(), false));
        //所有螞蟻依次尋找下一個節點,直到本輪完成
        antList.ForEach(n => n.NextCityUntilFinished(InfoT,Distance,Alpha,Beta,Rho));               
        //統計最優解
        planList.AddRange(antList);//先新增
        planList = planList.OrderBy(n => n.CpathLength).ToList();//排序
        //取出前面指定的幾條最短路徑
        if (planList.Count > BetterPlanCount) 
            planList.RemoveRange(BetterPlanCount, planList.Count - BetterPlanCount);
        NC++;
        //更新資訊素的值:迴圈所有路徑,依次進行新增
        //先揮發
        for (int i = 0; i < NCity; i++) //揮發過程
        {
            for (int j = 0; j < NCity; j++) InfoT[i, j] *= (1.0 - Rho);
        }
        //再增強,迴圈所有螞蟻
        foreach (var item in antList)
        {
            var temp =  1.0 / item.CpathLength;
            foreach (var edge in item.Edge) InfoT[edge.Key, edge.Value] += temp;    
        }
    }
}

5.2 螞蟻功能類

根據演算法的描述,m只螞蟻同時進行自己的工作和尋找路程,是一個並行的過程,因此也在單次過程中,螞蟻都是獨立的。螞蟻的每一次迭代,過程都比較清楚,尋找路徑過程,注意維護一些可用的節點列表,以及最後一條路徑的處理。看看螞蟻類的主要屬性和建構函式:

public class Ant
{
    #region 屬性
    /// <summary>螞蟻編號</summary>
    public Int32 Id { get; set; }
    /// <summary>當前螞蟻已經走過的路徑節點列表,也就是禁忌表
    /// 最後1個就是當前所處的位置
    /// </summary>
    public List<Int32> PathNodes { get; set; }
    /// <summary>當前螞蟻下一步可供選擇的節點列表</summary>
    public List<Int32> selectNodes { get; set; }
    /// <summary>該螞蟻旅行的總長度</summary>
    public Double CpathLength { get; set; }
    /// <summary>當前螞蟻走過的邊,key為起點,value為終點</summary>
    public Dictionary<int, int> Edge;
    private Random rand;
    int N;
    #endregion

    #region 建構函式
    /// <summary>建構函式</summary>
    /// <param name="id">螞蟻編號</param>
    /// <param name="allNodes">所有節點名稱列表</param>
    /// <param name="isFixStart">是否固定起點</param>
    public Ant(Int32 id,List<Int32> allNodes,Boolean isFixStart)
    {
        this.Id = id;
        this.selectNodes = allNodes;
        this.PathNodes = new List<int> ();
        //isFixStart為false給螞蟻隨機1個起點,否則都為0
        if (isFixStart)
        {
            this.PathNodes.Add(0);
            this.selectNodes.RemoveAt(0);
        }
        else
        {
            var temp = new Random().Next(0, allNodes.Count - 1);
            this.PathNodes.Add(temp);
            this.selectNodes.RemoveAt(temp);
        }
        this.CpathLength = 0;
        rand = new Random();
    }
    #endregion
}

  Ant類的核心是尋找下一個城市節點的過程,以及迴圈直到所有路徑都完成。如下面程式碼,是一個迴圈過程:

#region 螞蟻行為-依次按概率選擇下一個城市,直到走完
public void NextCityUntilFinished(double[,] info,double[,] distance,double a,double b,double p)
{
    N = distance.GetLength(0);
    Edge = new Dictionary<int, int>();//經過的邊:起點-終點
    //為空,就不需要計算了
    while(selectNodes.Count >0)
    {
        double sumt = 0;//分母的和值
        Int32 current = PathNodes.Last();
        //依次計算當前點到其他點可選擇點的 值
        Dictionary<Int32, double> dic = new Dictionary<int, double>();
        foreach (var item in selectNodes)
        {
            var temp = Math.Pow(info[current, item], a) * Math.Pow(1.0 / distance[current, item], b);
            sumt += temp;
            dic.Add(item, temp);
        }
        //計算各個點的概率
        var ratio = dic.ToDictionary(n => n.Key, n => n.Value / sumt);
        //產生1個隨機數,並按概率確定下一個城市
        Int32 nextCity = GetNextCityByRandValue(ratio, rand.NextDouble());
        //修改列表
        this.PathNodes.Add(nextCity);
        this.selectNodes.Remove(nextCity);
        this.CpathLength += distance[current, nextCity];
        Edge.Add(current, nextCity);//資訊素增強輔助計算
    }
    //最後1條路徑的問題,額外增加,直接 回原點
    this.CpathLength += distance[PathNodes.Last(), PathNodes.First()];
    Edge.Add(PathNodes.Last(), PathNodes.First());
    this.PathNodes.Add(PathNodes.First());//最後才新增           
}

/// <summary>按照dic中按照順序的節點的概率值,和隨機數rnd的值,確定哪一個為下一個城市</summary>
/// <param name="dic"></param>
/// <param name="rnd"></param>
/// <returns></returns>
int GetNextCityByRandValue(Dictionary<Int32,double> dic,double rnd)
{
    double sum = 0;
    foreach (var item in dic)
    {
        sum += item.Value;
        if (rnd < sum) return item.Key;
        else continue;
    }
    throw new Exception("無法選擇城市");
}
#endregion

  後面的GetNextCityByRandValue是一個輔助函式,進行隨機概率值的選擇,確定是否選擇哪一個節點。

6.資源與參考文獻

[2].文永軍.旅行商問題的兩種智慧演算法[M].西安電子科技大學,2010年

[3].楊瑞臣.有時間窗和在前約束車輛路徑問題的蟻群優化[M].西安建築科技大學,2005.

[4].谷俊峰.智慧演算法-第七章:蟻群演算法 PPT,大連理工大學,下載:蟻群演算法基本知識.ra