单词搜索--回溯算法

2023-11-03

LeetCode

单词搜索

给定一个二维网格 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"]

解法1:穷举法

解题思路:

要在一个二维矩阵里面找到所有可能的单词,最简单的方法就是穷举,现在的问题就是如何穷举所有的单词,我们用回溯,也就是从一个字符开始,依次探索它的四个方向,就像迷宫问题一样,当一个方向走完时,就回退一步,选择其他方向,就像迷宫问题一样

完整代码如下:

import java.util.ArrayList;
import java.util.Arrays;

class Solution {
    public ArrayList<String> findWords(char[][] board, String[] words) {
        StringBuilder word = new StringBuilder(); //用来存放探索到的单词
        ArrayList<String> res = new ArrayList<String>();//结果集
        ArrayList<String> Words = new ArrayList<String>();//主要是为了contains的方法
        boolean[][] used = new boolean[board.length][board[0].length];//用过的字符在一个单词中不能再次被使用
        for(String tmp : words)
            Words.add(tmp);
        for(int i=0; i<board.length; i++)//从每一个点开始遍历查找
            for(int j=0; j<board[0].length; j++)
            {
                used[i][j]=true; //true表示已被使用
                word.append(board[i][j]); //加入字符串中
                backtrack(board,used,Words,word,res,i,j);
                word.deleteCharAt(word.length()-1);//删除该字符
                used[i][j]=false;//置为未使用
            }
                    
        return res;
    }
    public void backtrack(char[][] board, boolean[][] used, ArrayList<String> Words, StringBuilder word, ArrayList<String> res, int r, int c)
    {
        if(Words.contains(word.toString())) //如果探索到的字符串在单词表里
        {
            if(Words.contains(word.toString()) && !res.contains(word.toString()))
                res.add(word.toString());
            //加入结果集中,不重复加入
        }
        for(int i=0; i<4; i++) //四个方向探索
        {
            switch(i)
            {
            case 0:
                if(c-1>=0)
                    if(!used[r][c-1])
                    {
                        used[r][c-1]=true;
                        word.append(board[r][c-1]);
                        backtrack(board,used,Words,word,res,r,c-1);
                        word.deleteCharAt(word.length()-1);
                        used[r][c-1]=false;
                        //同理
                    }
                break;
            case 1:
                if(r-1>=0) 
                    if(!used[r-1][c])
                    {
                        used[r-1][c]=true;
                        word.append(board[r-1][c]);
                        backtrack(board,used,Words,word,res,r-1,c);
                        word.deleteCharAt(word.length()-1);
                        used[r-1][c]=false;
                    }
                break;
            case 2:
                if(c+1<board[0].length) 
                    if(!used[r][c+1])
                    {
                        used[r][c+1]=true;
                        word.append(board[r][c+1]);
                        backtrack(board,used,Words,word,res,r,c+1);
                        word.deleteCharAt(word.length()-1);
                        used[r][c+1]=false;
                    }
                break;
            case 3:
                if(r+1<board.length) 
                    if(!used[r+1][c])
                    {
                        used[r+1][c]=true;
                        word.append(board[r+1][c]);
                        backtrack(board,used,Words,word,res,r+1,c);
                        word.deleteCharAt(word.length()-1);	        			
                        used[r+1][c]=false;
                    }
                break;
            }
        }
    }
}

这个解法超时了,因为做了太多的无用功,比如说,现在我从一个字符a开始探索,然而,单词表里的所有单词没有一个是以a开头的,那我们接下来的做的是不是都是无用功,因此我们希望能够通过前缀来决定应该探索哪个方向,这可以通过一个树结构来实现,树的结构为:

class TrieNode 
{
    HashMap<Character, TrieNode> children = new HashMap<Character, TrieNode>();
    //该结构用来存放孩子结点
    String word = null; //遍历到当前位置时的字符串
    public TrieNode() {}
}

树的结构类似于:

现在要做的就是构造该树

// Step 1). Construct the Trie
    TrieNode root = new TrieNode();
    for (String word : words) 首先从单词表里取出一个单词
    {
        TrieNode node = root; //从根结点开始
        for(Character letter : word.toCharArray()) //取出单词里面的每一个字符
        {
            if (node.children.containsKey(letter)) //如果该字符已经在树上
            {
                node = node.children.get(letter); //将node置为树上对应的结点
            }
            else 
            {
                TrieNode newNode = new TrieNode(); //新建一个结点
                node.children.put(letter, newNode); //加入到node结点上
                node = newNode; //将node置为新增结点
            }
        }
        node.word = word;  // store words in Trie //当一个单词的所有结点遍历完时,当前结点回溯到根节点就是该单词,所以将word置为该单词
    }

