禁忌搜尋演算法詳解
引言
對於優化問題相關演算法有如下分類:
禁忌搜尋是由區域性搜尋演算法發展而來,爬山法是從通用區域性搜尋演算法改進而來。在介紹禁忌搜尋之前先來熟悉下爬山法和區域性搜尋演算法。
區域性搜尋演算法
演算法的基本思想
在搜尋過程中,始終選擇當前點的鄰居中與離目標最近者的方向搜尋。
演算法過程
(1)隨機選擇一個初始的可能解x0 ∈D,xb=x0,P=N(xb);
//D是問題的定義域, xb用於記錄到目標位置的最優解,P為xb的鄰域。
(2)如果不滿足結束條件,則:
//結束條件為迴圈次數或P為空等
(3)Begin;
(4)選擇P的一個子集P',xn為P’的最優解 ;
//P’可根據問題特點,選擇適當大小的子集。可按概率選擇
(5)如果f(xn)<f(xb),則xb=xn,P=N(xb),轉(2);
//重新計算P,f(x)為指標函式
(6)否則P=P-P' ,轉(2);
(7)End;
(8)輸出計算結果;
(9)結束 ;
爬山法
演算法的基本思想
將搜尋過程比作爬山過程,在沒有任何有關山頂的其他資訊的情況下,沿著高度增加的方向爬。如果相鄰狀態沒有比當前值更高,則演算法停止,認為當前值即為頂峰。
演算法過程
(1) 設定初始狀態n=s0為當前狀態;
(2) 如果當前狀態已達標,演算法結束,搜尋成功;
(3)獲取當前狀態n的若干個臨近狀態m,計算這些h(m), nextn=min{h(m)};
(4) IF h(n) < h(nextn)
THEN n:=nextn;
ELSE 取當前狀態為最佳狀態並退出;
(5 ) GOTO (2)步;
該演算法在單峰的條件下,必能達到山頂。
顯而易見爬山法對於複雜情況的求解會遇到以下問題:
(1)區域性極值
(2)山脊:造成一系列的區域性極值
(3)高原:平坦的區域性極值區域——解決辦法:繼續側向移動
目前有些改進的爬山法,比如隨機爬山法、首選爬山法等等不再細說。
禁忌搜尋演算法
演算法思想
標記已經解得的區域性最優解或求解過程,並在進一步的迭代中避開這些區域性最優解或求解過程。區域性搜尋的缺點在於,太過於對某一區域性區域以及其鄰域的搜尋,導致一葉障目。為了找到全域性最優解,禁忌搜尋就是對於找到的一部分區域性最優解,有意識地避開它,從而或得更多的搜尋區域
演算法過程
(1)給定一個禁忌表(Tabu List)H=null,並選定一個初始解X_now.
(2)如果滿足停止規則,則停止計算,輸出結果;否則,在X_now的領域中選出滿足不受禁忌的候選集N(X_now).在N(X_now)中選擇一個評價值最賤的解X_next,X_next:=X_now;更新歷史記錄H, 重複步驟(2).
對搜尋效能有影響的因素
禁忌長度
控制其他變數,單就禁忌長度的選擇而言,禁忌長度越短,機器記憶體佔用越少,解禁範圍更大(搜尋範圍上限越大),但很容易造成搜尋迴圈(實際去搜索的範圍卻很小),過早陷入區域性最優。禁忌長度過長又會導致計算時間過長。
特赦規則
通俗定義:對於在禁忌的物件,如果出現以下情況,不論現在物件的禁忌長度如何,均設為0
(1)基於評價值的規則,若出現一個解的目標值好於前面任何一個最佳候選解,可特赦;
(2)基於最小錯誤的規則,若所有物件都被禁忌,特赦一個評價值最小的解;
(3)基於影響力的規則,可以特赦對目標值影響大的物件。
候選集
候選集的大小,過大增加計算記憶體和計算時間,過小過早陷入區域性最優。候選集的選擇一般由鄰域中的鄰居組成,可以選擇所有鄰居,也可以選擇表現較好的鄰居,還可以隨機選擇幾個鄰居。
評價函式
評價函式分為直接評價函式和間接評價函式。
直接評價函式:上述例子,均直接使用目標值作為評價函式。
間接評價函式:反映目標函式特性的函式(會比目標函式的計算更為簡便,用以減少計算時間等)。
終止規則
禁忌演算法是一個啟發式演算法,我們不可能讓搜尋過程無窮進行,所以一些直觀的終止規則就出現了
(1)確定步數終止,無法保證解的效果,應記錄當前最優解;
(2)頻率控制原則,當某一個解、目標值或元素序列的頻率超過一個給定值時,終止計算;
(3)目標控制原則,如果在一個給定步數內,當前最優值沒有變化,可終止計算。
程式碼
public class TabuSearchAlgorithm {
/** 迭代次數 */
private int MAX_GEN;
/** 每次搜尋鄰居個數 */
private int neighbourhoodNum;
/** 禁忌長度 */
private int tabuTableLength;
/** 節點數量,編碼長度 */
private int nodeNum;
/** 節點間距離矩陣 */
private int[][] nodeDistance;
/** 當前路線 */
private int[] route;
/** 最好的路徑 */
private int[] bestRoute;
/** 最佳路徑總長度 */
private int bestEvaluation;
/** 禁忌表 */
private int[][] tabuTable;
/**禁忌表中的評估值*/
private int[] tabuTableEvaluate;
private DynamicDataWindow ddWindow;
private long tp;
public TabuSearchAlgorithm() {
}
/**
* constructor of GA
*
* @param n
* 城市數量
* @param g
* 執行代數
* @param c
* 每次搜尋鄰居個數
* @param m
* 禁忌長度
*
**/
public TabuSearchAlgorithm(int n, int g, int c, int m) {
nodeNum = n;
MAX_GEN = g;
neighbourhoodNum = c;
tabuTableLength = m;
}
/**
* 初始化Tabu演算法類
*
* @param filename
* 資料檔名,該檔案儲存所有城市節點座標資料
* @throws IOException
*/
private void init(String filename) throws IOException {
// 讀取資料
int[] x;
int[] y;
String strbuff;
FileReader fileReader = new FileReader(filename);
BufferedReader data = new BufferedReader(fileReader);
nodeDistance = new int[nodeNum][nodeNum];
x = new int[nodeNum];
y = new int[nodeNum];
String[] strcol;
for (int i = 0; i < nodeNum; i++) {
// 讀取一行資料,資料格式1 6734 1453
strbuff = data.readLine();
// 字元分割
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 < nodeNum - 1; i++) {
nodeDistance[i][i] = 0; // 對角線為0
for (int j = i + 1; j < nodeNum; 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) {
nodeDistance[i][j] = tij + 1;
nodeDistance[j][i] = nodeDistance[i][j];
} else {
nodeDistance[i][j] = tij;
nodeDistance[j][i] = nodeDistance[i][j];
}
}
}
nodeDistance[nodeNum - 1][nodeNum - 1] = 0;
route = new int[nodeNum];
bestRoute = new int[nodeNum];
bestEvaluation = Integer.MAX_VALUE;
tabuTable = new int[tabuTableLength][nodeNum];
tabuTableEvaluate=new int[tabuTableLength];
for (int i = 0; i < tabuTableEvaluate.length; i++) {
tabuTableEvaluate[i]=Integer.MAX_VALUE;
}
}
/** 生成初始群體 */
void generateInitGroup() {
System.out.println("1.生成初始群體");
boolean iscontinue = false;
for (int i = 0; i < route.length; i++) {
do {
iscontinue = false;
route[i] = (int) (Math.random() * nodeNum);
for (int j = i - 1; j >= 0; j--) {
if (route[i] == route[j]) {
iscontinue = true;
break;
}
}
} while (iscontinue);
// System.out.println("i="+i+", route[i]="+route[i]);
}
}
/** 複製編碼體,複製Gha到Ghb */
public void copyGh(int[] Gha, int[] Ghb) {
for (int i = 0; i < nodeNum; i++) {
Ghb[i] = Gha[i];
}
}
/** 計算路線的總距離 */
public int evaluate(int[] chr) {
// 0123
int len = 0;
// 編碼,起始城市,城市1,城市2...城市n
for (int i = 1; i < nodeNum; i++) {
len += nodeDistance[chr[i - 1]][chr[i]];
}
// 城市n,起始城市
len += nodeDistance[chr[nodeNum - 1]][chr[0]];
return len;
}
/**
* 隨機獲取鄰域路徑
* @param route 當前路徑
* */
public int[] getNeighbourhood(int[] route) {
int temp;
int ran1, ran2;
int[] tempRoute=new int[route.length];
copyGh(route, tempRoute);
ran1 = (int) (Math.random() * nodeNum);
do {
ran2 = (int) (Math.random() * nodeNum);
} while (ran1 == ran2);
temp = tempRoute[ran1];
tempRoute[ran1] = tempRoute[ran2];
tempRoute[ran2] = temp;
return tempRoute;
}
/**
* 隨機獲取一定數量的領域路徑
* */
public int[][] getNeighbourhood(int[] route, int tempNeighbourhoodNum) {
int[][] NeighbourhoodRoutes=new int[tempNeighbourhoodNum][nodeNum];
List<int[]> tempExchangeNodeList=new ArrayList<>();
int temp;
int ran0, ran1;
int[] tempRoute=null;
boolean iscontinue;
for(int i=0; i<tempNeighbourhoodNum; i++) {
tempRoute=new int[route.length];
copyGh(route, tempRoute);
do{
iscontinue=false;
//隨機生成一個鄰域;
ran0 = (int) (Math.random() * nodeNum);
do {
ran1 = (int) (Math.random() * nodeNum);
} while (ran0 == ran1);
//判斷是否重複
for (int j = 0; j <tempExchangeNodeList.size(); j++) {
if (tempExchangeNodeList.get(j)[0]<tempExchangeNodeList.get(j)[1]) {
if ((ran0 < ran1 && (tempExchangeNodeList.get(j)[0]==ran0 && tempExchangeNodeList.get(j)[1]==ran1))
||(ran0 > ran1 && (tempExchangeNodeList.get(j)[0]==ran1 && tempExchangeNodeList.get(j)[1]==ran0))) {
iscontinue=true;
}
}else {
if ((ran0 < ran1 && (tempExchangeNodeList.get(j)[0]==ran1 && tempExchangeNodeList.get(j)[1]==ran0))
||(ran0 > ran1 && (tempExchangeNodeList.get(j)[0]==ran0 && tempExchangeNodeList.get(j)[1]==ran1))) {
iscontinue=true;
}
}
}
if (iscontinue==false) {
temp = tempRoute[ran0];
tempRoute[ran0] = tempRoute[ran1];
tempRoute[ran1] = temp;
tempExchangeNodeList.add(new int[]{ran0,ran1});//將交換點新增到列表中用於查重;
//判斷是否與route相同
for (int j = 0; j < tempRoute.length; j++) {
if (tempRoute[j]!=route[j]) {
iscontinue=false;
}
}
if (iscontinue==false && !isInTabuTable(tempRoute)) {
NeighbourhoodRoutes[i]=tempRoute;
}else {
iscontinue=true;
}
}
}while(iscontinue);
}
return NeighbourhoodRoutes;
}
/** 判斷路徑是否在禁忌表中 */
public boolean isInTabuTable(int[] tempRoute) {
int i, j;
int flag = 0;
for (i = 0; i < tabuTableLength; i++) {
flag = 0;
for (j = 0; j < nodeNum; j++) {
if (tempRoute[j] != tabuTable[i][j]){
flag = 1;// 不相同
break;
}
}
if (flag == 0){// 相同,返回存在相同
break;
}
}
if (i == tabuTableLength){// 不等
return false;// 不存在
} else {
return true;// 存在
}
}
/** 解禁忌與加入禁忌,注意禁忌策略的選擇 */
public void flushTabuTable(int[] tempGh) {
int tempValue=evaluate(tempGh);
// 找到禁忌表中路徑的最大值;
int tempMax=tabuTableEvaluate[0];
int maxValueIndex=0;
for (int i = 0; i < tabuTableLength; i++) {
if(tabuTableEvaluate[i]>tempMax){
tempMax=tabuTableEvaluate[i];
maxValueIndex=i;
}
}
// 新的路徑加入禁忌表
if (tempValue<tabuTableEvaluate[maxValueIndex]) {
if (tabuTableEvaluate[maxValueIndex]<Integer.MAX_VALUE) {
copyGh(tabuTable[maxValueIndex], route);
}
System.out.println("測試點:更新禁忌表,maxValueIndex= "+maxValueIndex);
for (int k = 0; k < nodeNum; k++) {
tabuTable[maxValueIndex][k] = tempGh[k];
}
tabuTableEvaluate[maxValueIndex]=tempValue;
}
}
/**啟動禁忌搜尋*/
public void startSearch() {
int nn;
int neighbourhoodEvaluation;
int currentBestRouteEvaluation;
/** 存放鄰域路徑 */
int[] neighbourhoodOfRoute = new int[nodeNum];
/** 當代最好路徑 */
int[] currentBestRoute = new int[nodeNum];
/** 當前代數 */
int currentIterateNum = 0;
/** 最佳出現代數 */
int bestIterateNum = 0;
int[][] neighbourhoodOfRoutes=null;
//用於控制迭代次數
int[]priviousRoute=new int[nodeNum];
// 初始化編碼Ghh
generateInitGroup();
// 將當前路徑作為最好路徑
copyGh(route, bestRoute);
currentBestRouteEvaluation=evaluate(route);
bestEvaluation = currentBestRouteEvaluation;
System.out.println("2.迭代搜尋....");
while (currentIterateNum < MAX_GEN) {
for (int i = 0; i < route.length; i++) {
priviousRoute[i]=route[i];
}
neighbourhoodOfRoutes=getNeighbourhood(route, neighbourhoodNum);
System.out.println("測試點:currentIterateNum= "+currentIterateNum);
for(nn = 0; nn < neighbourhoodNum; nn++) {
// 得到當前路徑route的一個鄰域路徑neighbourhoodOfRoute
// neighbourhoodOfRoute=getNeighbourhood(route);
neighbourhoodOfRoute=neighbourhoodOfRoutes[nn];
neighbourhoodEvaluation = evaluate(neighbourhoodOfRoute);
// System.out.println("測試:neighbourhoodOfRoute="+neighbourhoodEvaluation);
if (neighbourhoodEvaluation < currentBestRouteEvaluation) {
copyGh(neighbourhoodOfRoute, currentBestRoute);
currentBestRouteEvaluation = neighbourhoodEvaluation;
// System.out.println("測試:neighbourhoodOfRoute="+neighbourhoodEvaluation);
}
}
if (currentBestRouteEvaluation < bestEvaluation) {
bestIterateNum = currentIterateNum;
copyGh(currentBestRoute, bestRoute);
bestEvaluation = currentBestRouteEvaluation;
System.out.println("測試:currentBestRouteEvaluation="+currentBestRouteEvaluation);
}
copyGh(currentBestRoute, route);
// 解禁忌表,currentBestRoute加入禁忌表
// System.out.println("測試點:currentBestRoute= "+currentBestRoute);
flushTabuTable(currentBestRoute);
currentIterateNum++;
for (int i = 0; i < priviousRoute.length; i++) {
if (priviousRoute[i] != route[i]) {
currentIterateNum=0;
break;
}
}
printRunStatus();
}
//結果顯示:
System.out.println("最佳長度出現代數:");
System.out.println(bestIterateNum);
System.out.println("最佳長度:");
System.out.println(bestEvaluation);
System.out.println("最佳路徑:");
for (int i = 0; i < nodeNum; i++) {
System.out.print(bestRoute[i] + ",");
}
}
/**
* @Description: 輸出結執行狀態
*/
private void printRunStatus() {
long millis=System.currentTimeMillis();
if (millis-tp>20) {
tp=millis;
ddWindow.addData(millis, bestEvaluation);
}
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
System.out.println("Start....");
TabuSearchAlgorithm tabu = new TabuSearchAlgorithm(48, 120, 500, 100);
tabu.ddWindow=new DynamicDataWindow("禁忌搜尋演算法優化求解過程");
tabu.ddWindow.setVisible(true);
tabu.init("C:\\Users\\att48.txt");
tabu.startSearch();
}
}
總結
啟發式搜尋演算法蘊含著許多人生哲學,它雖不是數學方法,其思想更類似於人類解決問題的思想和一些人生中總結的道理,值得好好體會。最後用網上一段描述各種搜尋演算法的例子來作為總結:
為了找出地球上最高的山,一群有志氣的兔子們開始想辦法。
(1)兔子朝著比現在高的地方跳去。他們找到了不遠處的最高山峰。但是這座山不一定是珠穆朗瑪峰。這就是爬山法,它不能保證區域性最優值就是全域性最優值。
(2)兔子喝醉了。他隨機地跳了很長時間。這期間,它可能走向高處,也可能踏入平地。但是,他漸漸清醒了並朝他踏過的最高方向跳去。這就是模擬退火。
(3)兔子們知道一個兔的力量是渺小的。他們互相轉告著,哪裡的山已經找過,並且找過的每一座山他們都留下一隻兔子做記號。他們制定了下一步去哪裡尋找的策略。這就是禁忌搜尋。
(4)兔子們吃了失憶藥片,並被髮射到太空,然後隨機落到了地球上的某些地方。他們不知道自己的使命是什麼。但是,如果你過幾年就殺死一部分海拔低的兔子,多產的兔子們自己就會找到珠穆朗瑪峰。這就是遺傳演算法。
參考資料
1.爬山法 。
2.區域性搜尋案例與求解方法。
3.《群體智慧優化演算法及其應用》雷秀娟 著。