迷宮類的廣度優先搜索與樹的層次遍歷類似但是又有不同,比如樹的層次遍歷不需要判重即不需要標記,樹也不基本需要判斷是否可以入列(即下一節點是否合法);而圖需要標記進行判重,並且入列時還需要考慮狀態是否合法,合法才入列。
目錄
1.本文例子的迷宮如下:
0 |
0 |
1 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
0 |
這裏介紹一種極簡DP,可以記錄迷宮上的每個點到起點的最短距離,有時候題目變形,只需要對DP操作即可,這種同時也可以將DP用於判重的記錄表;
如果求最短路徑不要求記錄路徑的話,則隊列節點中不需要多餘的指針;這裏加上只要碰到需要求最短的路徑並打印都可以使用。要求打印所有路徑目前還沒想好怎麼做,對於這句話不是很理解:“如果是求所有路徑,注意此時,狀態轉換圖是DAG,即允許兩個父節點指向同一個子節點。具體實現時,每個節點要“延遲”加入到已訪問集合 visited ,要等一層全部訪問完後,再加入到 visited 集合。”目前的想法是如果是所有路徑,考慮深度優先實現
2.幫助理解代碼:
(1)方向表示:把地圖左上角當座標原點,橫着的是y增長方向,豎的是x增長方向,最後一行的1表示(3,2)及3行2列
,那麼方向左就是“y軸”減1,即列減1,(0,-1)表示左,(0,1)表示右
/*方向的模擬,座標原點在左上方,向左的話其實是列減1*/
static const int s_dirs[MAX_DIR][DIM] = {{0,-1},{0,1},{-1,0},{1,0}};//左,右,上,下
(2)DP數組:本文的DP數組其實就是地圖的矩陣,值爲-1的點表示節點未遍歷,非-1的值表示節點已經遍歷,並且是該點到起點的最短距離,如dp[2][2]=3,表示2行2列的點到起點的距離是3
(3)路徑打印:用到遞歸,當然也可以用棧,但是比較麻煩,又需要實現,雖然不難
//**********遞歸打印***********
void print_path(POINT_T* point)
{
if(point->pre == NULL)
{
printf("(%d,%d)",point->x,point->y);//爲了把第一個打出來
return;
}
else
{
print_path(point->pre);
printf("->(%d,%d)",point->x,point->y);
}
(4)二維數組的訪問
對於直接“棧上變量二維數組”,通過參數傳進來,對元素的訪問一般轉爲一維數組的形式訪問。
a.調用者A定義
int maze[MAZE_MAX_X][MAZE_MAX_Y] = {
{0,0,1,0},
{0,0,0,0},
{0,0,1,0},
{0,0,1,0},
};
B(maze,max_x,max_y);
b. B中的訪問maze的元素則用一維的
int B(int** maza,int max_x,int max_y)
{
//訪問maze[i][j]
*(((int*)maze)+max_y*i+j) = 6;
}
當然如下兩種maze的定義則不需要,傳參後仍然可以maze[i][j].
a.一維度和二維度都是堆上的
int **maze;
maze = (int**)malloc(sizeof(int*)*4);
for(i = 0;i < 4;i++)
{
maze[i] = (int*)malloc(sizeof(int)*4);
memset(maze[i],0,sizeof(int)*4);
}
- 一維是棧上的,二維度是堆上的
int *maze[4]={0};
for(i = 0;i < 4;i++)
{
maze[i] - malloc(sizeof(int)*4);
memset(maze[i],0,sizeof(int)*4)
}
3.BFS的一般步驟
1) 查找給定的結點s可以達到的點,並加入廣度優先搜索樹中(以隊列實現,下稱隊列)同時標記爲已訪問;
2) 遍歷隊列,判斷隊列中的結點是否滿足給定的條件或要求,滿足則結束搜索;若不滿足結束條件,對隊列中的每個結點繼續查找其可以達到並且未使用過的結點,加入隊列中,並記錄爲已使用。
3)重複2。
注意:第二步中的記錄已訪問需要根據實際情況選擇是每個加入後立馬記錄(這樣的話,同級的兄弟結點也不能重複使用),還是同級的全部完成後再記錄已訪問(這樣同級的兄弟結點允許重複使用)。
4.本文的隊列結構
隊列節點相當於實現爲鏈式的可以找到其前面一個座標點
typedef struct Node_
{
int x;
int y;
//前驅節點,爲了能打印出路徑(這利用到遞歸打印了),因爲入隊列的不一定都是有用的
struct Node_ *pre;
}POINT_T;
typedef struct Que_
{
int size;
int cap;
int head;
int tail;
//隊列中這個搞成數組的形式
POINT_T* path;
}QUE_LINE;
5.廣度優先搜索核心
int BfsMAZE_ShortPath_WithPathPrint(int** maze,int max_x,int max_y,int *start,int *end,QUE_LINE* que,int **dp)
{
POINT_T point = {0};
POINT_T next = {0};
POINT_T* cur = NULL;
int i = 0;
point.x = start[0];
point.y = start[1];
point.pre = NULL;
//起點入隊列並標記
Queue_Push(que,&point);
*(((int*)dp)+point.x * max_y + point.y) = 0;
//printf("que.size = %d,que->cap = %d,que->head = %d,que->tail = %d\n",que->size,que->cap,que->head,que->tail);
while(Queue_Is_Empty(que) != 1)
{
cur = Queue_Pop(que);
//printf("cur:(%d,%d)\n",cur->x,cur->y);
//嘗試四個方向,如果下一個狀態合法,則入隊列
for(i = 0; i <= MAX_DIR;i++)
{
next.x = cur->x + s_dirs[i][0];
next.y = cur->y + s_dirs[i][1];
next.pre = cur;
//如果是邊界,狀態不合法,不能入隊
if(Point_Is_Boundry(&next,max_x,max_y) == 1)
{
printf("(%d,%d) is bondry\n",next.x,next.y);
continue;
}
//如果是牆,也不合法,不能入隊
if(*(((int*)maze)+next.x * max_y + next.y) == 1)
{
//printf("*(((int*)maze)+max_y * next[0] + next[1]) = %d,maze is 1\n",*(((int*)maze)+max_y * next.x + next.y));
continue;
}
//如果節點已經遍歷過了,也不能入隊
if(*(((int*)dp)+next.x * max_y + next.y) != -1)
{
//printf("*(((int*)dp)+max_x * next[0] + next[1]) = %d,dp is 1\n", *(((int*)dp)+max_y * next.x + next.y));
continue;
}
//如果找到了重點,停止搜索,並打印路徑;如果沒有要求打印路徑的話把return去掉可以把//DP給記錄完全,及把地圖上所有點到起點的路徑都給記下來
if(next.x == end[0] && next.y == end[1])
{
print_path(&next);
return *(((int*)dp)+cur->x * max_y + cur->y)+1;
}
//printf("now (%d,%d) is push queue\n",next.x,next.y);
Queue_Push(que,&next);
*(((int*)dp)+next.x * max_y + next.y) = *(((int*)dp)+cur->x * max_y + cur->y) + 1;
}
}
return 0;
}
6.所有代碼
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
/*
BFS一般三步走即可:
1) 查找給定的結點s可以達到的點,並加入廣度優先搜索樹中(以隊列實現,下稱隊列)同時標記爲已訪問;
2) 遍歷隊列,判斷隊列中的結點是否滿足給定的條件或要求,滿足則結束搜索;若不滿足結束條件,對隊列中的每個結點繼續查找其可以達到並且未使用過的結點,加入隊列中,並記錄爲已使用。
3)重複2。
注意:第二步中的記錄已訪問需要根據實際情況選擇是每個加入後立馬記錄(這樣的話,同級的兄弟結點也不能重複使用),還是同級的全部完成後再記錄已訪問(這樣同級的兄弟結點允許重複使用)。
*/
/*
1. 是求路徑長度,還是路徑本身(或動作序列)?
i. 如果是求路徑長度,則狀態裏面要存路徑長度(或雙隊列+一個全局變量)
ii. 如果是求路徑本身或動作序列
i. 要用一棵樹存儲寬搜過程中的路徑
ii. 是否可以預估狀態個數的上限?能夠預估狀態總數,則開一個大數組,用樹的雙親表示法;如
果不能預估狀態總數,則要使用一棵通用的樹。這一步也是第4步的必要不充分條件。
2. 如何表示狀態?即一個狀態需要存儲哪些些必要的數據,才能夠完整提供如何擴展到下一步狀態的所
有信息。一般記錄當前位置或整體局面。
3. 如何擴展狀態?這一步跟第2步相關。狀態裏記錄的數據不同,擴展方法就不同。對於固定不變的數據
結構(一般題目直接給出,作爲輸入數據),如二叉樹,圖等,擴展方法很簡單,直接往下一層走,
對於隱式圖,要先在第1步裏想清楚狀態所帶的數據,想清楚了這點,那如何擴展就很簡單了。
4. 如何判斷重複?如果狀態轉換圖是一顆樹,則永遠不會出現迴路,不需要判重;如果狀態轉換圖是一
個圖(這時候是一個圖上的BFS),則需要判重。
i. 如果是求最短路徑長度或一條路徑,則只需要讓“點”(即狀態)不重複出現,即可保證不出現迴路
ii. 如果是求所有路徑,注意此時,狀態轉換圖是DAG,即允許兩個父節點指向同一個子節點。具體
實現時,每個節點要“延遲”加入到已訪問集合 visited ,要等一層全部訪問完後,再加入
到 visited 集合。
iii. 具體實現
i. 狀態是否存在完美哈希方案?即將狀態一一映射到整數,互相之間不會衝突。
ii. 如果不存在,則需要使用通用的哈希表(自己實現或用標準庫,例如 unordered_set )來判
重;自己實現哈希表的話,如果能夠預估狀態個數的上限,則可以開兩個數組,head和
next,表示哈希表,參考第 ??? 節方案2。
iii. 如果存在,則可以開一個大布爾數組,來判重,且此時可以精確計算出狀態總數,而不僅僅是
預估上限。
5. 目標狀態是否已知?如果題目已經給出了目標狀態,可以帶來很大便利,這時候可以從起始狀態出
發,正向廣搜;也可以從目標狀態出發,逆向廣搜;也可以同時出發,雙向廣搜。
代碼模板
總結
288
廣搜需要一個隊列,用於一層一層擴展,一個hashset,用於判重,一棵樹(只求長度時不需要),用於存
儲整棵樹。
對於隊列,可以用 queue ,也可以把 vector 當做隊列使用。當求長度時,有兩種做法:
1. 只用一個隊列,但在狀態結構體 state_t 裏增加一個整數字段 level ,表示當前所在的層次,當碰
到目標狀態,直接輸出 level 即可。這個方案,可以很容易的變成A*算法,把 queue 替換
爲 priority_queue 即可。
2. 用兩個隊列, current, next ,分別表示當前層次和下一層,另設一個全局整數 level ,表示層數
(也即路徑長度),當碰到目標狀態,輸出 level 即可。這個方案,狀態裏可以不存路徑長度,只需
全局設置一個整數 level ,比較節省內存;
對於hashset,如果有完美哈希方案,用布爾數組( bool visited[STATE_MAX] 或 vector<bool>
visited(STATE_MAX, false) )來表示;如果沒有,可以用STL裏的 set 或 unordered_set 。
對於樹,如果用STL,可以用 unordered_map<state_t, state_t > father 表示一顆樹,代碼非常簡
潔。如果能夠預估狀態總數的上限(設爲STATE_MAX),可以用數組( state_t nodes[STATE_MAX] ),
即樹的雙親表示法來表示樹,效率更高,當然,需要寫更多代碼。
*/
/*
極簡動態規劃配合BFS求解P2P最短路徑,dp數組的元素有效時表示起點到該點的最短距離,也即表示訪問過了,同樣需要一個隊列,這裏實現爲線性隊列
*/
/*隊列實現*/
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
typedef struct Node_
{
int x;
int y;
//前驅節點,爲了能打印出路徑(這利用到遞歸打印了),因爲入隊列的不一定都是有用的
struct Node_ *pre;
}POINT_T;
typedef struct Que_
{
int size;
int cap;
int head;
int tail;
//隊列中這個搞成數組的形式
POINT_T* path;
}QUE_LINE;
void Queue_Reinit(QUE_LINE* que)
{
que->size = 0;
que->head = 0;
que->tail = 0;
}
void Queue_Init(QUE_LINE* que,int cap)
{
que->cap = cap;
que->path = (POINT_T*)malloc(sizeof(POINT_T)*cap);
memset(que->path,0,sizeof(POINT_T)*cap);
Queue_Reinit(que);
}
int Queue_Is_Empty(QUE_LINE *que)
{
return (que->size == 0) ? 1 :0;
}
int Queue_Is_Full(QUE_LINE *que)
{
return (que->size >= que->cap) ? 1 :0;
}
int Queue_Push(QUE_LINE *que,POINT_T* point)
{
if(Queue_Is_Full(que) != 1)
{
que->size++;
memcpy((POINT_T*)&que->path[que->tail],(POINT_T*)point,sizeof(POINT_T));
que->tail = (++que->tail)%que->cap;
return 1;
}
return 0;
}
POINT_T* Queue_Pop(QUE_LINE* que)
{
int res_index = que->head;
if(Queue_Is_Empty(que) != 1)
{
que->size--;
que->head = (++que->head)%que->cap;
return &que->path[res_index];
}
return NULL;
}
void Queue_Destroy(QUE_LINE* que)
{
int i = 0;
for(i = 0; i <= que->size - 1; i++)
{
free(&que->path[i]);
}
}
//**********遞歸打印***********
void print_path(POINT_T* point)
{
if(point->pre == NULL)
{
printf("(%d,%d)",point->x,point->y);//爲了把第一個打出來
return;
}
else
{
print_path(point->pre);
printf("->(%d,%d)",point->x,point->y);
}
}
#define MAX_DIR 4
#define DIM 2
/*方向的模擬,座標原點在左上方,向左的話其實是列減1*/
static const int s_dirs[MAX_DIR][DIM] = {{0,-1},{0,1},{-1,0},{1,0}};//左,右,上,下
int Point_Is_Boundry(POINT_T* point,int max_x,int max_y)
{
if(point->x < 0 || point->x >= max_x || point->y < 0 || point->y >= max_y)
return 1;
return 0;
}
int BfsMAZE_ShortPath_WithPathPrint(int** maze,int max_x,int max_y,int *start,int *end,QUE_LINE* que,int **dp)
{
POINT_T point = {0};
POINT_T next = {0};
POINT_T* cur = NULL;
int i = 0;
point.x = start[0];
point.y = start[1];
point.pre = NULL;
//入隊列並標記
Queue_Push(que,&point);
*(((int*)dp)+point.x * max_y + point.y) = 0;
//printf("que.size = %d,que->cap = %d,que->head = %d,que->tail = %d\n",que->size,que->cap,que->head,que->tail);
while(Queue_Is_Empty(que) != 1)
{
cur = Queue_Pop(que);
//printf("cur:(%d,%d)\n",cur->x,cur->y);
//嘗試四個方向
for(i = 0; i <= MAX_DIR;i++)
{
next.x = cur->x + s_dirs[i][0];
next.y = cur->y + s_dirs[i][1];
next.pre = cur;
//如果是邊界,狀態不合法,不能入隊
if(Point_Is_Boundry(&next,max_x,max_y) == 1)
{
//printf("(%d,%d) is bondry\n",next.x,next.y);
continue;
}
//如果是牆,也不合法,不能入隊
if(*(((int*)maze)+next.x * max_y + next.y) == 1)
{
//printf("*(((int*)maze)+max_y * next[0] + next[1]) = %d,maze is 1\n",*(((int*)maze)+max_y * next.x + next.y));
continue;
}
//如果節點已經遍歷過了,也不能入隊
if(*(((int*)dp)+next.x * max_y + next.y) != -1)
{
//printf("*(((int*)dp)+max_x * next[0] + next[1]) = %d,dp is 1\n", *(((int*)dp)+max_y * next.x + next.y));
continue;
}
//如果找到了重點,停止搜索,並打印路徑;
if(next.x == end[0] && next.y == end[1])
{
print_path(&next);
return *(((int*)dp)+cur->x * max_y + cur->y)+1;
}
//printf("now (%d,%d) is push queue\n",next.x,next.y);
Queue_Push(que,&next);
*(((int*)dp)+next.x * max_y + next.y) = *(((int*)dp)+cur->x * max_y + cur->y) + 1;
}
}
return 0;
}
#define MAZE_MAX_X 4
#define MAZE_MAX_Y 4
int main()
{
int len = 0, i =0,j = 0;
int dp[MAZE_MAX_X][MAZE_MAX_Y] = {0};
int maze[MAZE_MAX_X][MAZE_MAX_Y] = {
{0,0,1,0},
{0,0,0,0},
{0,0,1,0},
{0,0,1,0},
};
//起點、終點,即座標
int start[2] = {0,1};
int end[2] = {1,3};
QUE_LINE que = {0};
Queue_Init(&que,100);
//注意是搞成-1
memset((int*)dp,-1,sizeof(int)*MAZE_MAX_X*MAZE_MAX_Y);
len = BfsMAZE_ShortPath_WithPathPrint((int**)maze,MAZE_MAX_X,MAZE_MAX_Y,start,end,&que,(int**)dp);
printf("len = %d\n",len);
for(i = 0; i <= MAZE_MAX_X - 1;i ++ )
{
for(j = 0; j <= MAZE_MAX_X - 1;j++)
{
printf("%d ",dp[i][j]);
}
printf("\n");
}
return 0;
}
最短的一條帶路徑打印:https://pan.baidu.com/s/1x5od2I-s9WkaQTynWAcbXw
最短的路徑不帶打印:https://pan.baidu.com/s/1NfES9yTyHuvVsd2FaWq6xw
7.結果
本例子的起點是(0,1),中點事(1,3)見代碼定義