boggle game

可以先去看下: Word Search II. 这题的不同之处在于我们要找到一个在矩阵里不重叠的词的集合

题目

给定一个2D矩阵包括 a-z 和字典 dict,找到矩阵上最大的单词集合,这些单词不能在相同的位置重叠。返回最大集合的 大小

可能还有种变体是让求包含最大的单词集合的Path, 这个之后再说

思路
思路1: 暴力的易懂解法
  1. 先用word search II求出可能的所有单词(允许重叠)
  2. 去重叠, 用combination的写法求出不重叠的最大集合

复杂度: K: dict里的单词数, L: 单词的平均长度, m,n: board的长宽
第一步: O(K’‘L + m’‘n’‘4^L)
K’
‘L是建Trie, mn是外层循环, 4^L是搜索用时, 因为上下左右四种选择, L是搜索树深度
第二步: O(L’*’2^K)
2^K是普通Combination耗时, L是每次递归都要mark路径为blocked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
public class  {
public static void main(String[] args) {
String[] input =
{
"aple",
"ppig",
"plex",
"agel"
};
String[] dict = {"apple", "plex", "pig", "page", "age"};

String[] input2 = {"aaaa","aaaa","aaaa"};
String[] dict2 = {"aaa", "aa", "a", "aaaa", "aaaaa", "aaaaaa"};

Solution sol = new Solution();
List<String> res = sol.boggle(dict2, sol.stringToBoard(input2));
System.out.println("can fit " + res.size() + " words");
for (String w : res) {
System.out.print(w + ", ");
}
}

private char[][] board;
private Trie trie;
int m;
int n;
int[] dx = {0, -1, 1, 0};
int[] dy = {1, 0, 0, -1};
private int maxWords = -1;
private List<Path> res = new ArrayList<>();
public List<String> boggle (String[] dict, char[][] board) {

List<Path> paths = findWords(dict, board);
combination(new ArrayList<>(), paths, new HashSet<>(), 0);
List<String> wordList = new ArrayList<>();
for (Path path : res) {
wordList.add(path.word);
}
return wordList;
}

public void combination(List<Path> currPathList, List<Path> paths,
Set<Integer> blockedPoints, int startIdx) {
// 注意这里像subset, 不是只有startIdx == paths.size时候才加入答案
if (currPathList.size() > maxWords) {
maxWords = currPathList.size();
res.clear();
res.addAll(currPathList);
}

if (startIdx == paths.size()) {
return;
}

for (int i = startIdx; i < paths.size(); i++) {
//注意这里用个bool判断可以继续再往blockedPoints里面放,不然会污染
boolean blocked = false;
List<Integer> candPath = paths.get(i).path;
for (Integer point : candPath) {
if (blockedPoints.contains(point)) {
blocked = true;
}
}
if (blocked) continue;
if (!blocked) blockedPoints.addAll(candPath);
currPathList.add(paths.get(i));
combination(currPathList, paths, blockedPoints, i + 1);
blockedPoints.removeAll(candPath);
currPathList.remove(currPathList.size() - 1);
}
}

public List<Path> findWords(String[] dict, char[][] board) {
buildTrie(dict);
m = board.length;
n = board[0].length;
this.board = board;

List<Path> res = new ArrayList<>();

for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
findNeighborWords(new ArrayList<>(), res, i, j, trie.root);
}
}

return res;
}

public void findNeighborWords(List<Integer> path,
List<Path> wordPath,
int x, int y, TrieNode node) {
if (x < 0 || x >= m || y < 0 || y >= n
|| board[x][y] == '#' || !node.children.containsKey(board[x][y])) {
return;
}
char c = board[x][y];
path.add(x * n + y);
board[x][y] = '#';
node = node.children.get(c);//错点,board[x][y]已改
if (node.word != null) {
wordPath.add(new Path(node.word, new ArrayList<>(path)));
}

for (int i = 0; i < 4; i++) {
int xx = x + dx[i];
int yy = y + dy[i];
findNeighborWords(path, wordPath, xx, yy, node);
}

path.remove(path.size() - 1);
board[x][y] = c;
}


class Path {
List<Integer> path;
String word;

public Path(String word, List<Integer> path) {
this.path = path;
this.word = word;
}

public boolean intersect (Path that) {
for (int i : this.path) {
if (that.path.contains(i)) {
return true;
}
}
return false;
}
}

public void buildTrie(String[] dict) {
trie = new Trie();
for (String w : dict) {
trie.addWord(w);
}
}

