日练算法-单词拆分

2023-10-22 08:10
文章标签 算法 单词 拆分 日练

本文主要是介绍日练算法-单词拆分,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1 题目

1.1 题目描述

给你一个字符串 str 和一个字符串列表 wordDict 作为字典。
请你判断是否可以利用字典中出现的单词拼接出 str 。原题链接:https://leetcode-cn.com/problems/word-break/

解题注意:

1.拆分时可以重复使用字典中的单词,可以假设字典中没有重复的单词;
2.不要求字典中出现的单词都被使用

示例

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。

注意,你可以重复使用字典中的单词

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

1.2 题目分析及解答

解题思路1-遍历

解答该题同样可以使用暴力的遍历方式,只是这两种遍历较之前有些变化。如下:

1.深度优先遍历搜索(DFS)
2.广度优先遍历搜索(BFS)

DFS方法

首先,针对深度遍历搜索方法,其定义为:是沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索(百度百科)。具体思路如下图:

思路:每次截取一个子串,判断他是否存在于字典中,如果不存在于字典中,继续截取更长的子串……如果存在于字典中,然后递归拆分剩下的子串,这是一个递归的过程。上面的执行过程我们可以把它看做是一棵n叉树的DFS遍历

package list;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;public class IDEA_1 {public static void main(String[] args){//数组可以随机定义,此处选取LeetCode数据String str = "leetcode";List<String> wordDict = Arrays.asList("leet", "code");System.out.println("单词拆分结果:"+ wordBreak(str,wordDict));}public static boolean wordBreak(String s, List<String> wordDict) {return dfs(s, wordDict, new HashSet<>(), 0);}//start表示的是从字符串s的哪个位置开始public static boolean dfs(String s, List<String> wordDict, Set<Integer> indexSet, int start) {//字符串都拆分完了,返回trueif (start == s.length())return true;for (int i = start + 1; i <= s.length(); i++) {//如果已经判断过了,就直接跳过,防止重复判断if (indexSet.contains(i))continue;//截取子串,判断是否是在字典中if (wordDict.contains(s.substring(start, i))) {if (dfs(s, wordDict, indexSet, i))return true;//标记为已判断过indexSet.add(i);}}return false;}
}

BFS

对于BFS遍历算法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。如下图

BFS算法只需要使用一个队列记录每一层需要记录的值即可。BFS中在截取的时候,如果截取的子串存在于字典中,我们就要记录截取的位置,到下一层的时候就从这个位置的下一个继续截取,代码如下:

package list;
import java.util.*;public class IDEA_2 {public static void main(String[] args){//数组可以随机定义,此处选取LeetCode数据String str = "leetcode";List<String> wordDict = Arrays.asList("leet", "code");System.out.println("单词拆分结果:"+ wordBreak(str,wordDict));}public static boolean wordBreak(String s, List<String> wordDict) {//这里为了提高效率,把list转化为set,因为set的查找效率要比list高Set<String> setDict = new HashSet<>(wordDict);//记录当前层开始遍历字符串s的位置Queue<Integer> queue = new LinkedList<>();queue.add(0);int length = s.length();//记录访问过的位置,减少重复判断boolean[] visited = new boolean[length];while (!queue.isEmpty()) {int index = queue.poll();//如果字符串都遍历完了,直接返回trueif (index == length)return true;//如果被访问过,则跳过if (visited[index])continue;//标记为访问过visited[index] = true;for (int i = index + 1; i <= length; i++) {if (setDict.contains(s.substring(index, i))) {queue.add(i);}}}return false;}
}

以上两种方法可以解决问题,但是执行效率较低,leetcode和牛客等网站编译不易通过。

解题思路2-动态规划

1、确定dp数组以及下标的含义——这里dp[i]:字符串长度为i,dp[i]=true表示可以拆分为一个或多个在字典中出现的单词
2、确定递推公式,if(dp[j]=true && set.contains(s.substring(j,i)),才会出现dp[i]=true
3、dp数组初始化,dp[0]=true
4、确定遍历顺序,双层for循环遍历,i从0到s.length(遍历所有字符串长度),j从长度0到i
5、举例推导dp数组
6、最后返回dp[s.length()]

我们定义dp[i]表示字符串的前i个字符经过拆分是否都存在于字典wordDict中。如果要求dp[i],我们需要往前截取k个字符,判断子串[i-k+1,i]是否存在于字典wordDict中,并且前面[0,i-k]子串拆分的子串也是否都存在于wordDict中,如下图所示:

前面 [0,i-k] 子串拆分的子串是否都存在于 wordDict 中只需要判断 dp[i-k] 即可,而子串 [i-k+1,i] 是否存在于字典 wordDict 中需要查找,所以动态规划的递推公式很容易列出来

dp[i]=dp[i-k]&&dict.contains(s.substring(i-k, i));
package list;
import java.util.Arrays;
import java.util.List;public class IDEA_3 {public static void main(String[] args){//数组可以随机定义,此处选取LeetCode数据String str = "leetcode";List<String> wordDict = Arrays.asList("leet", "code");System.out.println("单词拆分结果:"+ wordBreak(str,wordDict));}/**1.定义:boolean dp[i] -> 用字典中的单词能否前i个字符串2.递推公式:i > j如果前j个字符串能够用字典中的词汇拼出(这是前提) && 从j到i这段字符串能够在字典中找到 -> 说明前i个字符串能够用字典中的词汇拼出if(dp[j] == true && wordDict.contains(s.substring(j,i))) -> dp[i] = true 3.初始化:dp[0] = true 空字符串不用字典中的词汇也能拼出 无为而治 同时之后的结果也是以dp[0]为基础 如果为false之后的结果也都会为false4.遍历顺序:这道题理论上先遍历字符串(背包)还是先遍历字典(物品)的结果都是一样的只不过因为涉及到子串问题的特殊性 单词的顺序会进行打乱 不会按照字典中的顺序出现因此为了方便可以使用先遍历背包再遍历物品的方式如果反过来的话 需要先用容器将物品进行存储 较为麻烦*/public static boolean wordBreak(String s, List<String> dict) {boolean[] dp = new boolean[s.length() + 1];for (int i = 1; i <= s.length(); i++) {//枚举k的值for (int k = 0; k <= i; k++) {//如果往前截取全部字符串,我们直接判断子串[0,i-1]//是否存在于字典wordDict中即可if (k == i) {if (dict.contains(s.substring(0, i))) {dp[i] = true;continue;}}//递推公式dp[i] = dp[i - k] && dict.contains(s.substring(i - k, i));//如果dp[i]为true,说明前i个字符串结果拆解可以让他的所有子串//都存在于字典wordDict中,直接终止内层循环,不用再计算dp[i]了。if (dp[i]) {break;}}}return dp[s.length()];}}

正在上传…重新上传取消

优化: 将 wordDict 构建为一个 Set 能提高 contains 方法的匹配效率,于是:

package list;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;public class IDEA_4 {public static void main(String[] args){//数组可以随机定义,此处选取LeetCode数据String str = "leetcode";List<String> wordDict = Arrays.asList("leet", "code");System.out.println("单词拆分结果:"+ wordBreak(str,wordDict));}public static boolean wordBreak(String s, List<String> wordDict) {boolean[] dp = new boolean[s.length() + 1];dp[0] = true;//优化之处Set<String> set = new HashSet<>(wordDict);for (int r = 1; r < s.length() + 1; r++) {for (int l = 0; l < r; l++) {if (dp[l] == true && set.contains(s.substring(l, r))) {dp[r] = true;}}}return dp[s.length()];}

还有大佬优化的方法,此处给出代码逻辑,供大家思考:

public boolean wordBreak(String s, List<String> wordDict) {//转化为是否可以用 wordDict 中的词组合成 s,完全背包问题//有顺序的完全背包问题,先遍历target,再遍历背包//dp[i]表示:字符串的前i个字符是否可以由背包中的单词组成int len = s.length();boolean[] dp = new boolean[len + 1];//初始化ddp[0] = true;// 如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。// 所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。//这里从1开始遍历是因为substring左开右闭for(int i = 1; i <= len; i++){//倒着遍历,考虑到单词长度可能远小于target长度,倒着遍历效率更高for(int j = i - 1; j >= 0; j--){//如果dp[j]为false,已经不符合题意了,继续下一个比较if(dp[j] == false) continue;if(wordDict.contains(s.substring(j, i)) && dp[j]){//如果为true,则必定为true,直接breakdp[i] = true;break;}} }return dp[len];
}

    ​    ​

参考:

1.https://leetcode-cn.com/problems/word-break/

这篇关于日练算法-单词拆分的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/260187

相关文章

基于Python实现一个图片拆分工具

《基于Python实现一个图片拆分工具》这篇文章主要为大家详细介绍了如何基于Python实现一个图片拆分工具,可以根据需要的行数和列数进行拆分,感兴趣的小伙伴可以跟随小编一起学习一下... 简单介绍先自己选择输入的图片,默认是输出到项目文件夹中,可以自己选择其他的文件夹,选择需要拆分的行数和列数,可以通过

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

使用雪花算法产生id导致前端精度缺失问题解决方案

《使用雪花算法产生id导致前端精度缺失问题解决方案》雪花算法由Twitter提出,设计目的是生成唯一的、递增的ID,下面:本文主要介绍使用雪花算法产生id导致前端精度缺失问题的解决方案,文中通过代... 目录一、问题根源二、解决方案1. 全局配置Jackson序列化规则2. 实体类必须使用Long封装类3.

Springboot实现推荐系统的协同过滤算法

《Springboot实现推荐系统的协同过滤算法》协同过滤算法是一种在推荐系统中广泛使用的算法,用于预测用户对物品(如商品、电影、音乐等)的偏好,从而实现个性化推荐,下面给大家介绍Springboot... 目录前言基本原理 算法分类 计算方法应用场景 代码实现 前言协同过滤算法(Collaborativ

基于Python打造一个智能单词管理神器

《基于Python打造一个智能单词管理神器》这篇文章主要为大家详细介绍了如何使用Python打造一个智能单词管理神器,从查询到导出的一站式解决,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 项目概述:为什么需要这个工具2. 环境搭建与快速入门2.1 环境要求2.2 首次运行配置3. 核心功能使用指

openCV中KNN算法的实现

《openCV中KNN算法的实现》KNN算法是一种简单且常用的分类算法,本文主要介绍了openCV中KNN算法的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录KNN算法流程使用OpenCV实现KNNOpenCV 是一个开源的跨平台计算机视觉库,它提供了各

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

Python实现合并与拆分多个PDF文档中的指定页

《Python实现合并与拆分多个PDF文档中的指定页》这篇文章主要为大家详细介绍了如何使用Python实现将多个PDF文档中的指定页合并生成新的PDF以及拆分PDF,感兴趣的小伙伴可以参考一下... 安装所需要的库pip install PyPDF2 -i https://pypi.tuna.tsingh