当前位置: 首页 > news >正文

leetcode 动态规划(单词拆分)

139.单词拆分
力扣题目链接(opens new window)

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。

你可以假设字典中没有重复的单词。

示例 1:

输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:

输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
示例 3:

输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
#算法公开课
《代码随想录》算法视频公开课 (opens new window):你的背包如何装满?| LeetCode:139.单词拆分 (opens new window),相信结合视频再看本篇题解,更有助于大家对本题的理解。

#思路
看到这道题目的时候,大家应该回想起我们之前讲解回溯法专题的时候,讲过的一道题目回溯算法:分割回文串 (opens new window),就是枚举字符串的所有分割情况。

回溯算法:分割回文串 (opens new window):是枚举分割后的所有子串,判断是否回文。

本道是枚举分割所有字符串,判断是否在字典里出现过。

那么这里我也给出回溯法C++代码:

class Solution {
private:bool backtracking (const string& s, const unordered_set<string>& wordSet, int startIndex) {if (startIndex >= s.size()) {return true;}for (int i = startIndex; i < s.size(); i++) {string word = s.substr(startIndex, i - startIndex + 1);if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1)) {return true;}}return false;}
public:bool wordBreak(string s, vector<string>& wordDict) {unordered_set<string> wordSet(wordDict.begin(), wordDict.end());return backtracking(s, wordSet, 0);}
};

这个时间复杂度其实也是:O(2^n)。只不过对于上面那个超时测试用例优化效果特别明显。

这个代码就可以AC了,当然回溯算法不是本题的主菜,背包才是!

#背包问题
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。

拆分时可以重复使用字典中的单词,说明就是一个完全背包!

动规五部曲分析如下:

确定dp数组以及下标的含义
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。

确定递推公式
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。

所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。

dp数组如何初始化
从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。

那么dp[0]有没有意义呢?

dp[0]表示如果字符串为空的话,说明出现在字典里。

但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。

下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。

确定遍历顺序
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。

还要讨论两层for循环的前后顺序。

如果求组合数就是外层for循环遍历物品,内层for遍历背包。

如果求排列数就是外层for遍历背包,内层for循环遍历物品。

我在这里做一个总结:

求组合数:动态规划:518.零钱兑换II (opens new window)求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)、动态规划:70. 爬楼梯进阶版(完全背包) (opens new window)求最小数:动态规划:322. 零钱兑换 (opens new window)、动态规划:279.完全平方数(opens new window)

而本题其实我们求的是排列数,为什么呢。 拿 s = “applepenapple”, wordDict = [“apple”, “pen”] 举例。

“apple”, “pen” 是物品,那么我们要求 物品的组合一定是 “apple” + “pen” + “apple” 才能组成 “applepenapple”。

“apple” + “apple” + “pen” 或者 “pen” + “apple” + “apple” 是不可以的,那么我们就是强调物品之间顺序。

所以说,本题一定是 先遍历 背包,再遍历物品。

举例推导dp[i]
以输入: s = “leetcode”, wordDict = [“leet”, “code”]为例,dp状态如图:
在这里插入图片描述
dp[s.size()]就是最终结果。

动规五部曲分析完毕,C++代码如下:

class Solution {
public:bool wordBreak(string s, vector<string>& wordDict) {unordered_set<string> wordSet(wordDict.begin(), wordDict.end());vector<bool> dp(s.size() + 1, false);dp[0] = true;for (int i = 1; i <= s.size(); i++) {   // 遍历背包for (int j = 0; j < i; j++) {       // 遍历物品string word = s.substr(j, i - j); //substr(起始位置,截取的个数)if (wordSet.find(word) != wordSet.end() && dp[j]) {dp[i] = true;}}}return dp[s.size()];}
};

时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度)
空间复杂度:O(n)

拓展

关于遍历顺序,再给大家讲一下为什么 先遍历物品再遍历背包不行。

这里可以给出先遍历物品再遍历背包的代码:

class Solution {
public:
bool wordBreak(string s, vector& wordDict) {
unordered_set wordSet(wordDict.begin(), wordDict.end());
vector dp(s.size() + 1, false);
dp[0] = true;
for (int j = 0; j < wordDict.size(); j++) { // 物品
for (int i = wordDict[j].size(); i <= s.size(); i++) { // 背包
string word = s.substr(i - wordDict[j].size(), wordDict[j].size());
// cout << word << endl;
if ( word == wordDict[j] && dp[i - wordDict[j].size()]) {
dp[i] = true;
}
// for (int k = 0; k <= s.size(); k++) cout << dp[k] << " "; //这里打印 dp数组的情况
// cout << endl;
}
}
return dp[s.size()];

}

};
使用用例:s = “applepenapple”, wordDict = [“apple”, “pen”],对应的dp数组状态如下:

在这里插入图片描述
最后dp[s.size()] = 0 即 dp[13] = 0 ,而不是1,因为先用 “apple” 去遍历的时候,dp[8]并没有被赋值为1 (还没用"pen"),所以 dp[13]也不能变成1。

除非是先用 “apple” 遍历一遍,再用 “pen” 遍历,此时 dp[8]已经是1,最后再用 “apple” 去遍历,dp[13]才能是1。

如果大家对这里不理解,建议可以把我上面给的代码,拿去力扣上跑一跑,把dp数组打印出来,对着递推公式一步一步去看,思路就清晰了。

Python:
回溯

class Solution:def backtracking(self, s: str, wordSet: set[str], startIndex: int) -> bool:# 边界情况:已经遍历到字符串末尾,返回Trueif startIndex >= len(s):return True# 遍历所有可能的拆分位置for i in range(startIndex, len(s)):word = s[startIndex:i + 1]  # 截取子串if word in wordSet and self.backtracking(s, wordSet, i + 1):# 如果截取的子串在字典中,并且后续部分也可以被拆分成单词,返回Truereturn True# 无法进行有效拆分,返回Falsereturn Falsedef wordBreak(self, s: str, wordDict: List[str]) -> bool:wordSet = set(wordDict)  # 转换为哈希集合,提高查找效率return self.backtracking(s, wordSet, 0)

DP(版本一)

class Solution:def wordBreak(self, s: str, wordDict: List[str]) -> bool:wordSet = set(wordDict)n = len(s)dp = [False] * (n + 1)  # dp[i] 表示字符串的前 i 个字符是否可以被拆分成单词dp[0] = True  # 初始状态,空字符串可以被拆分成单词for i in range(1, n + 1): # 遍历背包for j in range(i): # 遍历单词if dp[j] and s[j:i] in wordSet:dp[i] = True  # 如果 s[0:j] 可以被拆分成单词,并且 s[j:i] 在单词集合中存在,则 s[0:i] 可以被拆分成单词breakreturn dp[n]

DP(版本二)

class Solution:def wordBreak(self, s: str, wordDict: List[str]) -> bool:dp = [False]*(len(s) + 1)dp[0] = True# 遍历背包for j in range(1, len(s) + 1):# 遍历单词for word in wordDict:if j >= len(word):dp[j] = dp[j] or (dp[j - len(word)] and word == s[j - len(word):j])return dp[len(s)]

相关文章:

