單詞搜尋問題

2022-05-28 21:06:37

作者:Grey

原文地址:單詞搜尋問題

題目連結

LeetCode 212. 單詞搜尋 II

思路

總體思路是:列舉從board的每個位置開始,看能走出哪些單詞表中的單詞,虛擬碼如下:

for (int i = 0; i < board.length;i++) {
     for (int j = 0; j < board[0].length;j++) {
          int size = process(i,j, board, words);
          if (size == words.size) {
             return new ArrayList<>(words);
          }
     }
 }

遞迴函數process 表示從board[i][j]出發,能走出哪些單詞表中的單詞。返回值是能走出的單詞數量是多少,如果返回值正好等於單詞表的數量,不需要繼續嘗試了,直接返回可以走出所有單詞。

如果要達到上述目的,這個遞迴函數還差哪些引數呢?

首先,我需要一個List<String> ans來儲存所有走出的單詞是哪些;

其次,我需要一個變數List<Character> pre儲存我每次走到的字串是什麼;

最後,我需要一個快速判斷走的是不是無效路徑的資料結構,因為如果我沒有這個資料結構,我每走一步都需要暴力列舉我走出的pre是不是在單詞表中。例如,假設單詞表為:

[apple, banana]

假設一個3 x 5的board為:

['a','p','p','l','e']
['a','x','y','b','a']
['b','a','n','a','n']

如果我即將走的下一個字元是第二行第二列的x字元,這個資料結構可以快速幫我過濾掉這種情況,沒必要從x字元繼續往下走了。

這個資料結構就是字首樹,通過字首樹,可以很快找到某個字串是否是一個單詞的字首,同時,也可以很快得出某個字串是否已經完成了匹配。

完善後的遞迴函數完整簽名如下:

// 從board的i,j位置出發,
// 走過的路徑儲存在pre中,
// 收集到的單詞表中的單詞儲存在ans中
// trie就是單詞表建立的字首樹
int process(int i, int j, LinkedList<Character> pre, List<String> ans, char[][] board, Trie trie)

在整個遞迴呼叫之前,我們需要最先構造字首樹,字首樹的定義如下:

    public static class Trie {
        public Trie[] next;
        public int pass;
        public boolean end;
        public Trie() {
          // 由於只有26個小寫字母,所以只需要準備26大小的陣列即可。
            next = new Trie[26];
          // 遍歷過的字元次數
            pass = 0;
          // 是否是一個字串的結尾
            end = false;
        }
    }

針對單詞表,我們建立字首樹,過程如下:

        Set<String> set = new HashSet<>();
        Trie trie = new Trie();
        for (String word : words) {
            if (!set.contains(word)){
                set.add(word);
                buildTrie(trie,word);
            }
        }

之所以要定義Set,是因為想把單詞表去重,buildTrie的完整程式碼如下,以下為字首樹建立的經典程式碼,有路則複用,無路則建立,迴圈結束後,將end設定為true,表示這個單詞的結束標誌:

    private static void buildTrie(Trie trie, String word) {
        char[] str = word.toCharArray();
        for (char c : str) {
            if (trie.next[c - 'a'] == null) {
                trie.next[c - 'a'] = new Trie();
            }
            trie = trie.next[c - 'a'];
            trie.pass++;
        }
        trie.end = true;
    }

任何一個字元x,如果:

trie.next[x - 'a'] == null || trie.next[x - 'a'].pass == 0;

則表示沒有下一個方向上的路,或者下一個方向上的字元已經用過了,這種情況下,就直接可以無需繼續從這個字元開始嘗試。

到了某個字元,如果:

trie.end = true

表示這個字元已經是滿足條件的某個單詞的結尾了,可以開始收集答案。

字首樹準備好了以後,就可以考慮遞迴函數的base case了,

    public static int process(int i, int j, LinkedList<Character> pre, List<String> ans, char[][] board, Trie trie){
        if (i >= board.length || i < 0 || j >= board[0].length || j < 0) {
            return 0;
        } 
        if (board[i][j] == '0') {
            // 不走回頭路
            return 0;
        }
        if (trie.next[board[i][j] - 'a'] == null || trie.next[board[i][j] - 'a'].pass == 0) {
            // 沒有路可以走
            return 0;
        }
        ...
    }

第一個if表示越界,顯然返回0,因為你的決策已經讓i,j越界了,決策錯了,返回0沒毛病。