class Trie {
TrieNode root = new TrieNode();

public void addWord(String word) {
TrieNode curr = root;
for (char c : word.toCharArray()) {
curr.children.putIfAbsent(c, new TrieNode());
curr = curr.children.get(c);
}
curr.word = word;
}
}

class TrieNode {
String word = null;
Map<Character, TrieNode> children = new HashMap<>();
}

public char[][] stringToBoard(String[] input) {
int m = input.length;
int n = input[0].length();
char[][] res = new char[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
res[i][j] = input[i].charAt(j);
}
}
return res;
}
}
思路2: 高级的双层嵌套搜索

核心思想是:

  1. 对于每个cell, 求出以这个cell为起点的所有单词
  2. 将上一步求出的这些单词一个一个试着放进board, 对于每个单词

    a. 把这个单词的路径标记为已访问
    b..递归求在这个单词已访问的平行世界里的能fit进board的最大集合

网上大神的思路解析: https://jhonwillbao.github.io/2016/11/17/airbnb-boggle-game.html

  1. 还是用一个 Trie 来加速 Word 的查找
  2. 第一个循环,遍历 board 上每一个点,然后从这里找第一个单词(因为第一个单词的选择会影响最终单词数量),开始第一个递归。
  3. 第一个递归的作用是,从当前点开始,通过第二个递归拿到当前点可行的每一个单词。挨个放入,每放入一个更新当前 board 的使用情况,然后开始下一层搜索。
  4. 第二个递归的作用是,从当前点开始,找所有可行的单词 indexes,为第一个递归提供选择
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import java.util.*;

/**
* 注意: 如果字典里的词能重复放的话,主函数(findMostWords)要用list而不是set
* 如果字典里的词只能放一次,主函数要用set而不是list
*/

public class BoggleGame {
public static char[][] stringArrToBoard (String[] input) {
int rows = input.length;
int cols = input[0].length();
char[][] board = new char[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
board[i][j] = input[i].charAt(j);
}
}
return board;
}

public static void main(String[] args) {
String[] input = {"aaaa", "aaaa", "aaaa"};
//String[] dict = {"aaa", "aa", "a", "aaaa", "aaaaa", "aaaaaa"};
String[] dict = {"aa", "a", "aaaa", "aaaaa", "aaaaaa"};

// String[] input =
// {
// "aple",
// "ppig",
// "plex",
// "agel"
// };
// String[] dict = {"apple", "plex", "pig", "page", "age"};
BoggleGame sol = new BoggleGame();
sol.boggle(stringArrToBoard(input), dict);
}

class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isWord;
}

class Trie {
TrieNode root;

public Trie (TrieNode root) {
this.root = root;
}

public void addWord(String word) {
TrieNode curr = root;
for (char c : word.toCharArray()) {
if (curr.children[c - 'a'] == null) {
curr.children[c - 'a'] = new TrieNode();
}
curr = curr.children[c - 'a'];
}
curr.isWord = true;
}
}
static int[] dx = {1, -1, 0 , 0};
static int[] dy = {0, 0, 1, -1};
private char[][] board;
private String[] dict;
private boolean[][] visited;
private Trie trie;

public int boggle (char[][] board, String[] dict) {
this.board = board;
this.dict = dict;
buildTrie();
visited = new boolean[board.length][board[0].length];
List<String> res = new ArrayList<>();
findMostWords(res, new ArrayList<>(), 0, 0, trie.root);

System.out.println("can fit: " + res.size() + " words");
System.out.println("Words: ");
for (String str : res) {
System.out.print(str + ", ");
}
return res.size();
}

public void findMostWords (List<String> res, List<String> curWords, int x, int y, TrieNode curNode) {
int m = board.length;
int n = board[0].length;
// 遍历整个board, 以每个cell为起点找各自的最大集合
for (int i = x; i < m; i++) {
for (int j = y; j < n; j++) {
List<List<Integer>> nextWords = new ArrayList<>();
List<Integer> path = new ArrayList<>();
// 相当于word search II, 找到以这个点为起点能找到所有words
findNextWords(nextWords, path, i, j, curNode);
// 把每个词放入都试下
for (List<Integer> nextWordIdx : nextWords) {
String nextWord = "";
// 把这个词的路径标记已访问
for (int idx : nextWordIdx) {
int row = idx / n;
int col = idx % n;
visited[row][col] = true;
nextWord += board[row][col];
}
// 把这个词放入集合
curWords.add(nextWord);
if (curWords.size() > res.size()) {
res.clear();
res.addAll(curWords);
}

// 以此点为起点找这个平行世界的最大集合
findMostWords(res, curWords, i, j, curNode);//不要写成x,y!!!!

for (int idx : nextWordIdx) {
int row = idx / n;
int col = idx % n;
visited[row][col] = false;
}
curWords.remove(curWords.size() - 1);
}
}
y = 0;
}
}