最后一步,我们要写的就是回溯函数,回溯函数应该怎么写?首先要从一个字符开始,然后找它的四个方向,如果一个方向对应的字符不在树上,就找下一个方向

private void backtracking(int row, int col, TrieNode parent) 
{
    Character letter = this._board[row][col]; //取出当前字符
    TrieNode currNode = parent.children.get(letter);//找到当前字符在树上的结点

    // check if there is any match
    if (currNode.word != null) 
    { 
        //如果该结点的字符串不是空,说明是一个单词,将该单词加入结果集里面
        this._result.add(currNode.word);
        currNode.word = null;
    }

    // mark the current letter before the EXPLORATION
    this._board[row][col] = '#'; //将一个位置设为不可走

    // explore neighbor cells in around-clock directions: up, right, down, left
    int[] rowOffset = {-1, 0, 1, 0};
    int[] colOffset = {0, 1, 0, -1};
    四个方向的偏差
    for (int i = 0; i < 4; ++i) 
    { //找四个方向
        int newRow = row + rowOffset[i];
        int newCol = col + colOffset[i];
        if (newRow < 0 || newRow >= this._board.length || newCol < 0
            || newCol >= this._board[0].length) 
        {
            continue;
        }
        //越界处理
        if (currNode.children.containsKey(this._board[newRow][newCol])) //如果该字符在树上,就可以往下探索
        {
            backtracking(newRow, newCol, currNode);
        }
    }

    // End of EXPLORATION, restore the original letter in the board.
    this._board[row][col] = letter; //重新置为可走

    // Optimization: incrementally remove the leaf nodes
    if (currNode.children.isEmpty()) 
    {
        parent.children.remove(letter); //剪枝,因为如果一个结点的所有孩子都为空,而且该结点已经被探索过了,就可以剪掉
    }
}

完整代码:

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

class Solution {
  char[][] _board = null;
  ArrayList<String> _result = new ArrayList<String>();

  public List<String> findWords(char[][] board, String[] words) {

    // Step 1). Construct the Trie
    TrieNode root = new TrieNode();
    for (String word : words) {
      TrieNode node = root;

      for (Character letter : word.toCharArray()) {
        if (node.children.containsKey(letter)) {
          node = node.children.get(letter);
        } else {
          TrieNode newNode = new TrieNode();
          node.children.put(letter, newNode);
          node = newNode;
        }
      }
      node.word = word;  // store words in Trie
    }

    this._board = board;
    // Step 2). Backtracking starting for each cell in the board
    for (int row = 0; row < board.length; ++row) {
      for (int col = 0; col < board[row].length; ++col) {
        if (root.children.containsKey(board[row][col])) {
          backtracking(row, col, root);
        }
      }
    }

    return this._result;
  }
  
  private void backtracking(int row, int col, TrieNode parent) {
    Character letter = this._board[row][col];
    TrieNode currNode = parent.children.get(letter);

    // check if there is any match
    if (currNode.word != null) {
      this._result.add(currNode.word);
      currNode.word = null;
    }

    // mark the current letter before the EXPLORATION
    this._board[row][col] = '#';

    // explore neighbor cells in around-clock directions: up, right, down, left
    int[] rowOffset = {-1, 0, 1, 0};
    int[] colOffset = {0, 1, 0, -1};
    for (int i = 0; i < 4; ++i) {
      int newRow = row + rowOffset[i];
      int newCol = col + colOffset[i];
      if (newRow < 0 || newRow >= this._board.length || newCol < 0
          || newCol >= this._board[0].length) {
        continue;
      }
      if (currNode.children.containsKey(this._board[newRow][newCol])) {
        backtracking(newRow, newCol, currNode);
      }
    }

    // End of EXPLORATION, restore the original letter in the board.
    this._board[row][col] = letter;

    // Optimization: incrementally remove the leaf nodes
    if (currNode.children.isEmpty()) {
      parent.children.remove(letter);
    }
  }
}

心得体会