第三個if表示的情況,就是前面說的,字首樹判斷當前位置已經沒有繼續嘗試的必要了,返回0也沒毛病。

由於題目要求不能走回頭路,所以我將走過的位置上的字元修改為字元0,標識我走過這裡了,所以第二個if表示:如果我們決策到某個位置是0,說明我們走了回頭路,返回0也沒毛病。

如果順利通過了上述三個if,那麼說明當前決策的位置有利可圖,說不定就可以走出單詞表中的單詞,所以把當前位置的字元加入pre,表示我已經選擇了當前字元,請去上下左右四個方向幫我收集答案,程式碼如下:

pre.addLast(c);
trie = trie.next[index];
int fix = 0;
if(trie.end) {
    ans.add(buildString(pre));
    trie.end=false;
    fix++;
}
// 這句表示:先標識一下當前位置為0字元,表示我已經走過了
board[i][j] = '0';
// 以下四行表示:
// 請去上,下,左,右四個方向幫我收集答案吧。
fix +=process(i+1,j,pre,ans,board,trie);
fix+=process(i,j+1,pre,ans,board,trie);
fix+=process(i-1,j,pre,ans,board,trie);
fix+=process(i,j-1,pre,ans,board,trie);
// 深度優先遍歷的恢復現場操作。
board[i][j] = c;
pre.pollLast();
trie.pass-=fix;

其中if(trie.end)說明已經走出了一個符合條件的單詞,可以收集答案了。buildString(pre)就是把之前收集的字元拼接成一個字串,代表已經拼湊出來的那個單詞:

    private static String buildString(LinkedList<Character> pre) {
        LinkedList<Character> preCopy = new LinkedList<>(pre);
        StringBuilder sb = new StringBuilder();
        while (!preCopy.isEmpty()) {
            Character c = preCopy.pollFirst();
            sb.append(c);
        }
        return sb.toString();

    }

完整程式碼

public class LeetCode_0212_WordSearchII {
   public static class Trie {
       public Trie[] next;
       public int pass;
       public boolean end;
       public Trie() {
           next = new Trie[26];
           pass = 0;
           end = false;
       }
   }


   public static List<String> findWords(char[][] board, String[] words){
       Set<String> set = new HashSet<>();
       Trie trie = new Trie();
       for (String word : words) {
           if (!set.contains(word)){
               set.add(word);
               buildTrie(trie,word);
           }
       }
       LinkedList<Character> pre= new LinkedList<>();
       List<String> ans = new ArrayList<>();
       for (int i = 0; i < board.length;i++) {
           for (int j = 0; j < board[0].length;j++) {
               int times = process(i,j,pre,ans,board,trie);
               if (times == set.size()) {
                   return new ArrayList<>(set);
               }
           }
       }
       return ans;
   }



   public static int process(int i, int j, LinkedList<Character> pre, List<String> ans, char[][] board, Trie trie){
       if (i >= board.length || i < 0 || j >= board[0].length || j < 0) {
           return 0;
       }
       char c = board[i][j];
       if (c == '0') {
           // 不走回頭路
           return 0;
       }
       int index= c - 'a';
       if (trie.next[index] == null || trie.next[index].pass == 0) {
           // 沒有路可以走
           return 0;
       }
       pre.addLast(c);
       trie = trie.next[index];

       int fix = 0;
       if(trie.end) {
           ans.add(buildString(pre));
           trie.end=false;
           fix++;
       }
       board[i][j] = '0';
       fix +=process(i+1,j,pre,ans,board,trie);
       fix+=process(i,j+1,pre,ans,board,trie);
       fix+=process(i-1,j,pre,ans,board,trie);
       fix+=process(i,j-1,pre,ans,board,trie);
       board[i][j] = c;
       pre.pollLast();
       trie.pass-=fix;
       return fix;
   }

   private static String buildString(LinkedList<Character> pre) {
       LinkedList<Character> preCopy = new LinkedList<>(pre);
       StringBuilder sb = new StringBuilder();
       while (!preCopy.isEmpty()) {
           Character c = preCopy.pollFirst();
           sb.append(c);
       }
       return sb.toString();

   }

   private static void buildTrie(Trie trie, String word) {
       char[] str = word.toCharArray();
       for (char c : str) {
           if (trie.next[c - 'a'] == null) {
               trie.next[c - 'a'] = new Trie();
           }
           trie = trie.next[c - 'a'];
           trie.pass++;
       }
       trie.end = true;
   }
}

更多

演演算法和資料結構筆記