【探索-中级算法】单词搜索

在这里插入图片描述

解法一 递归

结合回溯与深搜,因同一单元格不能被重复使用,因此借助一个辅助数组用于记录单元格是否被访问过。

需要剪枝的策略:

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值