做了六七道中等难度的题,感觉就是中等难度一般不用优化就能过了,但是困难难度如果没有优化,就会超时,做了两三道都是这样,其实基本思路还是挺简单的,就是回溯

题目以及解法来源

作者:LeetCode
链接:https://leetcode-cn.com/problems/word-search-ii/solution/dan-ci-sou-suo-ii-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

单词搜索--回溯算法 的相关文章

  • JAVA中正则表达式的使用

    正则表达式 用法 一 使用正则表达式对String进行匹配 1 控制匹配长度 1 使用 n 来精确控制 2 使用 n 表示大于等于n个 3 使用 m n 控制范围 4 使用 表示可以出现 0次或一次 5 使用 表示可以出现 0次或多次 6
  • 8.翻转子串

    题目描述 假定我们都知道非常高效的算法来检查一个单词是否为其他字符串的子串 请将这个算法编写成一个函数 给定两个字符串s1和s2 请编写代码检查s2是否为s1旋转而成 要求只能调用一次检查子串的函数 给定两个字符串s1 s2 请返回bool
  • 大数(四则运算)

    四则运算 大数加法 高精度加法 大数减法 大数乘法 大数乘法 幂运算 大数乘法 高精度幂运算 大数除法 大数加法 思路 从后往前算 即由低位向高位运算 计算的结果依次添加到结果中去 最后将结果字符串反转 输入的时候两个数都是以字符串的形式输
  • 在事情没有变得糟糕之前_这是我们没有的问题的糟糕解决方案

    在事情没有变得糟糕之前 最糟糕的代码是什么 不要说JavaScript 它会在你身上成长 不 也不是Perl 好的 所以Perl非常令人讨厌 最糟糕的代码 这是我们不需要的代码 紧接着是几乎有效的代码 然后是无效的代码 几乎可以正常工作的代
  • Java字符串分析器

    package StruingTokenizer import java util StringTokenizer public class StringTokenizer public static void main String ar
  • 编程艺术 - 第一章 左旋转字符串

    题目 定义字符串的左旋转操作 把字符串前面的若干个字符移动到字符串的尾部 若把字符串abcdef左旋转2位得到字符串cdefab 请实现字符串左旋转的函数 要求对长度为n的字符串操作的时间复杂度为O n 空间复杂度为O 1 类似题目还有剑指
  • 第一章 pandas基础-练习题

    第一章 pandas基础 练习题 首先要导入对应的模块 import pandas as pd import numpy as np Ex1 口袋妖怪数据集 现有一份口袋妖怪的数据集 下面进行一些背景说明 代表全国图鉴编号 不同行存在相同数
  • 微信小程序富文本标签rich-text的使用

    wxml 接收对象数组
  • C/C++2019秋招面试题集合01

    C C 2019秋招面试题集合01 8 19 腾讯 提前批 客户端开发 1 给定一个字符串数组 和一个子串 求字符串中是否存在子串 如果存在则返回首个匹配到的索引位置 否则 返回 1 不能调用库函数 例如 字符串数组 Integrity P
  • 基础算法题——带分数(全排列,工具库)

    前言 这道题理解起来不难 但是要找到一个合适的方法对题目进行优化 就会相对麻烦些 蓝桥杯的题 真的到处都是坑的感觉 带分数题目 资源限制 时间限制 1 0s 内存限制 256 0MB 问题描述 100 可以表示为带分数的形式 100 3 6
  • 知识点——初识java中File类

    1 1 什么是File类 SUN公司提供给开发者操作文件和文件夹的一个类对象 Java中万物皆对象 计算机中万物皆文件 获取File类有三种方式 Constructor 构造方法 File String pathName 根据对应的文件路径
  • Python求三位水仙花数

    Python求三位水仙花数 简介 水仙花数 是指一个三位整数 其各位数字的3次方和等于该数本身 例如 ABC是一个 3位水仙花数 则 A的3次方 B的3次方 C的3次方 ABC 基础掌握 Python str 函数 https www ru
  • PTA天梯赛L1-058 6翻了(c语言实现)

    原题链接 这道题稍微有一点点灵活 乍一想还是有点想不到的 主要还是对6的个数进行计数 如果是6则计数有多少个6 如果不是6的话则要进行判断 如果在此之前6的个数超过了3 gt 3 但是小于等于9那么要输出9 如果在此之前6的个数超过了9 g
  • 基础算法题——帅到没朋友(唯一性)

    帅到没朋友 当芸芸众生忙着在朋友圈中发照片的时候 总有一些人因为太帅而没有朋友 本题就要求你找出那些帅到没有朋友的人 输入格式 输入第一行给出一个正整数N 100 是已知朋友圈的个数 随后N行 每行首先给出一个正整数K 1000 为朋友圈中
  • Java基础之String类型详解

    目录 1 简介 2 字符串的比较 3 String的实例化方式 1 直接赋值方式 2 构造方法实例化 4 String对象 常量 池 5 字符串修改 6 String类常用方法 1 字符串查找 2 字符串替换 3 字符串拆分 4 字符串截取
  • 【Java】Java中的String类

    文章目录 一 认识 String 类 二 String 类的常用方法 2 1 构造方法 2 2 String 类对象之间的比较 2 3 字符串查找 2 4 字符串的转换 2 5 字符串替换 2 6 字符串拆分 2 7 字符串截取 2 8 字
  • 经典算法题:大数乘法之乘法累加算法 Karatsuba算法(远远超出long long范围)

    问题 超过100位数字的整数的乘法 无法直接调用 运算 例如 求 1234567891011121314151617181920 2019181716151413121110987654321 的乘积结果 需要转化成数组问题或者字符串问题求
  • Python编程中的for循环语句学习教程

    本文来源于公众号 csdn2299 喜欢可以关注公众号 程序员学府 这篇文章主要介绍了Python编程中的for循环语句学习教程 是Python入门学习中的基础知识 需要的朋友可以参考下 Python for循环可以遍历任何序列的项目 如一
  • 滑动窗口专题(字节面试题)

    关键字 连续数组 字串 1 和为s的连续正整数序列 剑指offer57 II 输入一个正整数 target 输出所有和为 target 的连续正整数序列 至少含有两个数 序列内的数字由小到大排列 不同序列按照首个数字从小到大排列 示例 1
  • C语言实现随机发纸牌

    C语言实现随机发纸牌 为避免重复发牌 设二维数组sign 4 13 记载是否发过纸牌 其中行下表表示花色 列下标表示点数 设字符串指针数组card n 存储随机发的n张纸牌 例如card 0 梅花2 按照以下方法以此发出每一张牌 首先产生一

