5416. 检查单词是否为句中其他单词的前缀
- 知识点:枚举,std::string::substr
- 时间复杂度:O(n*m);n 为 sentence.size(), m 为 searchWord.size()。
本题的关键是如何判断词的开始位置,根据题目中句子由若干用单个空格分隔的单词组成的描述,词首应该符合下述两个特点中的一个:
- 如果一个位置是句首,那么这个位置也肯定是词首。
- 如果一个位置的前一个字符是空格,那么这个位置是词首。
确定词首位置后,可以通过 std::string::substr 函数取出子串,然后判断是否与 searchWord 是否相同即可。
class Solution {
public:
int isPrefixOfWord(string sentence, string searchWord) {
int cnt = 0;
for(int i = 0; i < sentence.size(); i++) {
// i 是第一个字符,或者 i-1 为空格。
if (i == 0 || sentence[i-1] == ' ') {
cnt++;
if(sentence.substr(i, searchWord.size()) == searchWord) {
return cnt;
}
}
}
return -1;
}
};
5417. 定长子串中元音的最大数目
- 知识点:滑动窗口
- 时间复杂度:O(n);n 为 s.size()。
从 0 到 n-1 枚举 i 作为滑动窗口的右端点。初始时,滑动窗口的左端点 j 为 0,当窗口的大小,即 i-j+1 > k 时,向左移动左端点,即 ++j。
对于每个确定的窗口 [j, i],需要维护元音的数量,当一次移动后,只需O(1)检查s[i+1],s[j+1]就可以获得窗口[j+1, i+1] 中的元音数量。
class Solution {
public:
int check(char c) {
if(c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
return 1;
}
return 0;
}
int maxVowels(string s, int k) {
int cnt = 0, anw = 0;
//移动右端点 i
for(int i = 0; i < s.size(); i++) {
cnt += check(s[i]);
//窗口超过 k 了,
if(i >= k) {
//从窗口中移除s[j], j = i-k
cnt -= check(s[i-k]);
}
// 更新下最大值
anw = max(anw, cnt);
}
return anw;
}
};
5418. 二叉树中的伪回文路径
- 知识点:深度优先遍历
- 时间复杂度:O(n);n 为树中结点数量。
如果集合中所有元素的某种排列是回文串,那么集合中最多有一种元素出现了奇数次。 那么题目就变成了统计所有根节点到叶子结点的路径中,符合上述特点的路径数量。 可以使用DFS完成上述统计,在DFS过程中使用一个计数器统计根节点到当前栈顶结点的路径中每个元素的出现次数:
- 当一个新节点放到栈顶时,更新计数器。
- 当一个结点从栈中弹出时,更新计数器。
- 特别的,当新节点为叶子节点时,检查计数器并更新答案。
计数器可以用 std::map, std::unordered_map 实现。鉴于元素取值范围为 [0,9],也可以直接使用数组实现。
/**
* 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 check(int *cnt) {
int odd = 0;
for(int i = 0; i <= 9; i++) {
if(cnt[i]&1) {
odd++;
}
}
if(odd <= 1) {
return 1;
}
return 0;
}
int dfs(TreeNode *root, int *cnt) {
if(root == nullptr) {
return 0;
}
cnt[root->val] ++;
if(root->left == nullptr && root->right == nullptr) {
int anw = check(cnt);
cnt[root->val]--;
return anw;
}
int anw = 0;
if(root->left != nullptr) {
anw += dfs(root->left, cnt);
}
if(root->right != nullptr) {
anw += dfs(root->right, cnt);
}
cnt[root->val] --;
return anw;
}
int pseudoPalindromicPaths (TreeNode* root) {
int cnt[10] = {0};
return dfs(root, cnt);
}
};
5419. 两个子序列的最大点积
- 知识点:动态规划
- 时间复杂度:O(n*m);n,m 分别为两个序列的长度。
动态规划题目一般要先想好两个问题:
- 状态如何定义。
- 状态之间如何转移。
对于该题,最终目标是在分别两个序列中选取等长且非空的子序列,使得两个子序列的点积最大。 即,在 nums1 的前 n 个数中,在 nums2 的前 m 个数字中分别选取等长且非空的子序列, 使其点积最大。 推而广之,我们可以将问题表示为,在nums1的前 x (x <= n)个数字中,在nums2的前y(y <= m)个数字中,分别选取等长且非空的子序列, 使其点积最大。 为了方便,我们用 f(x, y) 表示子问题的最优方案的点积。 当(x, y) = (n,m) 时,f(x,y) 就是最终答案。
状态转移主要是分析状态之间的关联或差异,利用小问题的解高效的构造大问题的解。 来思考下该题状态如何转移,该题的特点是小问题总是为大问题的前缀,总是可以向小问题中追加数字得到一个大问题。 设 nx = nums1[x],ny = nums2[y]。 f(x,y) 可能由以下几个状态转移得到:
- 向 f(x-1, y-1) 追加 nx,ny 获得 f(x, y)。
- 向 f(x, y-1) 追加 ny 获得 f(x, y)。
- 向 f(x-1, y) 追加 nx 获得 f(x,y)。
当然,也可以同时追加多个数字,由更小的问题获得 f(x, y),但这本质上还是通过上述三种子问题间接转移过来的。 那么,为何f(x-1,y-1) 不能用 f(x-1, y) 或者 f(x, y-1) 间接转移过来呢?因为在求解过程中要考虑nx 和 ny 在对应位置的情况。 总结一下,该题的状态方程如下:
class Solution {
public:
int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
int dp[501][501];
for(int i = 0; i <= nums1.size(); i++) {
for(int j = 0; j <= nums2.size(); j++) {
dp[i][j] = -1000*1000*500;
}
}
for(int i = 1; i <= nums1.size(); i++) {
for(int j = 1; j <= nums2.size(); j++) {
int a = nums1[i-1];
int b = nums2[j-1];
dp[i][j] = max(dp[i][j], a*b);
dp[i][j] = max(dp[i][j], dp[i-1][j-1] + a*b);
dp[i][j] = max(dp[i][j], dp[i-1][j-1]);
dp[i][j] = max(dp[i][j], dp[i-1][j]);
dp[i][j] = max(dp[i][j], dp[i][j-1]);
}
}
return dp[nums1.size()][nums2.size()];
}
};