解法一 递归
结合回溯与深搜,因同一单元格不能被重复使用,因此借助一个辅助数组用于记录单元格是否被访问过。
需要剪枝的策略:
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 的处理。