LeetCode学习笔记——单词搜索(回溯)

个人博客:The Blog Of WaiterXiaoYY 欢迎来互相交流学习。

今天来聊聊回溯思想,

回溯,可以理解为后退或者返回上一层,

通常用法是:对于每处都有多种方向选择前进,当选择一种方向前进后,发现不能再继续前进的时候,就返回该处继续选择剩下的方向前进,

这样子的思想就称为回溯。

比如给定一个二维表,求到达某处的的路径数量,

如果该处不是在边界,那么对于该处来说,就有四个方向可选择,

通常我们结合深度优先搜索(DFS)来做,

DFS的好处就是一条路走到黑,不撞南墙不回头

选定一个方向一直进行下去,当发现没有再进行下去的条件的时候,就开始回溯回来,继续选择剩下的三个方向继续DFS前进,

这个进行下去的条件一般会有两个,

  • 该处是否被访问过了,这个我们一般借用一个标记数组来辅助
  • 是否在边界,前进方向减少

当然有时候题目还会有其他条件,比如说障碍物等等。


以上就是对回溯的大概介绍,如果能够理解了上面,说明你大概对回溯有一定的认识了,

下面我们通过两道题目来认识一下回溯具体的实现,


单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false

提示:

  • board 和 word 中只包含大写和小写英文字母。
  • 1 <= board.length <= 200
  • 1 <= board[i].length <= 200
  • 1 <= word.length <= 10^3

解题思路

这道题,我一开始没有看仔细,一拿到题目,就刷刷的开始写,用了两个HashTable,一个记录board中出现的字母数量,一个用来记录单词中字母的数量,主要判断两种数量是否一致就可以判断单词是存在在网格中。

以上是错误示范,因为题目有个很苛刻的条件,

单词必须按照字母的顺序,通过相邻的单元格内的字母构成,

有人可能一下子对“相邻“这个概念有点懵,题目也给出解释了,通俗来讲就是当前的上下左右四个方向,其他的都不行,

比如有一个测试案例

[‘a’, ‘b’], [‘c’, ‘d’]

求“abcd”的时候返回的是false,

这是因为,

[‘a’, ‘b’],

[‘c’, ‘d’]

b和c不在相邻位置,

理解这个很重要,因为这就是涉及到我们的回溯的思想,

回归到题目,

我们从网格的左上角开始,与单词从左到右的字母进行比较,

当该处符合的时候,就把这里标记成已访问,然后对四个方向进行进行dfs,

这时候就涉及到了一个知识,如何重置dfs的状态

当对该点的四个方向都进行遍历后,发现并没有符合要求,那么此处的访问状态得改回未访问状态,为回溯做准备,

当符合要求的时候,就返回true;

下面以题目的事例进行图解说明:

查找 ABCCEE

搜索从左上角开始,

在这里插入图片描述

将单词从左到右进行对比,

在这里插入图片描述

此时匹配,但此时在边界,只有两个方向可以选择,先考虑右边,

在这里插入图片描述

右边匹配相等,此时还在边界,依旧只有两个方向,继续考虑右边,

在这里插入图片描述

此时匹配,和上面一样,继续考虑右边,

在这里插入图片描述

当前不匹配,此时回溯上一层,

在这里插入图片描述

从 “C” 处进行重新选择,之前在“C” 处选择了右边,此时只剩下下面,则往下面进行DFS,

在这里插入图片描述

此时匹配,上面已经标记被访问过,此时只剩下三个方向,先考虑右边,

在这里插入图片描述
当前不匹配,回溯上一层,

在这里插入图片描述

从 “C” 处进行重新选择,之前在“C” 处选择了右边,而上边又被访问过,此时只剩下下边和左边,先考虑下边,则往下面进行DFS,当前匹配,

在这里插入图片描述

此时在边界,只有两个方向选择,先考虑右边,此时匹配,则说明,从左上角(0,0)处出发,能找到一个单词,则返回true,如果不能,则从(0,1)开始继续DFS。

在这里插入图片描述


如果理解了上面,说明基本上已经搞定这道题了,

通过分析,我们需要几个东西:

  • 四个方向的表示

  • 标记数组(用于标记是否被访问)

  • DFS方法(用于对四个方向分别进行dfs)

  • 判断边界的方法

通过这些,我们就可以一步步搭建起我们的代码。


