LeetCode 79. Word Search

题目

Given a 2D board and a word, find if the word exists in the grid.The word can be constructed from letters of sequentially adjacent
cell, where “adjacent” cells are those horizontally or vertically neighboring. The same letter cell may not be used more than once.

Example:

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

Given word = “ABCCED”, return true. Given word = “SEE”, return true.
Given word = “ABCB”, return false.

分析

很经典的一道leetcode题了,考察的是DFS搜索和回溯。想记录一下这道题不仅是因为这种题折磨的我要死,因为每次都是能理解题目也能写出代码,但就是不能AC,而且通过这道题还发现了一些小惊喜。

先来讲题。这种搜索题的思想就是从图中每个位置开始,然后上下左右四个方向都进行搜索,直接DFS到不匹配单词或者是走到了尽头,那就回溯换方向。因为需要比较每次走到的位置,所以参数肯定少不了横纵坐标x,y,然后又要跟单词的每个位置进行比较,就少不了单词的字母下标index,再就是dfs需要考虑重复访问的问题,走过了一个点就不能再走回来,否则就死循环了,所以需要一个标记数组visited。分析到这我们就能写代码了,如何安排代码逻辑这也是这种题的难处。

代码的关键就是要写出搜索的函数check

  int dir[4][2] = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}};
    bool search(vector<vector<char>>& board, string& word, int index, int x, int y, vector<vector<bool>>& visited) {
        if (index == word.size()) return true;
        int xsize = board.size(), ysize = board[0].size();
        if (x < 0 || x >= xsize || y < 0 || y >= ysize || board[x][y] != word[index] || visited[x][y]) return false;
        //access
        visited[x][y] = true;
        
        bool ret = search(board, word, index + 1, x + dir[0][0], y + dir[0][1], visited)
            || search(board, word, index + 1, x + dir[1][0], y + dir[1][1], visited)
            || search(board, word, index + 1, x + dir[2][0], y + dir[2][1], visited)
            || search(board, word, index + 1, x + dir[3][0], y + dir[3][1], visited);
        visited[x][y] = false;
        return ret;
    }

分析一下:首先我们肯定需要对走到的每个位置和单词的对应字母进行一个比较,如果不满足条件就直接return false

    if (x < 0 || x >= xsize || y < 0 || y >= ysize || board[x][y] != word[index] || visited[x][y]) return false;

那代码走过这一句之后就表明当前位置和index下标处匹配上了,也就是现在我们要这个位置的字母,所以下一步就是给visited这个位置打上标记。
接着找下一个位置,因为可以四个方向搜索,四个方向都有可能找得到这个单词,所以四个方向的递归结果做一个并得到的结果作为返回值。所以这段代码肯定是写在visited打标记之后。

 bool ret = search(board, word, index + 1, x + dir[0][0], y + dir[0][1], visited)
                || search(board, word, index + 1, x + dir[1][0], y + dir[1][1], visited)
                || search(board, word, index + 1, x + dir[2][0], y + dir[2][1], visited)
                || search(board, word, index + 1, x + dir[3][0], y + dir[3][1], visited);

到这里好像就结束了主要逻辑,唯一需要注意的地方就是边界条件。
不难想象,当word的最后一个位置都匹配的时候,index = word.size() - 1,此时还会调用一次四个方向的search,index + 1的值就是word.size()了,所以显然找到的边界条件就是index == word.size(),而且这个条件还需要写在不满足return false之前,因为此时index的值超出了word的下标范围。到这里,这个程序就写好了,完整代码如下。

