单词搜索 II[困难]

news2024/11/16 17:55:09

一、题目

给定一个m x n二维字符网格board和一个单词(字符串)列表words, 返回所有二维网格上的单词。单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。

示例 1:

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

示例 2:

输入:board = [["a","b"],["c","d"]], words = ["abcb"]
输出:[]

m == board.length
n == board[i].length
1 <= m, n <= 12
board[i][j]是一个小写英文字母
1 <= words.length <= 3 * 104
1 <= words[i].length <= 10
words[i]由小写英文字母组成
words中的所有字符串互不相同

二、代码

【1】回溯 + 字典树: 前缀树(字典树)是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。前缀树可以用O(∣S∣)的时间复杂度完成如下操作,其中∣S|是插入字符串或查询前缀的长度:
1、向前缀树中插入字符串word
2、查询前缀串prefix是否为已经插入到前缀树中的任意一个字符串word的前缀;

根据题意,我们需要逐个遍历二维网格中的每一个单元格;然后搜索从该单元格出发的所有路径,找到其中对应words中的单词的路径。因为这是一个回溯的过程,所以我们有如下算法:
1、遍历二维网格中的所有单元格。
2、深度优先搜索所有从当前正在遍历的单元格出发的、由相邻且不重复的单元格组成的路径。因为题目要求同一个单元格内的字母在一个单词中不能被重复使用;所以我们在深度优先搜索的过程中,每经过一个单元格,都将该单元格的字母临时修改为特殊字符(例如 #),以避免再次经过该单元格。
3、 如果当前路径是words中的单词,则将其添加到结果集中。如果当前路径是words中任意一个单词的前缀,则继续搜索;反之,如果当前路径不是words中任意一个单词的前缀,则剪枝。我们可以将words中的所有字符串先添加到前缀树中,而后用O(∣S∣)的时间复杂度查询当前路径是否为 words中任意一个单词的前缀。

在具体实现中,我们需要注意如下情况:
1、因为同一个单词可能在多个不同的路径中出现,所以我们需要使用哈希集合对结果集去重。
2、在回溯的过程中,我们不需要每一步都判断完整的当前路径是否是words中任意一个单词的前缀;而是可以记录下路径中每个单元格所对应的前缀树结点,每次只需要判断新增单元格的字母是否是上一个单元格对应前缀树结点的子结点即可。

class Solution {
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public List<String> findWords(char[][] board, String[] words) {
        Trie trie = new Trie();
        for (String word : words) {
            trie.insert(word);
        }

        Set<String> ans = new HashSet<String>();
        for (int i = 0; i < board.length; ++i) {
            for (int j = 0; j < board[0].length; ++j) {
                dfs(board, trie, i, j, ans);
            }
        }

        return new ArrayList<String>(ans);
    }

    public void dfs(char[][] board, Trie now, int i1, int j1, Set<String> ans) {
        if (!now.children.containsKey(board[i1][j1])) {
            return;
        }
        char ch = board[i1][j1];
        now = now.children.get(ch);
        if (!"".equals(now.word)) {
            ans.add(now.word);
        }

        board[i1][j1] = '#';
        for (int[] dir : dirs) {
            int i2 = i1 + dir[0], j2 = j1 + dir[1];
            if (i2 >= 0 && i2 < board.length && j2 >= 0 && j2 < board[0].length) {
                dfs(board, now, i2, j2, ans);
            }
        }
        board[i1][j1] = ch;
    }
}

class Trie {
    String word;
    Map<Character, Trie> children;
    boolean isWord;

    public Trie() {
        this.word = "";
        this.children = new HashMap<Character, Trie>();
    }

    public void insert(String word) {
        Trie cur = this;
        for (int i = 0; i < word.length(); ++i) {
            char c = word.charAt(i);
            if (!cur.children.containsKey(c)) {
                cur.children.put(c, new Trie());
            }
            cur = cur.children.get(c);
        }
        cur.word = word;
    }
}

时间复杂度: O(m×n×3^(l−1)),其中m是二维网格的高度,n是二维网格的宽度,l是最长单词的长度。我们需要遍历m×n个单元格,每个单元格最多需要遍历4×3^(l−1)`条路径。
空间复杂度: `O(k×l)`,其中`k`是`words`的长度,`l`是最长单词的长度。最坏情况下,我们需要`O(k×l)`用于存储前缀树。

【2】删除被匹配的单词: 考虑以下情况。假设给定一个所有单元格都是a的二维字符网格和单词列表["a", "aa", "aaa", "aaaa"]。当我们使用方法一来找出所有同时在二维网格和单词列表中出现的单词时,我们需要遍历每一个单元格的所有路径,会找到大量重复的单词。为了缓解这种情况,我们可以将匹配到的单词从前缀树中移除,来避免重复寻找相同的单词。因为这种方法可以保证每个单词只能被匹配一次;所以我们也不需要再对结果集去重了。

class Solution {
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public List<String> findWords(char[][] board, String[] words) {
        Trie trie = new Trie();
        for (String word : words) {
            trie.insert(word);
        }

        Set<String> ans = new HashSet<String>();
        for (int i = 0; i < board.length; ++i) {
            for (int j = 0; j < board[0].length; ++j) {
                dfs(board, trie, i, j, ans);
            }
        }

        return new ArrayList<String>(ans);
    }

    public void dfs(char[][] board, Trie now, int i1, int j1, Set<String> ans) {
        if (!now.children.containsKey(board[i1][j1])) {
            return;
        }
        char ch = board[i1][j1];
        Trie nxt = now.children.get(ch);
        if (!"".equals(nxt.word)) {
            ans.add(nxt.word);
            nxt.word = "";
        }

        if (!nxt.children.isEmpty()) {
            board[i1][j1] = '#';
            for (int[] dir : dirs) {
                int i2 = i1 + dir[0], j2 = j1 + dir[1];
                if (i2 >= 0 && i2 < board.length && j2 >= 0 && j2 < board[0].length) {
                    dfs(board, nxt, i2, j2, ans);
                }
            }
            board[i1][j1] = ch;
        }

        if (nxt.children.isEmpty()) {
            now.children.remove(ch);
        }
    }
}

class Trie {
    String word;
    Map<Character, Trie> children;
    boolean isWord;

    public Trie() {
        this.word = "";
        this.children = new HashMap<Character, Trie>();
    }

    public void insert(String word) {
        Trie cur = this;
        for (int i = 0; i < word.length(); ++i) {
            char c = word.charAt(i);
            if (!cur.children.containsKey(c)) {
                cur.children.put(c, new Trie());
            }
            cur = cur.children.get(c);
        }
        cur.word = word;
    }
}

时间复杂度: O(m×n×3^(l−1)),其中m是二维网格的高度,n是二维网格的宽度,l是最长单词的长度。我们仍需要遍历m×n个单元格,每个单元格在最坏情况下仍需要遍历4×3^(l−1)条路径。
空间复杂度: O(k×l),其中kwords的长度,l是最长单词的长度。最坏情况下,我们需要O(k×l)用于存储前缀树。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1339170.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面试官:并发和并行的区别

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一波电子书籍资料&#xff0c;包含《Effective Java中文版 第2版》《深入JAVA虚拟机》&#xff0c;《重构改善既有代码设计》&#xff0c;《MySQL高性能-第3版》&…

UDP协议工作原理及实战(二)UDP客户端代码实现

这个是一个测试我们写的函数是否正确。 启动服务&#xff1a;这里边的udpsocket->bind(port)就是对端口号进行连接。

Sectigo和Certum的IP证书区别

IP证书是比较特别的一款数字证书。大多数SSL数字证书都是针对域名站点的数字证书&#xff0c;比如单域名SSL证书、多域名SSL证书和通配符SSL证书&#xff0c;而IP证书针对的是只拥有公网IP地址的站点。签发IP证书的CA认证机构并不多&#xff0c;Sectigo和Certum旗下都有IP证书&…

【网络安全】深入理解web安全攻防策略

前言 互联网时代&#xff0c;数据安全与个人隐私信息等受到极大的威胁和挑战&#xff0c;本文将以几种常见的攻击以及防御方法展开分析。 1. XSS (跨站脚本攻击) 定义&#xff1a;通过存在安全漏洞的Web网站注册用户的浏览器内运行非法的HTML标签或JavaScript进行的一种攻击…

前端简单动态圣诞树动画(HTML、js、css)

效果展示&#xff1a; 注释&#xff1a; 整体圣诞树分为3个部分&#xff0c;书的主干、粒子特效、树上的卡片树上的卡片(重点)&#xff1a;每一张卡片上都有一个名字&#xff0c;代表圣诞树的叶子&#xff0c;后面可以根据自己需求更改&#xff0c;比如全部改成喜欢人的名字&am…

最大公约和最小公倍数 C语言xdoj183

问题描述&#xff1a; 输入两个正整数 m 和 n&#xff0c;求其最大公约数和最小公倍数。 输入说明&#xff1a; 输入两个正整数 m 和 n。 输出说明&#xff1a; 输出 m 和 n 的最大公约数和最小公倍数。 输入样例&#xff1a; 8 12 输出样例&#xff1a; 4 24 #include <std…

【自然语言处理】第3部分:识别文本中的个人身份信息

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

linux系统 CentOS Tomcat 部署论坛

jdk安装命令&#xff1a;yum -y install java-1.8.0-openjdk-devel.x86_64 结尾上显示下图为成功 检查jdk环境是否配置成功命令&#xff1a;java -version或javac 显示版本 显示信息 mysql安装&#xff1a; 检查是否存mariadb数据库&#xff1a;rpm -qa | grep mariad 卸载ma…

基于JetCache整合实现一级、二级缓存方案(方案实现)

目录 一、整体方案说明 1.1 需求说明 1.2 整体方案实现组件结构图 二、Caffeine缓存实现 2.1 组件说明 2.2 组件结构图 2.3 组件Maven依赖 2.4 组件功能实现源码 2.4.1 CaffeineCacheManager扩展实现 2.4.2 CaffeineConfiguration配置类实现 2.4.3 涉及其他组件的类 …

Java EE 网络原理之HTTP 响应详解

文章目录 1. 认识"状态码"(status code)2. 通过 form 表单构造 HTTP 请求3. 通过 ajax 构造 HTTP 请求 1. 认识"状态码"(status code) 表示了这次请求对应的响应&#xff0c;是什么样的状态 &#xff08;成功&#xff0c;失败&#xff0c;其他的情况&…

Vue学习之第一、二章——Vue核心与组件化编程

第一章. Vue核心 1.1 Vue简介 1.1.1 官网 英文官网: https://vuejs.org/中文官网: https://cn.vuejs.org/ 1.1.2 Vue特点 遵循 MVVM 模式编码简洁, 体积小, 运行效率高, 适合移动/PC 端开发它本身只关注 UI, 也可以引入其它第三方库开发项目 1.2 初始Vue 这里可以参考&a…

Kali Linux如何启动SSH并在Windows系统远程连接

文章目录 1. 启动kali ssh 服务2. kali 安装cpolar 内网穿透3. 配置kali ssh公网地址4. 远程连接5. 固定连接SSH公网地址6. SSH固定地址连接测试 简单几步通过[cpolar 内网穿透](cpolar官网-安全的内网穿透工具 | 无需公网ip | 远程访问 | 搭建网站)软件实现ssh 远程连接kali! …

Java 基础学习第一弹

1. equels和的区别 equals方法用于比较对象的内容是否相等&#xff0c;可以根据自定义的逻辑来定义相等的条件&#xff0c;而操作符用于比较对象的引用是否相等&#xff0c;即它们是否指向同一块内存地址。equals方法是一个 实例方法&#xff0c;可以被所有的Java对象调用&…

文章解读与完整程序——《考虑“源-荷-储”协同互动的主动配电网优化调度研究》

摘要&#xff1a;伴随智能电网的建设和清洁能源的开发利用,配电网中的负荷类型呈现多元化发展,分布式电源、可控负荷、储能等资源的增加让单向潮流的传统配电网逐渐向双向潮流的主动配电网结构转变。在能源结构转变的同时,清洁能源自身的随机性和波动性给配电网带来了更大的调峰…

再获认可!棱镜七彩荣获ISC2023数字安全创新能力百强

12月27日&#xff0c;由北京经济和信息化局、通州区政府指导&#xff0c;中关村科技园区通州园管理委员会、ISC 平台主办&#xff0c;北京通州发展集团有限公司、赛迪顾问协办的数字安全技术创新论坛暨ISC 2023数字安全创新能力百强颁奖典礼在北京阳光国际会议中心成功举办&…

YOLOv8可视化:引入多种可视化CAM方法,为科研保驾护航

💡💡💡本文内容:调用pytorch下的CAM可视化库,支持十多种可视化方法,打开“黑盒”,让YOLOv8变得相对可解释性 收录 YOLOv8原创自研 https://blog.csdn.net/m0_63774211/category_12511737.html?spm=1001.2014.3001.5482 💡💡💡全网独家首发创新(原创),适…

实习知识整理10: 渲染默认地址以及实现渲染并选择其他地址

1. 渲染默认地址思路分析 &#xff08;1&#xff09;如果我们需要获取到默认地址可以通过userId从地址表中查找&#xff0c;因为从商品详情页点击购买按钮时&#xff0c;只传递商品的相关信息&#xff0c;所以我们就需要从session中获取用户的信息userId &#xff08;2&#…

INS量测更新

基础知识 1、ZK H X V 2、V ZK -HX 3、K Pk/k-1HT/(HPk/k-1HT R) 主要更新以下两个方程 4、Xk/k Xk/k-1 KV &#xff08;&#xff09; 5、Pk/k Pk/k-1 - KHPk/k-1 (I -KH)Pk/k-1; 剖析4和5两个方程&#xff0c;Xk/k-1,Pk/k-1时间更新已经更新完了&#x…

【软件工程大题】PAD图

常见题型:流程图转换为PAD图,伪代码转换为PAD图 PAD图基本要素 图解: (1) 选择分支,在P1和P2的左边标记 T or F (2) 如果只有if 没有else 省略即可(如不画P2),其余不变 (3) while C 的意思是 C为假的时候,跳出循环 until C 的意思是 C为真的时候,跳出循环 真题实战 题目…