作者:Grey
原文地址:單詞搜尋問題
總體思路是:列舉從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;
}
}