1. 程式人生 > >【探索-中級演算法】單詞搜尋

【探索-中級演算法】單詞搜尋

在這裡插入圖片描述

解法一 遞迴

結合回溯與深搜,因同一單元格不能被重複使用,因此藉助一個輔助陣列用於記錄單元格是否被訪問過。

需要剪枝的策略:

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 的處理。