周日收拾行李,准备返校,就没时间参加了,听说是手速题,还是挺简单的,最后一题两种假设 DP 状态的方法,第一种方法笔记简单,容易想到。第二种方法,如果做过类似的题目,比如 LeetCode 上的第 72 题 “编辑距离”,那就很好做了。
第一题:字符串分割 + 模拟(手动分割 或者 C++ stringstream 分割)。
第二题:滑动窗口。
第三题:DFS。
第四题:动态规划 DP,有两种状态假设方法。
详细题解如下。
1.检查单词是否为句中其他单词的前缀
AC代码(手动实现字符串分割 C++)
AC代码(利用stringstream进行分割 C++)
2. 定长子串中元音的最大数目
AC代码(C++)
3.二叉树中的伪回文路径
AC代码(C++)
4.两个子序列的最大点积
AC代码(状态一、O(n ^ 3) C++)
AC代码(状态二、O(n ^ 2) C++)
LeetCode第190场周赛地址:
https://leetcode-cn.com/contest/weekly-contest-190/
https://leetcode-cn.com/problems/check-if-a-word-occurs-as-a-prefix-of-any-word-in-a-sentence/
给你一个字符串 sentence 作为句子并指定检索词为 searchWord ,其中句子由若干用 单个空格 分隔的单词组成。
请你检查检索词 searchWord 是否为句子 sentence 中任意单词的前缀。
- 如果 searchWord 是某一个单词的前缀,则返回句子 sentence 中该单词所对应的下标(下标从 1 开始)。
- 如果 searchWord 是多个单词的前缀,则返回匹配的第一个单词的下标(最小下标)。
- 如果 searchWord 不是任何单词的前缀,则返回 -1 。
字符串 S 的 「前缀」是 S 的任何前导连续子字符串。
示例 1:
输入:sentence = "i love eating burger", searchWord = "burg" 输出:4 解释:"burg" 是 "burger" 的前缀,而 "burger" 是句子中第 4 个单词。
示例 2:
输入:sentence = "this problem is an easy problem", searchWord = "pro" 输出:2 解释:"pro" 是 "problem" 的前缀,而 "problem" 是句子中第 2 个也是第 6 个单词,但是应该返回最小下标 2 。
示例 3:
输入:sentence = "i am tired", searchWord = "you" 输出:-1 解释:"you" 不是句子中任何单词的前缀。
提示:
1 <= sentence.length <= 100
1 <= searchWord.length <= 10
sentence 由小写英文字母和空格组成。
searchWord 由小写英文字母组成。
前缀就是紧密附着于词根的语素,中间不能插入其它成分,并且它的位置是固定的——-位于词根之前。(引用自 前缀_百度百科 )
根据题意,其实主要是将 字符串 分割得到各个单词,然后枚举各个单词的前缀是不是 searchWord 即可。
大概的时间复杂度,分割字符串需要 O(n),判断是不是前缀,需要 O(m),所以总时间复杂度是 O(n * m),其中 n 是 sentence 的长度,m 是 searchWord 的长度。
那么分割 字符串时,由于 C++ 没有类似 Java 或 Python 中的 split 函数,所以相当于要自己实现。
一般实现,可以手动实现,遍历 字符串,当是 空格时,说明得到了一个 单词。
或者可以利用 stringstream,即将 sentence 作为 stringstream,然后输入(那么此时由于 输入是会按照 空格 进行分割),所以也就得到了各个单词(不断的读取)
判断是不是前缀,那就很简单, 也就是判断这个单词的前面 m 个字符,是不是和 searchWord 完全一样即可。
class Solution {
public:
int isPrefixOfWord(string sT, string sW) {
sT += " "; // 最后加上一个 空格,为了方便下面的处理
string cur = "";
int n = sW.size();
int idx = 1; // 记录是第几个 单词
for(auto c : sT)
{
if(c == ' ')
{
int m = cur.size();
if(n <= m)
{
bool flag = true;
for(int i = 0;i < n; ++i)
{
if(cur[i] != sW[i]) flag = false;
}
if(flag) return idx;
}
++idx;
cur = "";
}
else
{
cur += c;
}
}
return -1;
}
};
class Solution {
public:
int isPrefixOfWord(string sentence, string searchWord) {
stringstream ssin(sentence); // 利用 stringstream
string word;
int m = searchWord.size();
for(int i = 1; ssin >> word; ++i)
{
if(word.size() < m) continue;
bool flag = true;
for(int j = 0;j < m && flag; ++j) // 判断是不是前缀
{
if(word[j] != searchWord[j]) flag = false;
}
if(flag) return i;
}
return -1;
}
};
https://leetcode-cn.com/problems/maximum-number-of-vowels-in-a-substring-of-given-length/
给你字符串 s 和整数 k 。
请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。
英文中的 元音字母 为(a, e, i, o, u)。
示例 1:
输入:s = "abciiidef", k = 3 输出:3 解释:子字符串 "iii" 包含 3 个元音字母。
示例 2:
输入:s = "aeiou", k = 2 输出:2 解释:任意长度为 2 的子字符串都包含 2 个元音字母。
示例 3:
输入:s = "leetcode", k = 3 输出:2 解释:"lee"、"eet" 和 "ode" 都包含 2 个元音字母。
提示:
1 <= s.length <= 10^5
s
由小写英文字母组成1 <= k <= s.length
怎么说呢,一看到题目,就知道是一个 固定长度范围的,那就想到了用 滑动窗口,也就是我们一开始取了 k 长度,然后下一个 k 长度,其实就是,区间右边往后移动一个,区间左边往后移动一个(相当于原来区间,加上新的,去掉一开始的,得到新区间)
所以这样子的,利用滑动窗口的时间复杂度是 O(n)
class Solution {
public:
bool check(char c) // 判断是不是元音字母
{
if(c == 'a') return true;
else if(c == 'e') return true;
else if(c == 'i') return true;
else if(c == 'o') return true;
else if(c == 'u') return true;
return false;
}
int maxVowels(string s, int k) {
int n = s.size();
int ans = 0;
int cur = 0;
for(int i = 0;i < k; ++i) // 一开始的区间
{
if(check(s[i])) ++cur;
}
ans = cur;
for(int i = k; i < n; ++i) // 然后不断加入新的点,去掉最前面的点,得到新区间
{
if(check(s[i])) ++cur;
if(check(s[i - k])) --cur;
ans = max(ans, cur);
}
return ans;
}
};
https://leetcode-cn.com/problems/pseudo-palindromic-paths-in-a-binary-tree/
给你一棵二叉树,每个节点的值为 1 到 9 。我们称二叉树中的一条路径是 「伪回文」的,当它满足:路径经过的所有节点值的排列中,存在一个回文序列。
请你返回从根到叶子节点的所有路径中 伪回文 路径的数目。
示例 1:
【示例有图,具体看链接】 输入:root = [2,3,1,3,1,null,1] 输出:2 解释:上图为给定的二叉树。总共有 3 条从根到叶子的路径:红色路径 [2,3,3] ,绿色路径 [2,1,1] 和路径 [2,3,1] 。 在这些路径中,只有红色和绿色的路径是伪回文路径,因为红色路径 [2,3,3] 存在回文排列 [3,2,3] ,绿色路径 [2,1,1] 存在回文排列 [1,2,1] 。
示例 2:
【示例有图,具体看链接】 输入:root = [2,1,1,1,3,null,null,null,null,null,1] 输出:1 解释:上图为给定二叉树。总共有 3 条从根到叶子的路径:绿色路径 [2,1,1] ,路径 [2,1,3,1] 和路径 [2,1] 。 这些路径中只有绿色路径是伪回文路径,因为 [2,1,1] 存在回文排列 [1,2,1] 。
提示:
- 给定二叉树的节点数目在
1
到10^5
之间。- 节点值在
1
到9
之间。
其实就是,我们要统计,从 根节点 到任意一个叶节点的情况。
伪回文串,只要求是一个排列,也就是说,只要 1 - 9 这 9 个数字各自出现的次数,可以排列出一种 回文串即可。那么根据回文串,我们可以知道,是对称的,所以 出现次数应该是 偶数,除了 可以最中间的那一个数 是 奇数出现。因此,只要 1 -9 这 9 个数各自的出现次数中,奇数的情况 <= 1 即可是 伪回文。
那么剩下的就是 DFS,注意,应该是 DFS + 回溯,因为我们要统计 从 根节点到 另一个 节点的 出现次数,比如当了 a 节点,那么继续往下 dfs 那没问题,如果 从 a 节点返回,去到 和 a 同层的其他节点开始,那么 a 节点这个 出现次数 就要去掉。
所以是 dfs + 回溯,时间复杂度是,需要遍历每一个节点,到了叶节点的时候,需要去枚举 1- 9 每个数字各自的出现次数,所以总的时间复杂度是 O(9 * n)
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int cnt[10];
int ans;
void dfs(TreeNode* root)
{
if(root == nullptr) return;
++cnt[root->val]; // 把该节点的值,保存下来
if(root->left == nullptr && root->right == nullptr) // 如果这个节点是 叶节点,那么就说明这是一条路径了,就要判断 是不是伪回文。
{
int c = 0;
for(int i = 1;i <= 9; ++i)
{
if(cnt[i] % 2 == 1) ++c;
}
if(c <= 1) ++ans; // 奇数的出现次数 <= 1,说明是可以排列得到一个回文串的。
}
else // 如果节点不是叶节点,那就要继续 dfs 下去,直到叶节点。
{
dfs(root->left);
dfs(root->right);
}
--cnt[root->val]; // 最后是 回溯
}
int pseudoPalindromicPaths (TreeNode* root) {
ans = 0;
dfs(root);
return ans;
}
};
https://leetcode-cn.com/problems/max-dot-product-of-two-subsequences/
给你两个数组 nums1 和 nums2 。
请你返回 nums1 和 nums2 中两个长度相同的 非空 子序列的最大点积。
数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5] 是 [1,2,3,4,5] 的一个子序列而 [1,5,3] 不是。
示例 1:
输入:nums1 = [2,1,-2,5], nums2 = [3,0,-6] 输出:18 解释:从 nums1 中得到子序列 [2,-2] ,从 nums2 中得到子序列 [3,-6] 。 它们的点积为 (2*3 + (-2)*(-6)) = 18 。
示例 2:
输入:nums1 = [3,-2], nums2 = [2,-6,7] 输出:21 解释:从 nums1 中得到子序列 [3] ,从 nums2 中得到子序列 [7] 。 它们的点积为 (3*7) = 21 。
提示:
1 <= nums1.length, nums2.length <= 500
-1000 <= nums1[i], nums2[i] <= 100
方法一、O(n ^ 4) --> O(n ^ 3) 的状态设置
状态设置:dp[ i ][ j ] 为 选择 A 的第 i 个 和 选择 B 的第 j 个,的最大点积和
那么我们就需要 去找到 所有 dp[ 0 ~ i - 1][0 ~ j - 1] 中的最大值,这样子转移过来,所以转移方程是
dp[ i ][ j ] = max(dp[ 0 ~ i - 1][0 ~ j - 1]) + A[ i ] * B[ j ]
那么初始值就是 dp[ 0 ][ all j ] = dp[ all i ][ 0 ] = 0,也就是啥也不选的时候。
因为最后我们要求是,非空,也就是至少要选一个,因此最后的答案是枚举,所有 dp[ 1 ...][ 1....] 至少要选择一个的中的最大值即可。
那么这里有一个问题,在转移的时候,dp[ 0 ~ i - 1][0 ~ j - 1] 最大值,如果直接枚举,那就需要时间复杂度是 O(n ^ 4) 这样子会超时。
那么我们分析,当 枚举 i 和 j 的时候,我们是要得到它们之前的最大值。
比如 一开始 i = 3,j = 3的时候,那么j = 4 的时候,原本已经有了 dp[ 0 1 2 ][ 0 1 2] 的最大值 mx 了,那么此时当 j = 4 的时候,需要多 dp[ 0 1 2][3],就需要 计算 mx 和这几个 中的最大值,因此,每一个 计算 j 的时候,需要计算 dp[ 0~ i-1][ j -1],所以其实只需要 多遍历 一次 i 即可,所以总的时间复杂度为 O(n ^ 3),不会超时。
方法二、O(n ^ 2) 的状态设置
此时我们假设 dp[ i ][ j ] 表示,A 的前 i 个,和 B 的前 j 个 中,选出了某一些组成的最大点积和(没要求一定要选 第 i 个 和 第 j 个)
那么此时,我们的转移过来,有四种可能:
1、没有选择 A 的 第 i 个,没有选择 B 的第 j 个,说明此时的最大点积和,应该就是 前 i - 1 和 前 j - 1 组合的,也就是 dp[ i - 1][ j - 1]
2、没有选择 A 的 第 i 个,选择了 B 的 第 j 个,那么此时,最大点积和,应该是 前 i - 1 和 前 j 个的,但是此时我们要注意的一点,因为我们设置的 状态 dp[ i ][ j ] 不需要要求 一定要选 第 i 个 和 第 j 个。那么此时,我们是相当于 前 i - 1 和 前 j 个,同时要求是选择了 第 j 个。
但是注意了,我们知道 dp[ i - 1 ][ j ] 是前 i - 1 和 前 j 个,包括两个状态,一定选择 第 j 个,和不一定要求选择其。那么 dp[ i - 1 ][ j ] 的范围更大,那么我们用其来更新,只是将 考虑范围变大,那么对于找最优解来说,是可以的(只要不是把范围缩小),因此我们可以用 dp[ i - 1 ][ j ] 当作来更新 转移方程。
3、选择 A 的 第 i 个,没有选择 B 的 第 j 个。类似 2,那么就是 dp[ i ][ j -1]
4、选择 A 的 第 i 个,选择 B 的 第 j 个,那么就是,dp[ i - 1 ][ j -1] + A[ i ] * B[ j ]
(注意的是,1 情况,其实被 2 和 3 情况包括在其中的,所以可以看成是,只有 2 3 4 三个情况)
那么就是上面的四种情况的中的最大值 当作 dp[ i ][ j ]。
初始化:也就是根据我们定义的状态来进行判断,也就是 dp[ 0 ][ all j ] = dp[ all i ][ 0 ] = 0。即其中一个没有选的时候,点积和是 0
最后答案:要注意,我们要求,是非空,也就是至少要选择,那么如果直接找 dp[ i ][ j ] 所有中的最大值,由于我们定义的状态,不需要一定选择,可能出现,最大值是 0,即没有选择任何一个。那么这就不符合题意
因此,我们要找的答案,不是直接 dp[ i ][ j ],而应该是,转移 4 情况,也就是,至少保证有选择,那么取 4 情况中的所有最大值才是最后答案。
这样子,我们只需要枚举 i 和 j 即可,时间复杂度是 O(n ^ 2)
const int INF = 5e7 + 50;
class Solution {
public:
int maxDotProduct(vector& nums1, vector& nums2) {
int n = nums1.size(), m = nums2.size();
vector > dp(n + 1, vector (m + 1, -INF));
dp[0][0] = 0;
for(int i = 1;i <= n; ++i)
{
int mx = 0;
for(int j = 1;j <= m; ++j)
{
dp[i][j] = max(dp[i][j], mx + nums1[i - 1] * nums2[j - 1]);
for(int ii = 0;ii < i; ++ii)
mx = max(mx, dp[ii][j]);
}
}
int ans = -INF;
for(int i = 1;i <= n; ++i)
{
for(int j = 1;j <= m; ++j)
{
ans = max(ans, dp[i][j]);
}
}
return ans;
}
};
const int INF = 5e7 + 50;
class Solution {
public:
int maxDotProduct(vector& nums1, vector& nums2) {
int n = nums1.size(), m = nums2.size();
vector > dp(n + 1, vector(m + 1, -INF));
// 初始化
for(int i = 0;i <= n; ++i) dp[i][0] = 0;
for(int j = 0;j <= m; ++j) dp[0][j] = 0;
int ans = -INF;
for(int i = 1;i <= n; ++i) // 开始转移
{
for(int j = 1;j <= m; ++j)
{
dp[i][j] = max(dp[i - 1][j - 1], max(dp[i - 1][j], dp[i][j - 1]));
int t = dp[i - 1][j - 1] + nums1[i - 1] * nums2[j - 1]; // 第 4 中情况
ans = max(ans, t); // 答案是第四种情况下的所有最大值
dp[i][j] = max(dp[i][j], t);
}
}
return ans;
}
};