【探索-中級演算法】單詞搜尋
解法一 遞迴
結合回溯與深搜,因同一單元格不能被重複使用,因此藉助一個輔助陣列用於記錄單元格是否被訪問過。
需要剪枝的策略:
1.當前元素與單詞的對應位置的字母不一致
2.當前元素已經被遍歷過
3.超出了 borad 的邊界
public boolean exist(char[][] board, String word) {
if (board==null||board[0]==null) return false;
if (word==null||word.length()==0) return true;
// 輔助陣列,記錄 map[i][j] 是否被遍歷過
boolean [][] map = new boolean[board.length][board[0].length];
// 迴圈遍歷 board,因為要先找到與 word 第一個字母相同的位置作為起點進行深搜
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (dfs(board, word.toCharArray(), 0, map,i,j)) return true;
}
}
return false;
}
public boolean dfs(char[][] board,char[] wordArr,int index,boolean[][] map,int x,int y) {
if (index==wordArr.length) return true;
if (x>=board.length||x<0||y>=board[0].length||y<0) return false;
if (!map[x][y]&&board[x][y] == wordArr[index]) {
map[ x][y] = true;
boolean result = dfs(board, wordArr, index + 1, map, x + 1, y)
|| dfs(board, wordArr, index + 1, map, x - 1, y)
|| dfs(board, wordArr, index + 1, map, x , y+1)
|| dfs(board, wordArr, index + 1, map, x, y-1);
// 在遞迴遍歷當前元素之後還原被訪問的記錄
map[x][y] = false;
return result;
}
return false;
}
需要注意的是,每次在遞迴遍歷當前元素之後還原被訪問的記錄,如果不這樣做的話,就會影響到其他遞迴路徑。
比如對於
[ ['A','B','C','E']
['S','F','E','S']
['A','D','E','E']
]
與 word = "ABCESEEEFS"
在前述演算法中,當經過 A -> B -> C
,當到達 C
的時候,會面臨兩種選擇,根據演算法中遍歷的先後順序,會先走 E(1,2) -> S(1,3) -> E(2,3) -> E(2,2)
但是這條路徑走到最後明顯不符合要求,因此就要回溯,回溯到 S(1,3)
,然後走 E(0,3)
,也不符合要求,最終會回溯到 C
的位置,此時如果不把 E(1,2) 、S(1,3) 、E(2,3) 、E(2,2)、E(0,3)
這幾個被遍歷過的點的標記重製的話,那麼回溯到 C
再探索其他路徑的時候(即正常情況下需要走 E(0,3)
),就因未清除標記而無法正常進行下去。
解法二 非遞迴
參考自:https://blog.csdn.net/hjh00/article/details/49563319
入棧的內容:[x, y, steps ],其中x, y是當前滿足要求的節點的座標,steps是一個列表,存放與(x,y)的四個方向上的鄰居節點,前提是這些鄰居節點是下一個滿足要求的字母。如果沒有滿足鄰居節點,則steps為空,則出棧;如果不為空,則從steps.pop()一個(tx, ty)出來判斷,如果(tx, ty)有符合要求鄰居節點的則(tx, ty)入棧,否則繼續從steps中取出新的鄰居節點,然後繼續判斷;如果直到堆疊為空,還沒有找到,則返回False,找到返回True。找的過程中必須標記以訪問的點,這些點不能再次訪問,否則重複了。
使用非遞迴的解法時,需要藉助一個堆疊來儲存遍歷的路徑上節點,然後弄清楚什麼時候入棧,什麼時候出棧,什麼時候達到題目的要求。
在解法上,藉助一個數據結構來儲存相關的狀態。
static class Node {
int x, y;
// 用於儲存 board[x][y] 四周符合條件的下一個節點
Stack<Node> surroundingNodes = new Stack<>();
public Node(int x, int y) {
this.x = x;
this.y = y;
}
}
public boolean exist(char[][] board, String word) {
if (board == null || board[0] == null) return false;
if (word == null || word.length() == 0) return true;
char[] wordArr = word.toCharArray();
// 記錄節點是不是被訪問過,防止在記錄某節點周圍的節點時,被重複新增,形成環
// 如 {{A, B, E, E, C}} 與 word = ABEEE 的情況
// 如果不記錄是否被訪問過,那麼在 <E, E> 這裡會反覆處理,影響正常的邏輯
boolean[][] visited = new boolean[board.length][board[0].length];
// 如果 word 的字母元素比 board 的整個元素還多,則直接返回 false
if (wordArr.length>board.length * board[0].length) return false;
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (board[i][j] == wordArr[0]) {
// 當 word 只有一個字母時,直接返回
// 否則進入到迴圈時因為 index == 1 且要索引 word[1] 而陣列越界
if (wordArr.length==1) return true;
Stack<Node> pathNodes = new Stack<>();
Node head = new Node(i, j);
visited[i][j] = true;
int index = 1;
setSurrounding(board, head, wordArr, index,visited);
pathNodes.push(head);
while (!pathNodes.isEmpty()) {
Node cur = pathNodes.peek();
Node next = null;
Stack<Node> curNodeSurrounding = cur.surroundingNodes;
if (!curNodeSurrounding.isEmpty()) {
// 如果 borad[cur.x][cur.y] 的周圍有符合要求的點
// 則新增符合要求的點到儲存路徑的 stack 中
// 且要把該符合要求的點從 borad[cur.x][cur.y] 的
// surroundingNodes 中剔除,以及標記被訪問過
next = curNodeSurrounding.pop();
pathNodes.push(next);
visited[next.x][next.y] = true;
}
// 因為當前 borad[cur.x][cur.y] 的四周沒有一個符合條件的下一級節點
// 則將 cur 彈出,即回溯
if (next == null) {
Node tmp = pathNodes.pop();
// 重置其被訪問狀態
visited[tmp.x][tmp.y] = false;
--index;
} else {
++index;
// 找到了符合要求的路徑,則返回 true
if (index == wordArr.length) return true;
setSurrounding(board, next, wordArr, index,visited);
}
}
}
}
}
return false;
}
public void setSurrounding(char[][] board, Node node, char[] wordArr, int index,boolean[][] visited) {
int x = node.x;
int y = node.y;
// top,如果某節點沒有被訪問過,且符合要求
if (x - 1 >= 0 && !visited[x-1][y] && board[x - 1][y] == wordArr[index]) {
node.surroundingNodes.push(new Node(x - 1, y));
}
// bottom
if (x + 1 < board.length && !visited[x+1][y] && board[x + 1][y] == wordArr[index]) {
node.surroundingNodes.push(new Node(x + 1, y));
}
// left
if (y - 1 >= 0 && !visited[x][y-1] && board[x][y - 1] == wordArr[index]) {
node.surroundingNodes.push(new Node(x, y - 1));
}
// right
if (y + 1 < board[0].length && !visited[x][y+1] && board[x][y + 1] == wordArr[index]) {
node.surroundingNodes.push(new Node(x, y + 1));
}
}
在收集當前節點四周的符合條件的節點時,可能會遇到節點 1 與節點 2 收集了同一個節點的情況,如:
[A, B, E, E]
[E, E, E, E]
[E, E, E, E]
與 word= ABEEEEEE
,當遍歷到 B(0,1)
時,其 surroundingNodes = {E(0,2),E(1,1)}
,接著遍歷 E(0,2)
,然後是 E(1,2)
,而 E(1,2)
的 surroundingNodes = {E(1,1), ...}
,此時 E(1,1)
就被共有了(表明可能會被兩條路徑分別所屬,並進行處理),但是並不會影響正常的邏輯,因為 E(1,1)
每次被訪問之後,雖然就被設定訪問狀態為 true,但是之後如果包含該 E(1,1)
的路徑 path1 走不通時,其訪問狀態就被重置,並不會影響下一次被新的包含該點的路徑 path2 的處理。