class Solution {
public:
    int dir[4][2] = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}};
    bool search(vector<vector<char>>& board, string& word, int index, int x, int y, vector<vector<bool>> visited) {
        if (index == word.size()) return true;
        
        int xsize = board.size(), ysize = board[0].size();
        if (x < 0 || x >= xsize || y < 0 || y >= ysize || board[x][y] != word[index] || visited[x][y]) return false;
        visited[x][y] = true;
        
        bool ret = search(board, word, index + 1, x + dir[0][0], y + dir[0][1], visited)
            || search(board, word, index + 1, x + dir[1][0], y + dir[1][1], visited)
            || search(board, word, index + 1, x + dir[2][0], y + dir[2][1], visited)
            || search(board, word, index + 1, x + dir[3][0], y + dir[3][1], visited);
        return ret;
    }
    bool exist(vector<vector<char>>& board, string word) {
        vector<vector<bool>> visited(board.size(), vector<bool>(board[0].size()));
        for (int i = 0; i < board.size(); ++i) {
            for (int j = 0; j < board[i].size(); ++j) {
                if (search(board, word, 0, i, j, visited)) return true;
            }
        }
        return false;
    }
};

然后本地跑一下也过了case,提交一发。有意思的事情就发生了。

在这里插入图片描述
没错 超时了。我开始是以为这个算法的问题,但是想来想去,这毕竟是考察图的算法,时间复杂度一般不是卡oj的问题(当然有更好的算法那当我没说)。所以我把这个超时的case拷贝到本地run一发,发现本地是过了的,时间大概是200ms左右,这显然是不卡的,这就让我纳闷了,难道是因为网络io或者服务器judger的原因?还是说这道题我这个算法太naive了?可这就是一道medium…
总之,这道题卡了我很久,后来实在想不出来翻了大神们的代码和讲解,其实他们ac的算法跟我的想法是一样的,但为什么就过了呢?

仔细看了一下,根本原因就是在引用上面。我的代码在每次给visited打标记然后返回ret之后是没有也不用恢复visited为初始状态的,原因很简单,因为每次调用serach我用的是值传递,在递归的下一层中修改的visited是不影响上一层的。这样写带来了不用加代码的好处同时也增大了时间的开销,原因就在于每次调用search都需要把visited数组进行一个拷贝,我这里的visited是和board一样大的,所以当borad很大的时候,就需要在拷贝visited上花费较大的开销,这就是导致我的代码ac不掉的原因。

知道了原因,解决办法就很简单,该传值为传引用,但这样每次调用search对visited的改动会影响到下一层,所以在ret返回之后,需要加一句把visited数组还原。改过之后的代码是这样的

class Solution {
public:
    int dir[4][2] = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}};
    // visited的参数改为了传引用
    bool search(vector<vector<char>>& board, string& word, int index, int x, int y, vector<vector<bool>>& visited) {
        if (index == word.size()) return true;
        
        int xsize = board.size(), ysize = board[0].size();
        if (x < 0 || x >= xsize || y < 0 || y >= ysize || board[x][y] != word[index] || visited[x][y]) return false;
        visited[x][y] = true;
        
        bool ret = search(board, word, index + 1, x + dir[0][0], y + dir[0][1], visited)
            || search(board, word, index + 1, x + dir[1][0], y + dir[1][1], visited)
            || search(board, word, index + 1, x + dir[2][0], y + dir[2][1], visited)
            || search(board, word, index + 1, x + dir[3][0], y + dir[3][1], visited);
            
        //需要恢复visited的值
        visited[x][y] = false;
        return ret;
    }
    bool exist(vector<vector<char>>& board, string word) {
        vector<vector<bool>> visited(board.size(), vector<bool>(board[0].size()));
        for (int i = 0; i < board.size(); ++i) {
            for (int j = 0; j < board[i].size(); ++j) {
                if (search(board, word, 0, i, j, visited)) return true;
            }
        }
        return false;
    }
};

submit一发,ac了而且性能还不错。
在这里插入图片描述

总结

这道题的收获不仅是对DFS搜索,回溯的学习,更是发现了一个小tip,就是写算法题尽可能使用引用来减少拷贝的时间消耗,而且它的影响还真的挺大的。

PS:另外这道题还可以不加visited数组而是通过修改访问过的字符来表示已经访问过,从而减少空间复杂度。等过段时间把这种做法也写上 作为复习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值