區域性搜尋演算法詳解
轉載宣告:這篇文章是從網上好多文章總結摘抄來的,所以也不算是我寫的,沒法標出原轉載網址;
1.區域性搜尋
通常考察一個演算法的效能通常用區域性搜尋能力和全域性收斂能力這兩個指標。區域性搜尋是指能夠無窮接近最優解的能力,而全域性收斂能力是指找到全域性最優解所在大致位置的能力。區域性搜尋能力和全域性搜尋能力,缺一不可。向最優解的導向,對於任何智慧演算法的效能都是很重要的。
定義:
- 區域性搜尋是解決最優化問題的一種啟發式演算法。對於某些計算起來非常複雜的最優化問題,比如各種NP完全問題,要找到最優解需要的時間隨問題規模呈指數增長,因此誕生了各種啟發式演算法來退而求其次尋找次優解,是一種近似演算法(Approximate algorithms),以時間換精度的思想。區域性搜尋就是其中的一種方法。
- 鄰域動作是一個函式,通過這個函式,對當前解s,產生其相應的鄰居解集合。例如:對於一個bool型問題,其當前解為:s = 1001,當將鄰域動作定義為翻轉其中一個bit時,得到的鄰居解的集合N(s)={0001,1101,1011,1000},其中N(s) ∈ S。同理,當將鄰域動作定義為互換相鄰bit時,得到的鄰居解的集合N(s)={0101,1001,1010}.
描述演算法時需用到領域的概念,所謂領域,簡單的說即是給定點附近其他點的集合。在距離空間中,領域一般被定義為以給定點為圓心的一個圓;而在組合優化問題中,領域一般定義為由給定轉化規則對給定的問題域上每結點進行轉化所得到的問題域上結點的集合。公式描述如下:
設D為問題定義域,若存在一對映N,使得:
N:S∈D→N(S)∈2^D
則稱N(S)為N的領域,A∈2^D為N的鄰居。
區域性搜尋(Local Search)的一般過程是:
- 隨機選擇一個初始的可能解x,計算P=N(x)為x在對映N下的領域。
- 如果滿足結束條件則goto (9),其中結束條件包括規定的迴圈次數、P為空。
- Begin
- 選擇P的一個子集,y為此子集的一個最優解。
- 如果f(y)
< f(x),則將y改為臨時的最優解,並將P=N(y),其中f為一指標函式。 - 否則,從P中減去剛才選擇的子集.
- goto (2).
- End
- 輸出計算結果
- 結束
原始的區域性搜尋演算法存在三個改進的演算法,分別從三個不同的側面對其進行了優化。
區域性最優問題:
即考慮到演算法在搜尋過程中陷入到區域性極值點而結束的情況。設想我們去攀登一座山群的最高峰,而此山群有很多的小山峰,且我們對此山群一無所知,那麼當我們按照演算法的步驟來到一座小山峰(區域性極值點)時,我們會錯誤的判斷這就是最高峰,事實上這有可能是一個很糟糕的解(即與最高峰還差很遠)。
步長問題:
為便於理解,我們考慮用此區域性搜尋演算法尋找一開口向下的拋物線的頂點:設此頂點的x座標為10,求領域的對映N定義為N:x∈R,N(x)=x±3(即給定點x的領域僅有在其兩邊相距為3的兩個點),指標函式f(x)=-y(x)(為使指標函式值小的解為較優解,我們讓其取相反數);那麼當我們所選取的初始解為3時,無論如何演算法都將不能在頂點(最優解)處結束。根本原因就是我們的步長固定,所以能夠搜尋到的也僅為一些固定的點。解決此問題可以在搜尋的過程中改變步長(本質為改變對映函式N)。
起始點問題:
在上面步長問題的討論中,我們發現起始點的選擇也對問題的求解有很大的影響,選擇不好可能會導致得不出最優解的情況。一種很自然的解決方案就是隨機的選擇一些可能解,分別以這些可能解為初始解進行搜尋,最後將這些搜尋得到的解進行比較從而選出最優解。
2.區域性搜尋案例與求解方法
一般認為,NP完全問題的演算法複雜性是指數級的。當問題規模達到一定程度時,區域性搜尋演算法、模擬退火演算法和遺傳演算法等引入了隨機因素,不一定能找到最優解,但一般能快速找到滿意的解。
組合優化問題舉例
- TSP問題:從某個城市出發,經過n個指定的城市,每個城市只能且必須經過一次,最後回到出發城市,如何安排旅行商的行走路線以使總路程最短?
- 約束機器排序問題:n 個加工量為di(i=1,2,… n)的產品在一臺機器上加工,機器在第t個時段的工作能力為ct,完成所有產品加工的最少時段數。
- 指派問題: 一家公司經理準備安排N名員工去完成N項任務,每人一項。由於各員工的特點不同,不同的員工去完成同一項任務時獲得的回報是不同的。如何分配工作方案可以獲得最大收益?
- 0-1揹包問題: 設有一個容積為b的揹包,n個體積分別為ai(i=1,2,… n),價值分別為ci ( i=1,2,… n)的物品,如何以最大的價值裝包?
- 裝箱問題: 如何用個數最少的尺寸為1的箱子裝進n個尺寸不超過1的物品?
- SAT問題:稱 判定一個公式是否存在一個模型的問題為可滿足性問題(以後簡稱為SAT問題)。如果一個公式存在模型,則稱該公式是可滿足的,否則稱為不可滿足的。
- 皇后問題: 在n×n的國際象棋棋盤上,擺放n個皇后,使得n個皇后之間不能相互“捕捉”?
區域性搜尋演算法
區域性搜尋演算法是從爬山法改進而來的。爬山法:在沒有任何有關山頂的其他資訊的情況下,沿著最陡的山坡向上爬。區域性搜尋演算法的基本思想:在搜尋過程中,始終選擇當前點的鄰居中與離目標最近者的方向搜尋。
- 爬山演算法
1, n := s;
2, LOOP: IF GOAL(n) THEN EXIT(SUCCESS);
3, EXPAND(n) →{mi}, 計算h(mi), next n=min {h(mi)}
4, IF h(n)
5, n : =next n;
6, GO LOOP;
該演算法在單峰的條件下,必能達到山頂。
- 區域性搜尋演算法
(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)
// 重新計算P,f(x)為指標函式
(6)否則P=P-P’,轉(2)
(7)End
(8)輸出計算結果
(9)結束
區域性最優問題(或叫區域性峰值區域性陷井)
現實問題中,f在D上往往有多個區域性的極值點。
一般的區域性搜尋演算法一旦陷入區域性極值點,演算法就在該點處結束,這時得到的可能是一個糟糕的結果。
解決的方法就是每次並不一定選擇鄰域內最優的點,而是依據一定的概率,從鄰域內選擇一個點。
指標函式優的點,被選中的概率大,指標函式差的點,被選中的概率小。
考慮歸一化問題,使得鄰域內所有點被選中的概率和為1。
區域性搜尋演算法2——可變步長
(1)隨機選擇一個初始的可能解x0屬於D,xb=x0,P=N(xb);
//D是問題的定義域,xb用於記錄到目標位置的最優解,P為xb的鄰域。
(2)如果不滿足結束條件,則: //結束條件為迴圈次數或P為空等
(3)Begin
(4)選擇P的一個子集P’,xn為P’的最優解
(5)如果f(xn)
(6)按某種策略改變步長,計算P=N(xb),轉(2) 繼續
(7)否則P=P-P’,轉(2)
(8)End
(9)輸出計算結果
(10)結束
起始點問題
一般的區域性搜尋演算法是否能找到全域性最優解,與初始點的位置有很大的依賴關係。
解決的方法就是隨機生成一些初始點,從每個初始點出發進行搜尋,找到各自的最優解。再從這些最優解中選擇一個最好的結果作為最終的結果。
起始點位置影響搜尋結果示意圖
區域性搜尋演算法3——多次起始點
(1)k=0
(2)隨機選擇一個初始的可能解x0屬於D,xb=x0,P=N(xb);
(3)如果不滿足結束條件,則:
(4)Begin
(5)選擇P的一個子集P‘,xn為P’的最優解
(6)如果f(xn)
(7)否則P=P-P’,轉(3)
(8)End
(9)k=k+1
(10)如果k達到了指定的次數,則從k個結果中選擇一個最好的結果,否則轉(2)
(11)輸出結果
(12)結束
3.八皇后區域性搜尋示例
/***********************************************************************************
模組:QueenSearch.cpp
目的:解決經典的八皇后問題
思想:區域性搜尋
***********************************************************************************/
#include <Windows.h>
#include <stdio.h>
#include <tchar.h>
#define N 8 // 皇后數目,
////////////////////////////////////////////////////////////////////////////////////
// 皇后
typedef struct _Queen
{
unsigned int x;
unsigned int y;
}QUEEN, *pQUEEN, **ppQUEEN;
// 棋格
typedef struct _Grid
{
pQUEEN pQueen;
}GRID, *pGRID, **ppGRID;
////////////////////////////////////////////////////////////////////////////////////
/*
* 函式宣告提前
*/
// 初始化棋盤,使每行、每列上僅出現一位皇后
bool InitChessBroad(GRID ppChessBroad[][N], pQUEEN pN_Queen, unsigned n);
// 兩皇后衝突則返回true,否則返回false
bool isConflict(pQUEEN pQueen_1, pQUEEN pQueen_2);
// 計算給定一組皇后中發生衝突的數目
unsigned CountOfConflict(pQUEEN pN_Queen, unsigned n);
// 隨機交換兩位皇后的行、列號,若能使衝突數減少則返回true,若任意可能交換都不能夠使衝突數目減少則返回false
bool ChangeTwoQueen(GRID ChessBroad[][N], pQUEEN pN_Queen, unsigned n);
// 列印輸出
void PrintChessBroad(GRID ppChessBroad[][N], unsigned n);
////////////////////////////////////////////////////////////////////////////////////
bool InitChessBroad(GRID ppChessBroad[][N], pQUEEN pN_Queen, unsigned n)
{
int *pavCols; // 可用的行列
unsigned i, j;
int r;
pavCols = new int[n];
/*
考慮到rand()函式的特性,即以靜態變數為基礎按照一定的規則變化此變數,然而每當我們
程式啟動時靜態變數的數值是一樣的,因此不能說成是隨機的,只能說是以特定初始值按照
特定規則變化的一組數值,在此,我們以系統當前啟動時間為基準讓rand()執行一段時間。
*/
DWORD dwTickCount = GetTickCount() % 100;
while (dwTickCount--)
rand();
for (i = 0; i < n; i++)
pavCols[i] = i;
// 掃描每一行,在每一行的合適位置放入一皇后
for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
ppChessBroad[i][j].pQueen = NULL;
while (1)
{
r = rand() % n;
if (pavCols[r] != -1)
{
pavCols[r] = -1;
break;
}
}
//
ppChessBroad[i][r].pQueen = pN_Queen + i;
pN_Queen[i].x = i;
pN_Queen[i].y = r;
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////
bool isConflict(pQUEEN pQueen_1, pQUEEN pQueen_2)
{
if (pQueen_1 == pQueen_2)
return false;
if ((pQueen_1->x == pQueen_1->y) && (pQueen_2->x == pQueen_2->y) // 同在主對角線上
|| ((pQueen_1->x + pQueen_1->y == N - 1) && (pQueen_2->x + pQueen_2->y == N - 1))) // 同在副對角線上
return true;
return (pQueen_1->x == pQueen_2->x || pQueen_1->y == pQueen_2->y)? true:false; // 判定同行同列
}
////////////////////////////////////////////////////////////////////////////////////
unsigned CountOfConflict(pQUEEN pN_Queen, unsigned n)
{
unsigned i, j, Count = 0;
for (i = 0; i < n - 1; i++)
for (j = i + 1; j < n; j++)
if (isConflict(pN_Queen + i, pN_Queen + j))
Count++;
return Count;
}
///////////////////////////////////////////////////////////////////////////////////
bool ChangeTwoQueen(GRID ChessBroad[][N], pQUEEN pN_Queen, unsigned n)
{
int tmp, h1, h2;
unsigned i, j, Count = 0; // 衝突數目
bool *isavH_1, *isavH_2; // 指向一維陣列,用來標識兩層迴圈中可用的皇后
isavH_1 = new bool[n];
isavH_2 = new bool[n];
for (i = 0; i < n; i++)
isavH_1[i] = isavH_2[i] = true; // 初始均可用
Count = CountOfConflict(pN_Queen, n);
// 兩層迴圈產生從n個元素中選擇個的組合數
for (i = 0; i < n - 1; i++)
{
//隨機選擇一個可用的皇后作為第一組合數h1
while (1)
{
h1 = rand() % n;
if (isavH_1[h1])
{
isavH_1[h1] = false;
// 第二組合數從尚未選為第一組合數集合中選擇
for (j = 0; j < n; j++)
isavH_2[j] = isavH_1[j];
break;
}
}
for (j = i + 1; j < n; j++)
{
//隨機選擇一個可用的皇后作為第二組合數h2
while (1)
{
h2 = rand() % n;
if (isavH_2[h2])
{
isavH_2[h2] = false;
break;
}
}
// 交換標號為h1,h2皇后的x座標
tmp = pN_Queen[h1].x;
pN_Queen[h1].x = pN_Queen[h2].x;
pN_Queen[h2].x = tmp;
if (Count > CountOfConflict(pN_Queen, n))
{
// 更改相應的棋格
ChessBroad[pN_Queen[h2].x][pN_Queen[h1].y].pQueen = NULL;
ChessBroad[pN_Queen[h1].x][pN_Queen[h2].y].pQueen = NULL;
ChessBroad[pN_Queen[h1].x][pN_Queen[h1].y].pQueen = pN_Queen + h1;
ChessBroad[pN_Queen[h2].x][pN_Queen[h2].y].pQueen = pN_Queen + h2;
return true;
}
else
{
tmp = pN_Queen[h1].x;
pN_Queen[h1].x = pN_Queen[h2].x;
pN_Queen[h2].x = tmp;
}
// 交換標號為h1、h2皇后的y座標
tmp = pN_Queen[h1].y;
pN_Queen[h1].y = pN_Queen[h2].y;
pN_Queen[h2].y = tmp;
if (Count > CountOfConflict(pN_Queen, n))
{
// 更改相應的棋格
ChessBroad[pN_Queen[h1].x][pN_Queen[h2].y].pQueen = NULL;
ChessBroad[pN_Queen[h2].x][pN_Queen[h1].y].pQueen = NULL;
ChessBroad[pN_Queen[h1].x][pN_Queen[h1].y].pQueen = pN_Queen + h1;
ChessBroad[pN_Queen[h2].x][pN_Queen[h2].y].pQueen = pN_Queen + h2;
return true;
}
else
{
tmp = pN_Queen[h1].y;
pN_Queen[h1].y = pN_Queen[h2].y;
pN_Queen[h2].y = tmp;
}
}
}
// 不存在這樣的交換則返回false
return false;
}
////////////////////////////////////////////////////////////////////////////////////
void PrintChessBroad(GRID ppChessBroad[][N], unsigned n)
{
unsigned i, j;
printf("\n");
for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
{
if (ppChessBroad[i][j].pQueen == NULL)
printf("*\t");
else
printf("H\t");
}
printf("\n\n\n");
}
}
////////////////////////////////////////////////////////////////////////////////////
int _tmain(int argc, _TCHAR* argv[])
{
GRID ChessBroad[N][N];
QUEEN nQueen[N];
while (1)
{
InitChessBroad(ChessBroad, nQueen, N);
while (1)
{
// 完成皇后的佈局
if (0 == CountOfConflict(nQueen, N))
{
PrintChessBroad(ChessBroad, N);
return 1;
}
if (!ChangeTwoQueen(ChessBroad, nQueen, N))
break;
}
}
return 0;
}