題目如下:
用一個10行10列的二維平面表格表示迷宮,左上角作為迷宮的入口,右下角作為迷宮的出口。設迷宮中有一只貓在隨機游走,一只老鼠要從迷宮的入口逃到出口。如果老鼠遇到貓就會被吃掉。假定老鼠和貓的速度是相同的,而且貓不會主動搜尋老鼠。問題求解的目標是老鼠尋找一條從入口到出口的通路,並且不會被貓吃掉,寫出問題的求解規則。(心形為老鼠初始位置,菱形為貓初始位置,五角星為迷宮出口)
這是人工智能導論課堂上老師留的一道作業題,課堂展示之后,為了不想讓自創的解題思路就這樣隱沒,於是決定浪費一些時間寫成博客,紀錄下來。由於從開始着手解決問題到編程實現只用了不到兩天的時候,所以一定會有算法漏洞以及邏輯冗余的情況。希望各路大神盡情指點,小輩一定認真學習。由於沒有細心地在網上查找有沒有相同的解題思路,所以如果雷同,純屬巧合。
第一眼看到題目的時候,本能反應是使用棧作為數據結構,再進行回溯來遍歷整個迷宮,但又隱隱覺得別扭。后來又由於個人懶散,直到課堂展示的前兩天才開始真正認真地考慮如何解決題目。
首先,為了解決這個問題,我們需要解決的疑點有二:
一、老鼠如何找到迷宮的出口;
二、如何讓老鼠躲避貓,不被抓到。
有貓沒貓,會對老鼠尋找出口的軌跡造成一定影響,但不會影響老鼠大體上的行走規則、行走指導思想。所以我們優先重點考慮問題一:老鼠如何找到迷宮的出口。
說起走迷宮,記得小時候玩仙劍奇俠傳的時候,遇到過迷宮,走起來很費勁……直到有一天父親對我說,走迷宮呀,貼着一邊的牆走就可以啦!這句話,我一直記得很清,為什么?是因為這句話對我的現實生活和玩游戲造成了很大的影響。那么對於這道題、這個老鼠,賦予它貼牆走的智慧,它完全就可以非常輕松地找到迷宮出口。所以我們該如何遍歷整體迷宮?我們用貼牆走來遍歷整個迷宮!(雖然貼牆走在本題中展現出良好的優越性,但是我個人也意識到這個算法有一定的缺陷,會在本篇的最后說明。)
在真正開始講解之前,大家需要認可和熟悉我們解決遍歷整個迷宮的基本算法規則,貼牆行走便可遍歷此圖。如果有對此規則保持迷惑,可以在詳解貼牆走規則后,親自使用地圖,嘗試遍歷整個迷宮后做近一步理解。在此題中,我們使用貼右牆走的方法,選擇右牆是因為比較符合我們個人習慣,但左右牆都可以實現遍歷整個地圖。
需要給大家說清楚的是,什么叫貼右牆走?怎么算是貼右牆走?如何做到貼右牆走?規則總結起來很簡單,那就是右手要始終挨着牆並且腳步要往前邁,不管遇到任何牆角任何路口,右手要始終挨着牆,這一點非常重要。
首先,假如我們處於這樣的四周環境下,如下圖所示(X代表牆,十字架中心代表我們現所在位置,空白表示是通路),北側和南側是牆,只有西側和東側是道路可以行走。(我將使用東南西北作為方位來描述,而不是前后左右,因為東西南北是絕對的,而前后左右是相對的,原因會在下面慢慢說明)
假如,我們現在面朝東,那么按照我們貼着右牆走的規則,下一步我們將會向東側行駛。但是如果我們現在面朝西呢?我們需要轉個身子過來,這時候我們再按照貼着右牆走的規則,我們將會向西側行駛。
為什么在同一種所處環境下,我們會走不同的方向?為什么換一個面朝方向,我們下一步將要走的格子就會有變化?這一切都歸於一個字,右。貼右牆、貼右牆,而右是一個相對位置,當我們面朝不同方向的時候,我們的右牆也不是同一堵牆。所以大家可以看到,即使在這相同環境下,但面朝方向的不同,對於下一步的選擇,我們有着不同的答案。
而大家更好理解的可能是,當我們面朝相同方向的時候,如都朝北。但因為所處環境的差異,我們將走向不同的方向。如下圖所示,當我們身處左側圖時,我們會選擇向西;而當我們身處右側圖時,同樣是面朝北,我們會繼續向北。所以可以簡單得出,即使面朝同一方向,但由於處在不同環境,對於下一步的選擇,我們依然有不同的選擇。
舉前面的例子,只是想告訴大家。在這個迷宮中我們會遇到各種各樣的情況,4種朝向、14種環境,相乘后便是56種不一樣情形。對待這56種情形,老鼠做出的判斷也應該是不同的(遵循貼右牆行駛的原則而做出的判斷)。那么我們該如何在程序中實現對待這56種情形做出相應的判斷呢?
首先,描述這56種情況中的任何一種都是相對不易的,可以自己想一下,我們的判斷條件中需要有四周圍牆的有無和老鼠的朝向。而如果直接用程序實現,程序會顯得異常冗長,所以我們只能尋找更好的手段來標識判斷這56種情況。
所以,我們想到了用數字標識,數字簡單清晰,一長串case語句便可將四分之一情況歸納總結,但是我們應該如何對每一種情況給定一個特定的數字?用1,2,3,4,5,6一直到56來標識這56種情況可不可以?一個數字一種情況,標識每個圖的問題解決了,但留下來的問題是老鼠怎么知道它現在所在的位置是多少號?可能第一步還因為程序里有老鼠初始位置的信息,知道自己是第17種情況,應該向右走(假設),但是第二步呢?程序總不能把老鼠要走的路徑全都存在程序里吧?那和直接把走向迷宮終點的路徑告訴小鼠,讓小鼠沿着路徑走過去有什么區別嗎?所以我們不能這樣簡簡單單的使用數字標識,而是需要用這些數字來滿足一些實時性的要求,也就是說,當老鼠處於這樣的環境、這樣的朝向時,它可以立刻求出用來標識的數字。
我們現在有兩個變量,一是老鼠面朝方向、二是四周牆面情況。我們可以認為這是一種稍微復雜的“映射”,通過變量一和變量二的計算,得到變量三,而變量三便是我們想要的標識數字。而我們的要求只有一點,那就變量三必須是唯一的,如果有兩對(變量一,變量二)計算,可以同時得到相同的變量三,那么變量三將失去作為標識的作用,同時我們也認為此種映射法則是失敗的。
於是,我們先對老鼠面朝方向賦予了權重,面朝北權重為1,面朝西權重為2,面朝南權重為3,面朝東權重為17,如下所示:
然后我們再對四周牆壁賦予了權重,北面牆權重為7,西面牆權重為11,南面牆權重為31,東面牆權重為37,如下圖所示:
於是自創公式:
weight_value = direction_value * (map[x-1][y] * W_NORTH + map[x][y - 1] * W_WEST+ map[x + 1][y] * W_SOUTH+ map[x][y + 1] * W_EAST)
其中direction_value為老鼠當前面朝方向的權重,W_NORTH,W_WEST,W_SOUTH,W_EAST分別為北牆、西牆、南牆、東牆各自的權值。由於此迷宮存儲在二維數組中,值為0認為是牆、值為1認為是通路,而坐標(x , y)是老鼠所在的中心坐標。故此自創“權值公式”中文含義是:老鼠面朝方向的權值乘以所有通路方向的權值之和得到的數字,是此情形(此環境,此朝向)的標識數字。
標識數字的選擇,如上文所說,我們只有一個要求,要求它的唯一性。所以我們對這8個數字的嚴格挑選,需要確保在56次計算得出來的數字沒有任意兩個相同。近十次的挑選8個數字的組合后,組合(1,2,3,17,7,11,31,37)符合我們要求,故我們使用這8個數字作為我們的權值,來時時刻刻標記老鼠的情形,並進一步做出判斷指導。如下圖便是,這些標識數字代表的是下一步應該向東西南北各個方向前進。
Ps:由於公式的自創性,故應該會存在更好的公式,更方便理解和計算,望大神們點撥。
根據計算,我們可以總結得出,當出現某些特定數字時,我們將向某特定方向行駛,於是我們貼右牆就有了完整的指導方針!所以直至現在,我們已經將問題一:老鼠如何找到迷宮出口成功解決,接下來,我們將繼續解決問題二:如何讓老鼠躲避貓?
我們需要解決的問題有兩個:
第一:如何不讓老鼠主動碰到貓。
第二:如何做到當老鼠遇到貓逃跑后,依然可以盡快找到迷宮出口。
對於第一個問題,我們將采用,認為貓是一堵牆的方法,來絕對避免老鼠主動碰到貓;與此同時,為了確保問題的簡單,我們同時認為老鼠也是一堵牆,貓絕不可能主動碰到老鼠。而對於貓和老鼠誰先走的問題,我們做出這樣的理解,我們賦予老鼠這樣的智慧:等到貓走了,我再走。我們需要強調的是,我們不是在刻意模糊題目,而是在賦予老鼠這樣的規則,來確保老鼠的安全(當然,如果把老鼠也當作一堵牆,貓是永遠不會撲向老鼠的。但在解題的初期,我們是有考慮如何避免貓碰到老鼠,由於后期覺得較復雜就舍棄掉了,但解釋還是這樣解釋)
對於第二個問題,我們將使用貼牆走的方式來作為逃跑方向的指導方針並采用棧來存儲遇到貓后逃跑的路線。使用什么方向並不重要,所以我們直接使用之前的算法作為逃跑方向的指導;而對於為什么使用棧來存儲逃跑路線,是因為棧的后進先出的特點,很符合我們現有的情況。既然我們從這里跑,那么我們還要回到這里,我們還要從遇到貓的這個地方繼續開始對迷宮進行遍歷,否則我們可能會恰好漏掉迷宮出口的位置而進行第二次對迷宮的遍歷。
大體的思路是這樣,但實際的程序邏輯卻比較復雜,在這里,我們簡單的表示一下邏輯關系。
1 If(老鼠現在不在逃跑『也就是沒遇到貓』) 2 3 { 4 5 If(貓不在老鼠下一步的位置) 6 7 老鼠按照貼牆規則正常前進; 8 9 Else 『貓恰好擋住了老鼠的去路』 10 11 將老鼠所在位置信息入棧,並繼續按照貼牆規則前進; 12 } 13 14 Else 『老鼠現在正在逃跑』 15 16 { 17 18 If(貓繼續追過來了) 19 20 將老鼠所在位置信息入棧,並繼續按照貼牆規則前進; 21 22 Else『貓繼續追過來了貓不在棧頂所存信息的位置,可以簡單理解為貓沒有追來』 23 24 老鼠按照棧頂信息回到上一步的位置,並彈棧; 25 26 }
直到現在,我們已經成功的解決了貓與老鼠的迷宮問題,下面是C++的完成實現代碼。如果需要測試,需要在Do_it()函數中的while語句增添節點,通過一次debug便可實現貓和老鼠的一次移動。由於趕代碼太匆忙了,注解不足,同時代碼可能會有大量冗余,會在閑時慢慢修改,盡情諒解!
1 #include<iostream> 2 using namespace std; 3 #include <stdlib.h> 4 #include <time.h> 5 6 #define ROW 10 7 #define COL 10 8 9 #define T_NORTH 1 10 #define T_WEST 2 11 #define T_SOUTH 3 12 #define T_EAST 17 13 14 #define W_NORTH 7 15 #define W_WEST 11 16 #define W_SOUTH 31 17 #define W_EAST 37 18 19 int map[ROW][COL] = 20 { 21 { 0,0,0,0,0,0,0,0,0,0 }, 22 { 0,1,1,1,1,0,1,1,1,0 }, 23 { 0,0,0,1,0,0,1,0,1,0 }, 24 { 0,1,1,1,0,1,1,0,0,0 }, 25 { 0,1,0,1,1,1,1,0,1,0 }, 26 { 0,1,0,1,1,0,1,1,1,0 }, 27 { 0,1,0,0,0,0,0,1,1,0 }, 28 { 0,1,1,1,1,0,1,0,1,0 }, 29 { 0,1,0,1,1,0,1,1,1,0 }, 30 { 0,0,0,0,0,0,0,0,0,0 }, 31 }; 32 33 int x = 1, y = 1;//老鼠位置坐標(x,y) 34 int cx = 4, cy = 6;//貓位置坐標(cx,cy) 35 int cx0 = 4, cy0 = 6; 36 37 38 void Display_Map() 39 { 40 for (int i = 0; i < ROW; i++) 41 { 42 for (int j = 0; j < COL; j++) 43 { 44 if (i == x&&j == y) 45 cout << "●"; 46 else if (i == cx&&j == cy) 47 cout << "▲"; 48 else 49 { 50 if (!map[i][j]) 51 cout << "□"; 52 else 53 cout << "■"; 54 } 55 } 56 cout << endl; 57 } 58 } 59 60 void Cat_trail() 61 { 62 extern int cx, cy; 63 extern int cx0, cy0;//貓之前的位置(cx0,cy0) 64 int Cat_direction; 65 66 srand((unsigned)time(NULL)); 67 //初始化隨機數種子 68 69 //cout << "(" << cx << "," << cy << ")" << endl; 70 71 int Cat_judge = 0; 72 while (!Cat_judge) //產生一個隨機方向 73 { 74 Cat_direction = rand() % 4; 75 76 switch (Cat_direction) 77 { 78 case 0: 79 Cat_judge = map[cx - 1][cy]; 80 break; 81 case 1: 82 Cat_judge = map[cx][cy - 1]; 83 break; 84 case 2: 85 Cat_judge = map[cx + 1][cy]; 86 break; 87 case 3: 88 Cat_judge = map[cx][cy + 1]; 89 break; 90 default: 91 break; 92 } 93 } 94 95 //下一步行走 96 switch (Cat_direction) 97 { 98 case 0: 99 cx = cx - 1; 100 break; 101 case 1: 102 cy = cy - 1; 103 break; 104 case 2: 105 cx = cx + 1; 106 break; 107 case 3: 108 cy = cy + 1; 109 break; 110 default: 111 break; 112 } 113 114 map[cx0][cy0] = 1;//將貓剛走過的位置還原成0 115 cx0 = cx, cy0 = cy; 116 map[cx][cy] = 0; 117 118 } 119 120 void Do_it() 121 { 122 extern int x, y; 123 int x0 = 1, y0 = 1;//老鼠之前的位置(x0,y0) 124 125 int direction_num = T_EAST;//方向數,北1,西2,南3,東17 126 127 int weight_num = 0;//位置(x,y)權值 128 int pre_weight_num = 0; 129 int escape[10][3] = { 0 }; 130 int s_top = 0; 131 132 int pre_Way_judge = 0; 133 int Way_judge = 0; 134 int Escape_judge = 0; 135 136 while (x != 8 || y != 8) 137 { 138 map[x][y] = 0;//將老鼠在地圖上的位置標記 139 x0 = x, y0 = y; 140 141 pre_weight_num = direction_num*(map[x - 1][y] * W_NORTH + map[x][y - 1] * W_WEST + map[x + 1][y] * W_SOUTH + map[x][y + 1] * W_EAST);//計算貓移動前分叉路口數 142 switch (pre_weight_num) 143 { 144 //下一步,向北行駛 145 case 36:case 18:case 306:case 150: 146 case 110:case 98:case 49:case 38: 147 case 76:case 88:case 119:case 21: 148 case 7:case 14: 149 pre_Way_judge = 0; 150 break; 151 //下一步,向西行駛 152 case 158:case 237:case 165:case 147: 153 case 126:case 42:case 84:case 144: 154 case 96:case 187:case 33:case 11: 155 case 22:case 54: 156 pre_Way_judge = 1; 157 break; 158 //下一步,向南行駛 159 case 1343:case 1275:case 225:case 833: 160 case 646:case 114:case 1156:case 714: 161 case 204:case 136:case 527:case 93: 162 case 31:case 62: 163 pre_Way_judge = 2; 164 break; 165 //下一步,向東行駛 166 case 79:case 75:case 55:case 935: 167 case 48:case 748:case 132:case 44: 168 case 816:case 68:case 629:case 111: 169 case 37:case 74: 170 pre_Way_judge = 3; 171 break; 172 } 173 174 Cat_trail(); 175 176 int Cash_judge = 0;//是否碰撞 177 switch (pre_Way_judge) 178 { 179 case 0: 180 if (x - 1 == cx&&y == cy) 181 Cash_judge = 1; 182 break; 183 case 1: 184 if (x == cx&&y - 1 == cy) 185 Cash_judge = 1; 186 break; 187 case 2: 188 if (x + 1 == cx&&y == cy) 189 Cash_judge = 1; 190 break; 191 case 3: 192 if (x == cx&&y + 1 == cy) 193 Cash_judge = 1; 194 break; 195 } 196 197 weight_num = direction_num*(map[x - 1][y] * W_NORTH + map[x][y - 1] * W_WEST + map[x + 1][y] * W_SOUTH + map[x][y + 1] * W_EAST);//計算貓移動后分叉路口數 198 switch (weight_num) 199 { 200 //下一步,向北行駛 201 case 36:case 18:case 306:case 150: 202 case 110:case 98:case 49:case 38: 203 case 76:case 88:case 119:case 21: 204 case 7:case 14: 205 Way_judge = 0; 206 break; 207 //下一步,向西行駛 208 case 158:case 237:case 165:case 147: 209 case 126:case 42:case 84:case 144: 210 case 96:case 187:case 33:case 11: 211 case 22:case 54: 212 Way_judge = 1; 213 break; 214 //下一步,向南行駛 215 case 1343:case 1275:case 225:case 833: 216 case 646:case 114:case 1156:case 714: 217 case 204:case 136:case 527:case 93: 218 case 31:case 62: 219 Way_judge = 2; 220 break; 221 //下一步,向東行駛 222 case 79:case 75:case 55:case 935: 223 case 48:case 748:case 132:case 44: 224 case 816:case 68:case 629:case 111: 225 case 37:case 74: 226 Way_judge = 3; 227 break; 228 } 229 230 if (Escape_judge == 0) 231 { 232 if (!Cash_judge) 233 { 234 switch (Way_judge) 235 { 236 case 0: 237 { 238 x = x - 1; 239 direction_num = T_NORTH; 240 } 241 break; 242 case 1: 243 { 244 y = y - 1; 245 direction_num = T_WEST; 246 } 247 break; 248 case 2: 249 { 250 x = x + 1; 251 direction_num = T_SOUTH; 252 } 253 break; 254 case 3: 255 { 256 y = y + 1; 257 direction_num = T_EAST; 258 } 259 break; 260 } 261 262 map[x0][y0] = 1;//將老鼠剛走過的位置還原成1 263 264 } 265 else 266 { 267 escape[s_top][0] = x; 268 escape[s_top][1] = y; 269 escape[s_top][2] = direction_num; 270 271 Escape_judge = 1; 272 s_top++; 273 274 switch (Way_judge) 275 { 276 case 0: 277 { 278 x = x - 1; 279 direction_num = T_NORTH; 280 } 281 break; 282 case 1: 283 { 284 y = y - 1; 285 direction_num = T_WEST; 286 } 287 break; 288 case 2: 289 { 290 x = x + 1; 291 direction_num = T_SOUTH; 292 } 293 break; 294 case 3: 295 { 296 y = y + 1; 297 direction_num = T_EAST; 298 } 299 break; 300 } 301 302 map[x0][y0] = 1;//將老鼠剛走過的位置還原成1 303 304 } 305 } 306 else 307 { 308 if (cx == escape[s_top - 1][0] && cy == escape[s_top - 1][1]) 309 { 310 escape[s_top][0] = x; 311 escape[s_top][1] = y; 312 escape[s_top][2] = direction_num; 313 314 Escape_judge = 1; 315 s_top++; 316 317 switch (Way_judge) 318 { 319 case 0: 320 { 321 x = x - 1; 322 direction_num = T_NORTH; 323 } 324 break; 325 case 1: 326 { 327 y = y - 1; 328 direction_num = T_WEST; 329 } 330 break; 331 case 2: 332 { 333 x = x + 1; 334 direction_num = T_SOUTH; 335 } 336 break; 337 case 3: 338 { 339 y = y + 1; 340 direction_num = T_EAST; 341 } 342 break; 343 } 344 345 map[x0][y0] = 1;//將老鼠剛走過的位置還原成1 346 347 } 348 else 349 { 350 map[x][y] = 1; 351 352 s_top--; 353 x = escape[s_top][0]; 354 y = escape[s_top][1]; 355 direction_num = escape[s_top][2]; 356 357 if (0 == s_top) 358 Escape_judge = 0; 359 360 } 361 } 362 Display_Map(); 363 cout << endl; 364 } 365 } 366 367 int main() 368 { 369 Do_it(); 370 return 0; 371 }
關於數學,關於計算機,還有太多值得自己去學習,希望以后可以在這里記錄下來。
如有錯誤或者建議,請盡情指教!
大三上
2016/10/19凌晨