代码
class Solution {
    //用于跨方法使用
    private String word;
    private char[][] board;
    //标记数组
    private boolean[][] marked;
    //四个方向表示
    private int[][] direction ={{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    //网格的行和列
    private int row;
    private int column;
    
    public boolean exist(char[][] board, String word) {
        row = board.length;
        column = board[0].length;
        if(row <= 0)
            return false;
        this.board = board;
        this.word = word;
		//初始化标记数组
        marked = new boolean[row][column];
		//遍历网格,对每个点都开始进行dfs,直到出结果
        for(int i = 0; i < row; i++) {
            for(int j = 0; j < column; j++) {
                //如果在dfs中返回true,代表匹配到单词,则返回true
                if(dfs(i, j, 0))
                    return true;
            }
        }
        return false;
    }
	//DFS方法
    public boolean dfs(int i, int j, int start) {
        //start是指向单词字母的指针,一开始是0,指向首字母,然后++,代表从左往右遍历,在末尾要进行特判
        if(start == word.length() - 1)
            return board[i][j] == word.charAt(start);
        //如果此处已经匹配,则往此处进行方向选择
        if(board[i][j] == word.charAt(start)) {
            //将该处的状态标记为已访问
            marked[i][j] = true;
            //进行四个方向选择
            for(int k = 0; k < 4; k++) {
                int newX = i + direction[k][0];
                int newY = j + direction[k][1];
                //如果下一处没有越界,且还未访问过,则前进
                if(inArea(newX, newY) && !marked[newX][newY]) {
                    //如果一直进行dfs直到最后一个单词的字母也匹配上,则返回true
                    if(dfs(newX, newY, start + 1))
                        return true;
                }
            }
            //否则,将该处的状态标记为未访问,未下一轮做准备
            marked[i][j] = false;
        }
        return false;
    }
	//判断是否在边界,也是判断是否越界的方法
    public boolean inArea(int newX, int newY) {
        return newX >= 0 && newX <row && newY >= 0 && newY < column;
    }
}

以上就是单词搜索的做法,采用回溯,一步一步的前进,遇到不对的,就回溯,返回上一层继续选择前进,直到把这条路走到黑,也就是把单词遍历完都没有找到,那就得重新选择开始点,直到把网格全部的点作为开始点尝试过,都没有的话,说明该网格中的字母找不到这个单词。


以上是单词搜索 I ,还有一个单词搜索 II,如果以上已经掌握了,下面这个就是加两句代码的事,来看看题,


单词搜索 II

给定一个二维网格 board 和一个字典中的单词列表 words,找出所有同时在二维网格和字典中出现的单词。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。

示例:

输入: 
words = ["oath","pea","eat","rain"] and board =
[
  ['o','a','a','n'],
  ['e','t','a','e'],
  ['i','h','k','r'],
  ['i','f','l','v']
]

输出: ["eat","oath"]

解题思路

相比于第一题,这道题寻找的不是一个单词,而是一组单词,

那很简单啊,只要从单词表中一个一个的取出来进行比较就行了,

这不就是第一题了吗?

所以掌握了上一题,这一题就是复制粘贴,

但这道题还是需要一个列表来保存结果的,

直接上代码吧。


代码
class Solution {
    //用于跨方法使用
    private String word;
    private char[][] board;
    //标记数组
    private boolean[][] marked;
    //四个方向表示
    private int[][] direction ={{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    //网格的行和列
    private int row;
    private int column;
    
    public List<String> findWords(char[][] board, String[] words) {
        //列表保存结果
        List<String> res = new ArrayList<>();
        //从单词表中一个一个取出单词比较
        for(String word1 : words) {
            //如果找到则加入到列表中
            if(exist(board, word1))
                res.add(word1);
        }
        return res;
    }
    
    public boolean exist(char[][] board, String word) {
        row = board.length;
        column = board[0].length;
        if(row <= 0)
            return false;
        this.board = board;
        this.word = word;
		//初始化标记数组
        marked = new boolean[row][column];
		//遍历网格,对每个点都开始进行dfs,直到出结果
        for(int i = 0; i < row; i++) {
            for(int j = 0; j < column; j++) {
                //如果在dfs中返回true,代表匹配到单词,则返回true
                if(dfs(i, j, 0))
                    return true;
            }
        }
        return false;
    }
	//DFS方法
    public boolean dfs(int i, int j, int start) {
        //start是指向单词字母的指针,一开始是0,指向首字母,然后++,代表从左往右遍历,在末尾要进行特判
        if(start == word.length() - 1)
            return board[i][j] == word.charAt(start);
        //如果此处已经匹配,则往此处进行方向选择
        if(board[i][j] == word.charAt(start)) {
            //将该处的状态标记为已访问
            marked[i][j] = true;
            //进行四个方向选择
            for(int k = 0; k < 4; k++) {
                int newX = i + direction[k][0];
                int newY = j + direction[k][1];
                //如果下一处没有越界,且还未访问过,则前进
                if(inArea(newX, newY) && !marked[newX][newY]) {
                    //如果一直进行dfs直到最后一个单词的字母也匹配上,则返回true
                    if(dfs(newX, newY, start + 1))
                        return true;
                }
            }
            //否则,将该处的状态标记为未访问,未下一轮做准备
            marked[i][j] = false;
        }
        return false;
    }
	//判断是否在边界,也是判断是否越界的方法
    public boolean inArea(int newX, int newY) {
        return newX >= 0 && newX <row && newY >= 0 && newY < column;
    }
}


整理于2020.4.11,本文图片来源于liweiwei1419的题解,只为学习而用,侵删。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值