  • 面向对象的三大特性
  • Kali安装Xrdp结合内网穿透实现无公网ip远程访问系统桌面
  • 单例模式的八种写法、单例和并发的关系
  • 打印日期c++
  • Java获取文件的后缀名称
  • netcore html to pdf
  • 代码随想录算法训练营第32天|122.买卖股票的最佳时机II 55. 跳跃游戏 45.跳跃游戏II
  • 基于反卷积方法的重大突破:结构光系统中的测量误差降低3倍
  • 设计模式之并发特定场景下的设计模式 Two-phase Termination(两阶段终止)模式
  • Linux中常使用的命令之ls、cd、pwd、mkdir、rmdir
  • 数字后端设计实现之自动化useful skew技术(Concurrent Clock Data)
  • Linux - No space left on device
  • 后端程序员开发win小工具(未完待续)
  • JS浏览器的默认行为及阻止行为,阻止右键菜单、阻止超链接跳转、阻止拖拽事件
  • k8s的yaml文件中的kind类型都有哪些?(详述版Part1/2)
  • [js高手之路]搞清楚面向对象,必须要理解对象在创建过程中的内存表示
  • 【Linux系统编程】快速查找errno错误码信息
  • 【划重点】MySQL技术内幕:InnoDB存储引擎
  • Android路由框架AnnoRouter:使用Java接口来定义路由跳转
  • CSS居中完全指南——构建CSS居中决策树
  • go语言学习初探(一)
  • Java 最常见的 200+ 面试题:面试必备
  • React系列之 Redux 架构模式
  • RxJS: 简单入门
  • Spring Boot MyBatis配置多种数据库
  • 阿里云购买磁盘后挂载
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • 半理解系列--Promise的进化史
  • 区块链分支循环
  • 区块链将重新定义世界
  • 深入浅出webpack学习(1)--核心概念
  • 没有任何编程基础可以直接学习python语言吗?学会后能够做什么? ...
  • ​HTTP与HTTPS:网络通信的安全卫士
  • # 数据结构
  • #单片机(TB6600驱动42步进电机)
  • $refs 、$nextTic、动态组件、name的使用
  • (iPhone/iPad开发)在UIWebView中自定义菜单栏
  • (分布式缓存)Redis分片集群
  • (十五)devops持续集成开发——jenkins流水线构建策略配置及触发器的使用
  • (四)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (中等) HDU 4370 0 or 1,建模+Dijkstra。
  • (转)iOS字体
  • ***监测系统的构建(chkrootkit )
  • ..thread“main“ com.fasterxml.jackson.databind.JsonMappingException: Jackson version is too old 2.3.1
  • .bat批处理(一):@echo off
  • .Net Core 中间件验签
  • .NET Framework 服务实现监控可观测性最佳实践
  • .net Signalr 使用笔记
  • .net/c# memcached 获取所有缓存键(keys)
  • .NET企业级应用架构设计系列之应用服务器
  • /3GB和/USERVA开关
  • /usr/bin/python: can't decompress data; zlib not available 的异常处理
  • [ 云计算 | AWS ] 对比分析:Amazon SNS 与 SQS 消息服务的异同与选择
  • [Android开源]EasySharedPreferences:优雅的进行SharedPreferences数据存储操作
  • [asp.net core]project.json(2)