實用演算法實現-第 14 篇 啟發式搜尋
14.1 A*搜尋
A*(A star)演算法是一種很好的樹搜尋策略,在許多人工智慧的書籍中有所介紹。比如《人工智慧,一種現代方法》和《人工智慧,複雜問題求解的結構和策略》。
[i]文介紹到,A*演算法使用估價函式F(N)來估算從初始狀態S到當前狀態N,再到目標狀態T需要的最小步數。有公式:
F(N) = G(N) + H(N)
其中,G(N)是起始狀態S到當前狀態的實際代價G(N),它比最佳路徑的代價G*(N)大。
H(N)是從當前狀態到目標狀態T的實際代價的估算值H*(N),它比實際代價H(N)大。
對於一個起始狀態到當前狀態的實際路徑,G(N)是已經決定的。H(N)的選擇則至關重要。有兩個限制:
1. H(N)一定要小於等於當前結點N至目標結點T的實際代價。
2. 任意結點的F值必須大於其父輩狀態的F值,即F單調遞增。
當估價函式F滿足以上兩個限制條件,則啟發式搜尋演算法一定能得出問題的最優解。
14.1.1 例項
PKU JudgeOnline, 1077, Eight.
14.1.2 問題描述
15數碼遊戲就是完成類似下面的轉換的一種遊戲:
1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4
5 6 7 8 5 6 7 8 5 6 7 8 5 6 7 8
9 x 10 12 9 10 x 12 9 10 11 12 9 10 11 12
1314 11 15 13 14 11 15 13 14 x 15 13 14 15 x
r-> d-> r->
8數碼遊戲類似,只不過只有8個數碼而已。
給出8數碼遊戲的一個初始狀態,求達到目標狀態的一個解,不要求是最優解。如果沒有解,輸出“unsolvable”。
14.1.3 輸入
2 3 4 1 5 x 7 6 8
1.1.4 輸出
ullddrurdllurdruldr
14.1.5 分析
《人工智慧,一種現代方法》對使用A*演算法解決八數碼問題進行了深入的分析。提出了兩種啟發函式的策略:
1. h1 = 不在位的棋子的總數。
2. h2 = 所有棋子到其目標位置的曼哈頓距離的總和。
並且通過實驗,得到h2的效能總是優於h1。
具體實現中,用到最小堆實現的優先佇列。
這裡還需要用到一個排列到整數的對映方法,以判斷一種狀態是不是已經遍歷過。一個具體的對映方法如下:
把9看成插到8的排列中,那麼有9種插法,可以把362880等分成9段,分別代表一個位置,每段遞迴處理。
令pos(a)為數字a在排列中排在a前面並且小於a的數字的個數,那麼有虛擬碼:
int n=1;
for (int i=1; i<10; i++)
{
n+=pos(i)*(i-1)!;
}
下面程式中int calPos1(node *temp)和int calPos(node *temp)就是兩種對映方法。
也許[ii]中介紹的排列的“反序”概念,能很好地用來解釋上面這個演算法。
14.1.6 程式
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
bool visited[362880];
#define maxNum 10000000
#define MAX 0x7F7F7F7F
struct tile{
int x;
int y;
};
#define maxLength 100//貌似最數碼遊戲的解長度為...
struct node{
tile pos[3][3];
int key;
charop[maxLength];
int blankX;
int blankY;
};
nodeminHeap[maxNum];
int heapSize;
void minHeapify(int i)
{
int l;
int r;
int min;
node temp;
l = i * 2;
r = i * 2 + 1;
min = i;
if((l <=heapSize)&&
(minHeap[l].key < minHeap[min].key)){
min = l;
}
if((r <=heapSize)&&
(minHeap[r].key < minHeap[min].key)){
min = r;
}
if(min !=i)
{
temp = minHeap[min];
minHeap[min] = minHeap[i];
minHeap[i] = temp;
minHeapify(min);
}
}
void buildMinHeap()
{
int i;
for(i =heapSize / 2; i > 0; i--){
minHeapify(i);
}
}
nodeheapExtractMin()
{
node min;
if(heapSize< 1)
{
cout << "ERROR:no more" << endl;
}
min = minHeap[1];
minHeap[1] = minHeap[heapSize];
heapSize--;
minHeapify(1);
return min;
}
void heapIncreaseKey(int i, int key)
{
node temp;
if(key >minHeap[i].key)
{
cout << "ERROR:new key is smaller than current key" << endl;
}
minHeap[i].key = key;
while(i> 1 && minHeap[i/2].key > minHeap[i].key)
{
temp = minHeap[i];
minHeap[i] = minHeap[i/2];
minHeap[i/2] = temp;
i = i / 2;
}
}
void minHeapInsert(node temp)
{
heapSize++;
minHeap[heapSize] = temp;
minHeap[heapSize].key = MAX;
heapIncreaseKey(heapSize, temp.key);
}
inline int solved(node *temp)
{
int i, j;
for(i = 0;i < 3; i++){
for(j =0; j < 3; j++){
if((*temp).pos[i][j].y!= i || (*temp).pos[i][j].x != j)
{
return0;
}
}
}
return 1;
}
int adj[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
char operation[4] = {'l', 'r', 'u', 'd'};
int calKey(node *temp, int blankX2, int blankY2)
{
int key;
int x1;
int y1;
int x2;
int y2;
int blankX1= (*temp).blankX;
int blankY1= (*temp).blankY;
key = 1;
x1 = abs((*temp).pos[blankY1][blankX1].x -blankX1);
y1 = abs((*temp).pos[blankY1][blankX1].y -blankY1);
x2 = abs((*temp).pos[blankY2][blankX2].x -blankX2);
y2 = abs((*temp).pos[blankY2][blankX2].y -blankY2);
key += - (x1 + y1 + x2 + y2);
x1 = abs((*temp).pos[blankY2][blankX2].x -blankX1);
y1 = abs((*temp).pos[blankY2][blankX2].y -blankY1);
x2 = abs((*temp).pos[blankY1][blankX1].x -blankX2);
y2 = abs((*temp).pos[blankY1][blankX1].y -blankY2);
key += x1 + y1 + x2 + y2;
return key;
}
int factorial[9] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320};
int calPos1(node *temp)
{
int i;
int j;
int x, y;
int x1, y1;
int temp1,temp2;
int num;
int sum;
sum = 0;
for(i = 0;i < 9; i++)
{
y = i / 3;
x = i % 3;
temp1 = (*temp).pos[y][x].x +(*temp).pos[y][x].y * 3;
num = 0;
for(j =0; j < i; j++)
{
y1 = j / 3;
x1 = j % 3;
temp2 = (*temp).pos[y1][x1].x +(*temp).pos[y1][x1].y * 3;
if(temp2< temp1)
{
num++;
}
}
sum += (temp1 - num) * factorial[9 - i- 1];
}
return sum;
}
int calPos(node *temp)
{
inti,j,k=0,b[9],ret=0,num=0;
for(i=0;i<3;i++)
for(j=0;j<3;j++)
b[k++]=(*temp).pos[i][j].x +(*temp).pos[i][j].y * 3;
for(i=0;i<9;i++)
{
num=0;
for(j=0;j<i;j++)
if(b[j]>b[i]) num++;
ret+=factorial[i]*num;
}
return ret;
}
int main()
{
int i;
int j;
int x;
int y;
int blankX;
int blankY;
char c;
int length;
node insertNode;
node temp;
int pos;
bool find;
insertNode.key = 0;
for(i = 0;i < 3; i++){
for(j =0; j < 3; j++){
cin >> c;
if(c== 'x'){
insertNode.blankY = i;
insertNode.blankX = j;
insertNode.pos[i][j].x = 2;
insertNode.pos[i][j].y = 2;
}else{
x = c - '1';
y = x / 3;
x = x % 3;
insertNode.pos[i][j].x = x;
insertNode.pos[i][j].y = y;
}
insertNode.key +=abs(insertNode.pos[i][j].x - j);
insertNode.key +=abs(insertNode.pos[i][j].y - i);
}
}
heapSize = 0;
for(i = 0;i < maxLength; i++)
{
insertNode.op[i] = '\0';
}
memset(visited, 0, sizeof(visited));
pos = calPos(&insertNode);
visited[pos] = 1;
minHeapInsert(insertNode);
find = 0;
int count =0;
while(heapSize> 0){
if(heapSize> maxNum)
{
cout <<endl<< "Too large" << endl;
return-1;
}
temp = heapExtractMin();
if(solved(&temp)== 1)
{
find = 1;
cout << temp.op <<endl;
break;
}
for(i =0; i < 4; i++){
blankX = temp.blankX;
blankY = temp.blankY;
y = blankY + adj[i][0];
x = blankX + adj[i][1];
count++;
if(x< -1 || x > 3 || y < -1 || y > 3)
{
while(1)
{}
}
if(x< 0 || y < 0 || x > 2 || y > 2)
{
continue;
}
insertNode = temp;
insertNode.pos[y][x].x =temp.pos[blankY][blankX].x;
insertNode.pos[y][x].y =temp.pos[blankY][blankX].y;
insertNode.pos[blankY][blankX].x =temp.pos[y][x].x;
insertNode.pos[blankY][blankX].y =temp.pos[y][x].y;
pos = calPos(&insertNode);
if(visited[pos]!= 0){
continue;
}
visited[pos] = 1;
insertNode.key +=calKey(&temp, x, y);
insertNode.blankX = x;
insertNode.blankY = y;
length = strlen(temp.op);
insertNode.op[length] =operation[i];
minHeapInsert(insertNode);
}
}
if(find ==0)
{
printf("unsolvable\n");
}
return 1;
}
14.2 IDA*搜尋
A*演算法減少儲存需求的最簡單的方法就是將迭代深入的思想用在啟發式搜尋上,形成迭代深入A*演算法,即IDA*。
使用BFS搜尋需要實用一個佇列,這個佇列如果過大就可能超出記憶體限制。用DFS代替BFS,可以減少儲存需求。
IDA*演算法實際上是在迭代加深的深度優先搜尋的基礎上進行的一種改進。迭代加深的深度優先搜尋使用當前搜尋深度來判斷是否停止搜尋,而IDA*演算法則使用A*演算法的估價函式F(N) = G(N) + H(N)來判斷是否停止搜尋。可以知道,此時的G(N)就是當前搜尋深度,H(N)則是對於從當前結點N至目標結點T的實際代價一種樂觀估計。說H(N)樂觀,是因為在設計估價函式的時候,H(N)被約束為小於等於實際代價。由此可知,相對於樸素的迭代加深的深度優先搜尋,IDA*演算法省去了一些不必要的搜尋。
14.2.1 例項
PKU JudgeOnline, 2286, The Rotation Game.
14.2.2 問題描述
如上圖一種遊戲,給定初始狀態,通過拉動A到H的,對該行或列進行旋轉。目標是使得中間的八個數字相等。
輸出字典序最小的最優解的旋轉過程,及其最終狀態時中間的數字。
14.2.3 輸入
11 1 1 3 2 3 2 3 1 3 2 2 3 1 2 2 2 3 1 2 1 3 3
11 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3
0
14.2.4 輸出
AC
2
DDHH
2
14.2.5 分析
對搜尋的深度給一個步數限制。每次在狀態空間裡進行DFS搜尋的時候,如果遇見當前狀態決定了已經不可能在限制內找到解,就返回,而不再搜尋其子狀態。如果這次DFS沒找到,就繼續放寬限制,重新對狀態空間進行DFS。如此反覆直到找到一個解。
顯然這個解是最優的解。
確保解字典序最小的辦法是DFS中順次地從A到H中選擇子狀態。
14.2.6 程式
#include <iostream>
using namespace std;
int a[25];
int res[1000];
int ans;
int limit;
int state[8][7][2] = {
{ {1,23}, {3,1}, {7,3}, {12,7}, {16,12},{21,16}, {23,21} },
{ {2,24}, {4,2}, {9,4}, {13,9}, {18,13},{22,18}, {24,22} },
{ {11,5}, {10,11}, {9,10}, {8,9}, {7,8},{6,7}, {5,6} },
{ {20,14}, {19,20}, {18,19}, {17,18},{16,17}, {15,16}, {14,15} },
{ {24,2}, {22,24}, {18,22}, {13,18},{9,13}, {4,9}, {2,4} },
{ {23,1}, {21,23}, {16,21}, {12,16},{7,12}, {3,7}, {1,3} },
{ {14,20}, {15,14}, {16,15}, {17,16},{18,17}, {19,18}, {20,19} },
{ {5,11}, {6,5}, {7,6}, {8,7}, {9,8},{10,9}, {11,10} }
};
int isSuccess()
{
int ll =a[7];
if(a[8]!=ll|| a[9]!=ll || a[12]!=ll || a[13]!=ll ||
a[16]!=ll || a[17]!=ll || a[18]!=ll)
return0;
return ans= ll;
}
int getval()
{
int i;
int cnt[4];
memset(cnt,0,sizeof(cnt));
for(i = 7;i <=9 ;i++)cnt[a[i]]++;
for(i = 12;i <=13 ;i++)cnt[a[i]]++;
for(i = 16;i <=18 ;i++)cnt[a[i]]++;
return8-max(cnt[1],max(cnt[2],cnt[3]));
}
int dfs(int t)
{
inttemp[25];
if(t==limit)
returnisSuccess();
if(t +getval() > limit)return 0;
for(int i = 0 ; i < 8 ; i++)
{
res[t] = i;
memcpy(temp,a,sizeof(a));
for(int j = 0 ; j < 7 ; j++)
a[state[i][j][1]] =temp[state[i][j][0]];
int ret= dfs(t+1);
memcpy(a,temp,sizeof(temp));
if(ret!=0)return ret;
}
return 0;
}
int main()
{
while(true)
{
scanf("%d",&a[1]);
if(!a[1])
break;
for(int i = 2; i <=24; i++)
scanf("%d",&a[i]);
if(isSuccess())
{
cout<<"No moves needed"<<endl<<a[7]<<endl;
continue;
}
limit = 1;
while(dfs(0)== 0)limit++;
for(int i = 0 ; i < limit ;i++)putchar((char)(res[i]+'A'));
printf("\n%d\n",ans);
}
system("pause");
return 0;
}
本文章歡迎轉載,請保留原始部落格連結http://blog.csdn.net/fsdev/article[i] 實用演算法的分析與程式設計。吳文虎,王建德。
[ii] The Art of Computer Programming, Volume 3, Sorting and Searching.Donald E. Knuth.
相關推薦
實用演算法實現-第 14 篇 啟發式搜尋
14.1 A*搜尋 A*(A star)演算法是一種很好的樹搜尋策略,在許多人工智慧的書籍中有所介紹。比如《人工智慧,一種現代方法》和《人工智慧,複雜問題求解的結構和策略》。 [i]文介紹到,A*演算法使用估價函式F(N)來估算從初始狀態S到當前狀態N,再到目標狀態T
實用演算法實現-第 24 篇 高精度整數運算
24.1.1 例項 PKU JudgeOnline, 1503, Integer Inquiry. 24.1.2 問題描述 給定一組超長的正整數(100位),求出它們的和。 24.1.3 輸入 123456789012345678901234567890
【資料結構與演算法】之單鏈表、雙鏈表、迴圈連結串列的基本介紹及其Java程式碼實現---第三篇
一、連結串列的基本介紹 連結串列的定義:連結串列是一種遞迴的資料結構,它或者為空(null),或者是指向一個結點(node)的引用,該結點含有一個泛型的元素和一個指向另一條連結串列的引用。----Algorithms Fourth Edition 常見的連結串
【資料結構與演算法】之棧的基本介紹及其陣列、連結串列實現---第四篇
一、棧的基本介紹 1、棧的基本概念 棧是一種限制在一端進行插入和刪除操作的線性表資料結構。棧中有兩個比較重要的操作:push(壓棧:將元素壓入棧頂)和pop(彈棧:從棧頂彈出一個元素)。都滿足先進後出、後進先出的特點! 從圖中可以看出,我們常把棧的上面稱為棧
【資料結構與演算法】之佇列的基本介紹及其陣列、連結串列實現---第五篇
一、佇列的基本概念 1、定義 佇列是一種先進先出的線性表。它只允許在表的前端進行刪除操作,而在表的後端進行插入操作,具有先進先出、後進後出的特點。進行插入操作的一端成為隊尾(tail),進行刪除操作的一端稱為隊頭(head)。當佇列中沒有元素時,則稱之為空佇列。 在
【資料結構與演算法】之排序全家桶(十大排序詳解及其Java實現)---第七篇
本篇文章彙總了10種場常見的排序演算法,篇幅較長,可以通過下面的索引目錄進行定位查閱: 7、桶排序 一、排序的基本概念 1、排序的定義 排序:就是使一串記錄,按照其中的某個或者某些關鍵字的大小,遞增或遞減的排列起來
面經手冊 · 第14篇《volatile 怎麼實現的記憶體可見?沒有 volatile 一定不可見嗎?》
![](https://img-blog.csdnimg.cn/20201022075049456.png) 作者:小傅哥 部落格:[https://bugstack.cn](https://bugstack.cn) >沉澱、分享、成長,讓自己和他人都能有所收穫!
演算法風暴第1篇-陣列中出現次數超過一半的數字
用頭腦風暴學演算法,對於一個問題,我們不只是要解決它,還要去思考有什麼好的方法,差的方法去解決,甚至是一些錯誤的但可以提供思想借鑑的方法。 此問題“陣列中出現次數超過一半的數字”是一道非常經典的演算法題,我把它放在演算法風暴系列第一篇來解析,探討學習一個演算法
【小白學遊戲常用演算法】二、A*啟發式搜尋演算法
在上一篇部落格中,我們一起學習了隨機迷宮演算法,在本篇部落格中,我們將一起了解一下尋路演算法中常用的A*演算法。 通常情況下,迷宮尋路演算法可以使用深度優先或者廣度優先演算法,但是由於效率的原因,不會直接使用這些演算法,在路徑搜尋演算法中最常見的就是A*尋路演算法。使用A*演算法的魅力之處在於它不僅
“毛星雲OpenCV3程式設計入門之python實現”第三篇讀取視訊+呼叫攝像頭
1.6.1讀取視訊+呼叫攝像頭 # -*- coding: gbk -*- __author__ = 'sunzhilong' import cv2 #讀取視訊,以幀顯示 cap = cv2.VideoCapture("E:/Study/python/Open
“毛星雲OpenCV3程式設計入門之python實現”第六篇基本圖形繪製
4.3基本圖形繪製 python程式碼: # -*- coding: utf-8 -*- __author__ = 'sunzhilong' import cv2 import numpy as np image = np.zeros((600,600,3
“毛星雲OpenCV3程式設計入門之python實現”第七篇影象拆分、合併
5.3影象拆分、合併 python程式碼: # -*- coding: utf-8 -*- import cv2 import numpy as np srcImage = cv2.imread("E:/Study/python/OpenCV_study/i
“毛星雲OpenCV3程式設計入門之python實現”第八篇亮度、對比度
5.4亮度、對比度 python程式碼: # -*- coding: utf-8 -*- import cv2 import numpy as np gcontrastvalue = 80 # 對比度 gbrightvalue = 80
【搞定Java併發程式設計】第14篇:Java中鎖的概述
上一篇:重排序:https://blog.csdn.net/pcwl1206/article/details/84930669 1、Lock介面 鎖是用來控制多個執行緒訪問共享資源的方式,一般來說,一個鎖能夠防止多個執行緒同時訪問共享資源(但有些鎖可以允許多個執行緒併發訪問共享資源,比如:讀
mysql學習【第14篇】:pymysql pymysql模組
pymysql模組 一、安裝的兩種方法 第一種 #安裝 pip3 install pymysql 第二種
人工智慧: 自動尋路演算法實現(一、廣度優先搜尋)
前言 隨著人工智慧技術的日益發達,我們的生活中也出現了越來越多的智慧產品。我們今天要關注的是智慧家居中的一員:掃地機器人。智慧掃地機器人可以在主人不在家的情況下自動檢測到地面上的灰塵,並且進行清掃。有些更為對路線進行規劃,找到可以清理灰塵的最短路徑,達到省電的
R語言學習系列(資料探勘之決策樹演算法實現--ID3程式碼篇)
轉載自:http://blog.csdn.net/hawksoft/article/details/7760868 1、輔助類,用於計算過程和結果儲存 [csharp] view plaincopyprint? /// &
[038] 微信公眾帳號開發教程第14篇-自定義選單的建立及選單事件響應
微信5.0釋出 2013年8月5日,伴隨著微信5.0 iPhone版的釋出,公眾平臺也進行了重要的更新,主要包括: 1)運營主體為組織,可選擇成為服務號或者訂閱號; 2)服務號可以申請自定義選單; 3)使用QQ登入的公眾號,可以升級為郵箱登入; 4)使用郵箱登入的公眾號,
Java密碼學原型演算法實現——第三部分:雙線性對
背景介紹 技術部落格已經好久沒更新了。倒不是因為沒得寫,是因為實在是太忙了,而且研究也到了一個瓶頸期,需要大量閱讀文獻。 本來打算很長一段時間都不更新部落格了,甚至打算等我畢業工作後再更新一些有價值的部落格,但是最近在CSDN私信上和知乎上經常收到求救帖子,
今天開始做戰鬥,回合制戰鬥程式碼實現第四篇 刀塔傳奇戰鬥模式(即時卡牌戰鬥模式)
說是即時卡牌戰鬥,其實在我看來這種玩法也是回合制戰鬥的一種,差不多算是九宮格戰鬥的一種變種,在一個回合120秒內,分成了3次小規模的遇怪自動戰鬥,而這種自動戰鬥不在是回合而是即時的,但整個戰鬥過程,都不是做即時通訊的,所以對網路要求也不是很高,一般2d網路就