【动态规划:子数组/子串系列】单词拆分 环绕字符串中唯⼀的子字符串
单词拆分(medium)
139. 单词拆分
给你一个字符串 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
提示:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s
和wordDict[i]
仅有小写英文字母组成wordDict
中的所有字符串 互不相同
解题思路
状态表示和状态转移方程的解析如下图所示:
class Solution {
public:bool wordBreak(string s, vector<string>& wordDict) {// 优化:把字典放到hash中unordered_set<string> hash;for(const auto &e : wordDict)hash.insert(e);// 创建dp表,dp[i]表示从[0,i]区间内是否可以拼成一个字符串int n = s.size();vector<bool> dp(n + 1); // 多开一个虚拟位置,防止越界dp[0] = true; // 给虚拟位置初始化,保证后面填表正确s = ' ' + s; // 使原始字符串的下标统⼀+1,让我们无需在后面访问s的时候去控制下标// 填表for(int i = 1; i <= n; ++i){for(int j = i; j >= 1; --j){if(dp[j - 1] == true && hash.count(s.substr(j, i - j + 1))){dp[i] = true;break; // 只要找到了就break}}}return dp[n];}
};
8、环绕字符串中唯⼀的子字符串(medium)
467. 环绕字符串中唯一的子字符串
定义字符串 base
为一个 "abcdefghijklmnopqrstuvwxyz"
无限环绕的字符串,所以 base
看起来是这样的:
"...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd...."
.
给你一个字符串 s
,请你统计并返回 s
中有多少 不同非空子串 也在 base
中出现。
示例 1:
输入:s = "a"
输出:1
解释:字符串 s 的子字符串 "a" 在 base 中出现。
示例 2:
输入:s = "cac"
输出:2
解释:字符串 s 有两个子字符串 ("a", "c") 在 base 中出现。
示例 3:
输入:s = "zab"
输出:6
解释:字符串 s 有六个子字符串 ("z", "a", "b", "za", "ab", and "zab") 在 base 中出现。
提示:
1 <= s.length <= 105
- s 由小写英文字母组成
解题思路
这道题首先要先看懂题,其实就是要求这个字符串 base 的数量,而 base 其实就是 acbd…… 连续的子串,不能是 acd 这种只能是连续的 abcd,一定要先弄懂题目要求,弄懂了才能做!依旧是按照我们分析的几步来:
- 状态表示
- 还是一样,动态规划这类题还是按照 “经验+题目要求”,比如 以某处为结尾然后……的情况,这道题也是如此。
- 所以我们可以设定 dp[i] 表示以 i 结尾的所有子串中,是 base 子串的数量
- 状态转移方程
- 既然是要求以 i 结尾的所有子串的可能,那么我们按长度来解析:
- 子串长度为1:也就是当前字符本身,那么肯定是算一种 base 字符串的,所以 此时 dp 值为 1
- 子串长度大于1:这个时候就要根据之前的状态来得到当前的状态了!
- 因为此时子串长度大于 1,那么肯定会有 i-1 位置字符的参与,那么其实就转化为了 dp[i - 1] 的值了,因为它表示以 i-1 字符结尾的所有字符串中 base 字符串的出现次数。
- 如果此时当前字符能和以 i-1 结尾的所有 base 字符串拼接起来,那么得到的出现此时就是 dp[i - 1],那么如何才能将 i 处字符和之前的字符串符合拼接起来呢❓❓❓
- 其实就是题目要求,要连续的 abcd…… 这种形式,所以我们只需要判断是否
s[i] - 1 == s[i - 1]
即可,但是要注意的是有可能 s[i] 是 ‘a’,题目说到这是一个环绕 base 字符串,也就是说首尾相连,那么要让它们拼接起来,还得满足s[i] == ‘a’ && s[i - 1] == ‘z’
,把这种情况考虑到了才是完整的情况!
- 其实就是题目要求,要连续的 abcd…… 这种形式,所以我们只需要判断是否
- 也就是说,当
s[i] - 1 == s[i - 1]
或者s[i] == ‘a’ && s[i - 1] == ‘z’
有一个满足的时候,d[i] 才等于 dp[i - 1]
- 所以状态转移方程:dp[i] = 1 + dp[i - 1],其中 dp[i - 1] 是需要满足条件才能加上的,而 1 是必加项。
- 既然是要求以 i 结尾的所有子串的可能,那么我们按长度来解析:
- 初始化
- 因为状态转移方程涉及到 i-1,所以我们要对 dp[0] 进行初始化,如果只有两个字符,比如 “ab”,此时 dp[i - 1] 是成立的,需要加上,那么 dp[i] = 1 + dp[i - 1] 就得等于 2 才对,所以 dp[0] 初始化为 1。
- 但其实还有更妙的做法,因为我们上面不是得到了 dp[i] 的推导中,肯定要加上一吗,那么我们只要将整个 dp 数组初始化为 1,往后在求 dp[i] 的时候我们 直接使用 +=,比如 dp[i] += dp[i - 1] 的时候,其中 dp[i] 本身已经初始化为了 1,就不用再去加上 1 了,当然不这么做也是可以的!
- 填表顺序
- 显而易见,填表顺序「从左往右」
- 返回值
- 返回值并不是简单的累加上 dp 表的所有值,因为有可能字符串 s 中有重复的字符!
- 比如说【abczab】此时对于第一个字符 ‘b’ 来说,它有出现过子串 ‘b’、“ab”,所以 dp 值为 2;而对于第二个字符 ‘b’ 来说,它出现过 ‘b’、“ab” 和 “zab”,dp 值就为 3,它其中就包括了第一个字符 ‘b’ 所出现的子串,那么就重复了,此时要是我们去累加整个 dp 表的值,那么就多加了 2,那就错了!
- 可以发现,我们只需要取出现相同字符中,dp 值大的那个,它出现的子串肯定也包括了其它相同字符出现的子串!所以我们 去重的方法就是取这些相同字符中 dp 值大的那个!
- 所以我们可以使用哈希表,对于此题直接使用一个 26 空间大小的数组即可,因为只出现小写字母!然后将每个字符映射到对应的位置上去,并且 hash[i] 更新的是这个相同字符的最大 dp 值!
- 最后我们再去这个哈希表中累加出现的这些最大 dp 值,返回最终累加结果即可!
- 返回值并不是简单的累加上 dp 表的所有值,因为有可能字符串 s 中有重复的字符!
class Solution {
public:int findSubstringInWraproundString(string s) {// 创建dp表,dp[i]表示以i结尾的所有子串中的base的数量int n = s.size();vector<int> dp(n, 1); // 都初始化为1// 填表for(int i = 1; i < n; ++i){// 注意判断这里是s[i]-1而不是s[i]+1,不要搞错了!if((s[i] - 1 == s[i - 1]) || (s[i - 1] == 'z' && s[i] == 'a'))dp[i] += dp[i - 1];}// 返回base出现的总和之前的处理:// 注意在字符串s中可能有重复的字符,这样子的话两个相同字符的dp值可能是不一样的// 我们只取大的那个,因为dp值大的那个包含了dp值小的那个// 所以我们要做映射,将出现的字符的最大dp值映射到hash数组中int hash[26] = {0};for(int i = 0; i < n; ++i)hash[s[i] - 'a'] = max(hash[s[i] - 'a'], dp[i]);// 累加结果并且返回int ret = 0;for(int i = 0; i < 26; ++i)ret += hash[i];return ret;}
};