随机推荐

  • 智慧城市篇

    智慧城市篇 数字孪生智慧排水管网管理平台https mp weixin qq com s ZDgmKqHRztYk2ehBDbi3AA 2022年3月1日 住房和城乡建设部印发了 十四五 住房和城乡建设科技发展规划 提出关于实现城市基础设施
  • Mybatis中的resultType和resultMap

    一 概述 MyBatis中在查询进行select映射的时候 返回类型可以用resultType 也可以用resultMap resultType是直接表示返回类型的 而resultMap则是对外部ResultMap的引用 但是resultT
  • android组件悬浮,Andorid 任意界面悬浮窗,实现悬浮窗如此简单

    特性 1 支持拖动 提供自动贴边等动画 2 内部自动进行权限申请操作 3 可自由指定要显示悬浮窗的界面 4 应用退到后台时 悬浮窗会自动隐藏 5 位置不可变的悬浮窗无需权限申请 6 位置及宽高可设置百分比值 轻松适配各分辨率 7 链式调用
  • [Python / PyTorch] debug backward()

    问题描述 在自定义Loss的中 其backward 函数不支持在PyCharm中进行断点调试 因此需要以其他方式进行断点调试 解决方案 参考 Is there a way to debug the backward method of Fu
  • SQLI-Labs(3)8-14关【布尔盲注和时间盲注】

    目录 第八关 第九关 第十关 第十一关 第十二关 第十三关 第十四关 第八关 我们用测试语句来测试是否为注入点 从上图中得知存在注入点 那么接下来就是爆列 一共有三列 接下来用union select 和报错注入都试一下发现没有回显点 那么
  • thinkPHP使用PHPExcel实现导入导出

    目录 一 使用composer安装PHPExcel 二 使用PHPExcel 1 导入Excel文件 2 导出数据 3 导出方法使用demo 效果图 一 使用composer安装PHPExcel 安装命令 composer require
  • 常见的PLC通讯协议有哪些?

    PLC 可编程逻辑控制器 通讯方式有多种 以下是一些常见的通讯方式 串口通信 使用串行接口 如RS232 RS485等 进行通信 常用于与外部设备进行简单的数据传输 以太网通信 通过以太网接口进行通信 可以实现较高的数据传输速率和远程连接
  • 键值数据库PebblesDB读后感

    键值数据库PebblesDB读后感 在LevelDB RocksDB这种分层思路上 PebblesDB提出了一种减少写放大的思路 下面学习并总结 所述以论文为基础 也有个人 观点 客观论述请看原文 虽然LSM的写放大最近被研究很多 但是就写
  • 关于log4j

    log4j 在强调可重用组件开发的今天 除了自己从头到尾开发一个可重用的日志操作类外 Apache为我们提供了一个强有力的日志操作包 Log4j 官方站点 http logging apache org log4j Log4j是Apache
  • linux拒绝更改密码,【Linux】解决SSH服务拒绝密码

    xShell连接Linux服务器提示密码错误 1 检查虚拟机SSH服务是否开启 service sshd status 如果没有开启 请执行service sshd start启动该服务 或者通过service sshd restart重启
  • web3.0 nft 是什么? nft的意义是什么?

    英国一名12岁的男孩本雅明 他在暑假期间画了一系列的画作 并在网上以数字藏品的形式进行出售 不到9小时就全部售完 赚取的虚拟货币价值相当于250万人民币 这些画算不上很高端的艺术佳作 而是由密密麻麻的像素组成各种形状各异的鲸鱼 每条鲸鱼都有
  • Vulhub漏洞靶场搭建和使用

    今天继续给大家介绍渗透测试相关知识 本文主要内容是Vulhub漏洞靶场搭建和使用 免责声明 本文所介绍的内容仅做学习交流使用 严禁利用文中技术进行非法行为 否则造成一切严重后果自负 再次强调 严禁对未授权设备进行渗透测试 一 Vulhub漏
  • 视频播放器测试点

    视频播放器测试点 在负责XX项目组的测试中 接触了好多的关于播放器的测试 基于这些 再结合我测试过程中遇到的问题整理的测试点分别从以下几个方面进行 功能测试 视频资源可以正常获取 不管是服务器返回还是后台添加等 视频的封面图 页面UI等正常
  • 【转载】关于在keil5的time environment没有StdPeriph Drivers(标准库)但是又想使用库函数的解决办法

    关于在keil5的time environment没有StdPeriph Drivers 标准库 但是又想使用库函数的解决办法 本人刚刚接触keil5 遇到了一些问题 希望我的方法能帮到大家 作者本人使用的是芯片是stm32f407VET6
  • SpringBoot配置动态定时任务

    1 配置ScheduledTask 主要是实现SchedulingConfigurer 动态传入cron package com hzl boot config import lombok Data import org springfra
  • Spring Cloud OAuth2(一) 搭建授权服务

    本文内容主要为spring cloud 授权服务的搭建 采用jwt认证 GitHub 地址 https github com fp2952 spring cloud base tree master auth center auth cen
  • [USACO13DEC]Optimal Milking G【线段树维护最大独立集】

    题目链接 P3097 USACO13DEC Optimal Milking G 很明显的是这道题有4e4个点 直接跑最大独立集的话 那么测评机承受不起啊 所以 这里要维护一个区间dp的形式 每个区间有左右两个端点 我们现在要合并两个区间的话
  • HTTP缓存

    HTTP缓存 什么是HTTP缓存 http缓存指的是 当客户端向服务器请求资源时 会先抵达浏览器缓存 如果浏览器有 要请求资源 的副本 就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源 常见的http缓存只能缓存get请求响应的
  • Java中的作用域

    目录 Java作用域 Java中变量类型主要有3种 成员变量 静态变量和局部变量 成员变量或方法也有4种作用域 静态修饰符的特点 静态使用的注意事项 静态的优缺点 当成员变量被静态修饰后 和非静态成员变量的区别 方法作用域 块作用域 基本使
  • 单词搜索--回溯算法

    LeetCode 单词搜索 给定一个二维网格 board 和一个字典中的单词列表 words 找出所有同时在二维网格和字典中出现的单词 单词必须按照字母顺序 通过相邻的单元格内的字母构成 其中 相邻 单元格是那些水平相邻或垂直相邻的单元格