// 相当于word search II
public void findNextWords(List<List<Integer>> words, List<Integer> path, int x, int y, TrieNode curNode) {
if (!canGo(x, y) || curNode.children[board[x][y] - 'a'] == null) {
return;
}

char c = board[x][y];
curNode = curNode.children[c - 'a'];

path.add(x * board[0].length + y);
// find a word
if (curNode.isWord) {//这里也要注意,上面换了curNode才能直接用isWord
words.add(new ArrayList<>(path));
// path.remove(path.size () - 1);//注意别漏
// return;//注意这里没有也能解出,面试时候可以不写
}
visited[x][y] = true;
for (int i = 0; i < 4; i++) {
int xx = x + dx[i];
int yy = y + dy[i];
findNextWords(words, path, xx, yy, curNode);
}
visited[x][y] = false;
path.remove(path.size() - 1);//别漏
}

private boolean canGo(int x, int y) {
if (x < 0 || x >= board.length || y < 0 || y >= board[0].length || visited[x][y]) {
return false;
}
return true;
}

private void buildTrie() {
trie = new Trie(new TrieNode());
for (String word : dict) {
trie.addWord(word);
}
}
}

po一个别人的思路 (code已过lintcode的AC)

https://www.1point3acres.com/bbs/thread-465509-1-1.html

看到网上的解法都是用好几个search来嵌套,一是不straight forward,二是比较容易出错,不好debug。下面分享下我的解法: m, n是board的长宽,l是word的平均长度,k是board上面找到word的个数。
step1. 找到所有word, 并记录path。 时间复杂度,O(mnl)
step2. 找到每个word的有相交的邻居。 时间复杂度 O(kkl)
step3. 用combination找word的可行的组合,track最长组合的长度。 时间复杂度 O(2^k)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def boggleGame(self, board, words):
# find word on the board
word_list = self.findWords (board, words)

# assign indexes to word paths
paths = {}
index = 0
for k, v in word_list.items ():
for _ in v:
paths[index] = set (_)
index += 1

# create adjacent list of paths
adj = collections.defaultdict (set)
for i in paths:
for j in paths:
if i == j:
continue
if paths[i] & paths[j]:
adj[i].add (j)
adj[j].add (i)

# search paths combination
self.max_len = 0
self.combination (len (paths), adj, [], set (), 0)

# return max len of combination
return self.max_len

def combination(self, n, adj, path, removed, start):
if start == n:
self.max_len = max (self.max_len, len (path))
# print(removed, path)
for i in range (start, n):
if i in removed:
continue
path.append (i)
# print(adj[i],'adj')
removed = removed.union (adj[i])
# print(removed,'fjds')
self.combination (n, adj, path, removed, i + 1)
path.pop ()
removed = removed - adj[i]

def findWords(self, board, words):
"""
:type board: List[List[str]]
:type words: List[str]
:rtype: List[str]
"""
trie = self.buildTrie (board, words)-baidu 1point3acres
# print (trie)
rst = collections.defaultdict (list)
for i in range (len (board)):
for j in range (len (board[0])):
self.search (trie, board, i, j, rst, [])

return (rst)

def search(self, trie, board, i, j, rst, path):
# print(i, j)

if i < 0 or j < 0 or i >= len (board) or j >= len (board[0]) or board[i][j] not in trie:
return

c = board[i][j]
trie = trie[board[i][j]]
path.append ((i * len (board[0]) + j))
board[i][j] = '#'
if 'word' in trie:
# print ('yes', trie['word'])
if path[::-1] not in rst[trie['word']]:
rst[trie['word']].append (list (path))
board[i][j] = c
path.pop ()
return

self.search (trie, board, i + 1, j, rst, path)
self.search (trie, board, i - 1, j, rst, path)
self.search (trie, board, i, j + 1, rst, path)
self.search (trie, board, i, j - 1, rst, path)
board[i][j] = c
path.pop ()

def buildTrie(self, board, words):
trie = {}
for w in words:
crt = trie
for c in w:
if c not in crt:
crt[c] = {}
crt = crt[c]
crt['word'] = w
return trie