新鲜出炉今天的周赛, 这场周赛难度没有很大, 但是当时做的时候第二题都错了两次, 直接影响了后面两题的心态…最后排到 200 多名, 只能拿参与奖了 😥 不过还是要赞下新功能一键填入示例, 终于不需要每次手动把测试用例输入进去了, 懒人福音啊…
[1455] 检查单词是否为句中其他单词的前缀
题目难度: 简单
题目描述
给你一个字符串 sentence 作为句子并指定检索词为 searchWord ,其中句子由若干用 单个空格 分隔的单词组成。
请你检查检索词 searchWord 是否为句子 sentence 中任意单词的前缀。
-
如果 searchWord 是某一个单词的前缀,则返回句子 sentence 中该单词所对应的下标(下标从 1 开始)。
-
如果 searchWord 是多个单词的前缀,则返回匹配的第一个单词的下标(最小下标)。
-
如果 searchWord 不是任何单词的前缀,则返回 -1 。
字符串 S 的 「前缀」是 S 的任何前导连续子字符串。 -
1 <= sentence.length <= 100
-
1 <= searchWord.length <= 10
-
sentence 由小写英文字母和空格组成。
-
searchWord 由小写英文字母组成。
-
前缀就是紧密附着于词根的语素,中间不能插入其它成分,并且它的位置是固定的——-位于词根之前。(引用自 前缀_百度百科 )
题目样例
示例 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”
输出
0
题目思考
- 题目很直白, 直接遍历匹配就行了吧…
解决方案
思路
- 遍历数组, 检查当前字符串的前缀是目标字符串即可
复杂度
- 时间复杂度 O(NM): N 表示单词个数, M 表示搜索字符串长度, 对于每个单词需要匹配一遍搜索字符串, 所以复杂度是 O(NM)
- 空间复杂度 O(1) 或 O(N): 如果利用 split 转成字符串列表就是 O(N), 如果直接对字符串本身进行处理就是 O(1). 下面的代码中用的是 O(N)方法, 更简洁一些, 感兴趣的同学可以自己试试 O(1)空间的做法, 就是碰到空格后重置下标即可
代码
Python 3
class Solution:
def isPrefixOfWord(self, sentence: str, searchWord: str) -> int:
a = sentence.split()
res = -1
for j, w in enumerate(a):
if len(w) < len(searchWord):
# 长度小于搜索字符串, 直接不满足要求
continue
for i in range(len(searchWord)):
if w[i] != searchWord[i]:
# 某一位不匹配了, 直接break
break
else:
# 都匹配, 就是要求的结果
# 注意要求的下标是从1开始的
return j + 1
return res
C++
class Solution {
public:
int isPrefixOfWord(string sentence, string searchWord) {
auto split = [](const string& s, const char delim = ' ') {
vector<string> res;
istringstream stream(s);
string substr;
while(getline(stream, substr, delim)) {
res.emplace_back(substr);
}
return res;
};
auto words = split(sentence);
auto searchSize = searchWord.size();
for (int i = 0; i < words.size(); ++i) {
if (words[i].size() >= searchSize && words[i].compare(0, searchSize, searchWord) == 0) {
return i + 1;
}
}
return -1;
}
};
[1456] 定长子串中元音的最大数目
题目难度: 中等
题目描述
给你字符串 s 和整数 k 。
请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。
英文中的 元音字母 为(a, e, i, o, u)。
- 1 <= s.length <= 10^5
- s 由小写英文字母组成
- 1 <= k <= s.length
题目样例
示例 1
输入
s = “abciiidef”, k = 3
输出
3
解释
子字符串 “iii” 包含 3 个元音字母。
示例 2
输入
s = “aeiou”, k = 2
输出
2
解释
任意长度为 2 的子字符串都包含 2 个元音字母。
示例 3
输入
s = “leetcode”, k = 3
输出
2
题目思考
- 固定长度为 k 的字符串, 你想到了什么方法?
解决方案
思路
- 滑动窗口
- 固定一个长度为 k 的窗口, 以及窗口内元音字母的数目
- 然后窗口每次向右滑动 1 格, 更新元音字母数目, 以及结果即可
- 特别注意边界条件和初始的滑动过程, 我当时做的时候就是在窗口初始化之后, 错误地让左边界从 1 开始移除, 结果错了两次的…
复杂度
- 时间复杂度 O(N): 每个字符只需要遍历两次
- 空间复杂度 O(1): 只使用了几个变量和常数空间的集合
代码
Python 3
class Solution:
def maxVowels(self, s: str, k: int) -> int:
v = {'a', 'e', 'i', 'o', 'u'}
res = 0
vcnt = 0
for i in range(k):
if s[i] in v:
# 先初始化计算[0,k]窗口的元音字母个数
vcnt += 1
res = vcnt
for b in range(len(s)):
e = b + k
if e >= len(s):
break
if s[e] in v:
# 更新新的右边界的值
vcnt += 1
if s[b] in v:
# 去除旧的左边界的值
vcnt -= 1
res = max(res, vcnt)
return res
C++
class Solution {
public:
int maxVowels(string s, int k) {
vector<int> isVowel(s.size(), 0);
unordered_set<char> d = {'a', 'e', 'i', 'o', 'u'};
for (int i = 0; i < s.size(); ++i) {
if (d.find(s[i]) != d.end()) {
isVowel[i] = 1;
}
}
if (s.size() <= k) {
return accumulate(isVowel.begin(), isVowel.end(), 0);
}
int num = accumulate(isVowel.begin(), isVowel.begin() + k, 0);
int res = num;
for (int i = k; i < s.size(); ++i) {
num += isVowel[i] - isVowel[i - k];
res = max(res, num);
}
return res;
}
};
[1457] 二叉树中的伪回文路径
题目难度: 中等
题目描述
给你一棵二叉树,每个节点的值为 1 到 9 。我们称二叉树中的一条路径是 「伪回文」的,当它满足:路径经过的所有节点值的排列中,存在一个回文序列。
请你返回从根到叶子节点的所有路径中 伪回文 路径的数目。
- 给定二叉树的节点数目在 1 到 10^5 之间。
- 节点值在 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] 。
示例 3
输入
[9]
输出
1
题目思考
- 先看数据规模, 数量是 10^5, O(N^2)算法很可能超时
- 分析题目, 回文序列有哪些性质, 如何利用这一点?
- 注意到每个节点的值只有 1~9, 你能想到什么优化思路吗?
解决方案
方案 1
思路
- 根据回文序列性质, 其中的每个字符的个数是有要求的, 最多只能有 1 个字符是奇数个(放在最中间), 其他字符都必须是偶数个. 当然也可以所有字符都是偶数个, 即对称中心是两字符的中间, 而不是某个字符本身.
- 利用这一点, 我们可以 dfs 遍历, 然后遍历过程中维护一个计数字典, key 是节点值, value 是对应的个数
- 然后遍历到叶子节点时, 统计当前计数字典是否满足回文序列的要求即可
- 可以额外增加一个变量 oddcnt 代表计数字典中数目为奇数的元素个数, 加速运算, 不需要每次在叶子节点处遍历所有计数字典统计奇数数目 key 的个数了
- 进一步优化: 可以将上面的计数字典和 oddcnt 转换成一个集合, 只存数目为奇数的元素即可, 然后每次该元素数目变化时, 就根据其新的个数将其从集合移入或移除
复杂度
- 时间复杂度 O(N): 每个节点只需要遍历一遍
- 空间复杂度 O(N): 递归的栈的消耗, 集合只使用了常数个元素, 因为节点的值是 1~9
代码
Python 3
class Solution:
def pseudoPalindromicPaths(self, root: TreeNode) -> int:
self.res = 0
if not root:
return 0
# 把集合移入和移出操作提取出来, 简化代码
def changeset(oddsets, v):
if v in oddsets:
oddsets.remove(v)
else:
oddsets.add(v)
def dfs(node, oddsets):
if not node.left and not node.right:
# 叶子节点, 且奇数个数的元素数目不大于1就是满足条件的路径
if len(oddsets) <= 1:
self.res += 1
return
if node.left:
# 注意每次改变集合状态后, 在dfs遍历完要恢复成原始状态, 避免对后面的遍历产生影响, 下同
changeset(oddsets, node.left.val)
dfs(node.left, oddsets)
changeset(oddsets, node.left.val)
if node.right:
changeset(oddsets, node.right.val)
dfs(node.right, oddsets)
changeset(oddsets, node.right.val)
dfs(root, {root.val})
return self.res
C++
class Solution {
public:
int pseudoPalindromicPaths (TreeNode* root) {
int res = 0;
unordered_set<int> oddsets;
auto changeset = [&oddsets](int val) {
if (oddsets.find(val) == oddsets.end()) {
oddsets.insert(val);
} else {
oddsets.erase(val);
}
};
function<void(TreeNode*)> dfs = [&](TreeNode* root) {
if (!root) {
return;
}
changeset(root->val);
if (root->left || root->right) {
dfs(root->left);
dfs(root->right);
} else {
if (oddsets.size() <= 1) {
++res;
}
}
changeset(root->val);
};
dfs(root);
return res;
}
};
方案 2
思路
- 回顾方案 1, 我们使用了一个集合来标记奇数数目元素
- 根据节点取值范围, 以及上面的 changeset 操作, 我们是不是也可以用位运算?
- 我们只需要取一个 10 位的数, 然后 changeset 就对应异或的过程, 这样相比方案 1 代码会简化很多
- 通常来说, 如果状态数不多的时候, 我们总是可以尝试把字典或者集合转成位运算, 这样时间和空间都会节省不少
复杂度
- 时间复杂度 O(N): 每个节点只需要遍历两遍
- 空间复杂度 O(N): 递归的栈的消耗
代码
Python 3
class Solution:
def pseudoPalindromicPaths(self, root: TreeNode) -> int:
self.res = 0
if not root:
return 0
def get1bit(n):
# 计算一个数字1的位数
res = 0
while n:
n &= n - 1
res += 1
return res
def dfs(node, mask):
if not node.left and not node.right:
# 叶子节点, 且奇数个数的元素数目不大于1就是满足条件的路径
if get1bit(mask) <= 1:
self.res += 1
return
if node.left:
dfs(node.left, mask ^ (1 << node.left.val))
if node.right:
dfs(node.right, mask ^ (1 << node.right.val))
dfs(root, 1 << root.val)
return self.res
C++
class Solution {
public:
int pseudoPalindromicPaths (TreeNode* root) {
int res = 0;
auto countBits = [](int value) {
int count = 0;
while (value) {
++count;
value &= value - 1;
}
return count;
};
function<void(TreeNode*, int)> countPath = [&](TreeNode* root, int mask) {
if (!root) {
return;
}
mask ^= 1 << (root->val);
if (root->left || root->right) {
countPath(root->left, mask);
countPath(root->right, mask);
} else if (countBits(mask) <= 1) {
++res;
}
};
countPath(root, 0);
return res;
}
};
[1458] 两个子序列的最大点积
题目难度: 困难
题目描述
给你两个数组 nums1 和 nums2 。
请你返回 nums1 和 nums2 中两个长度相同的 非空 子序列的最大点积。
数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5] 是 [1,2,3,4,5] 的一个子序列而 [1,5,3] 不是。
- 1 <= nums1.length, nums2.length <= 500
- -1000 <= nums1[i], nums2[i] <= 100
定义 a = [a1, a2,…, an] 和 b = [b1, b2,…, bn] 的点积为:
题目样例
示例 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 。
示例 3
输入
nums1 = [-1,-1], nums2 = [1,1]
输出
-1
题目思考
- 观察数据规模, O(N^2)复杂度肯定可以, O(N^3)复杂度不太确定
- 可以尝试动态规划的思路吗?
- 如何进行预处理?
解决方案
思路
- 这种求子序列的问题可以先尝试能否用动态规划解决, 此题也是可以的, 不过需要一些优化
- 设 dp[i,j]为使用 nums1 的第 i 个元素和 nums2 的第 j 个元素所能得到的最大序列
- 那么不难看出:
dp[i,j] = max(nums1[i]*nums2[j], nums1[i]*nums2[j]+mx)
, mx 是max(dp[ii,jj]) (ii<i 且 jj<j)
, 可能为负数, 这时候就只用 i 和 j 自身的乘积了 - 如果我们不做任何优化的话, 也即每次计算 dp[i,j]的时候都计算一遍 mx, 这个复杂度就达到了 O(NMNM), 显然是不能接受的, 所以我们必须进行预处理
- 注意到 mx 的下标取值范围是小于当前 i 和 j 的所有组合, 所以我们完全可以利用这一点
- 额外引入两个数组 premx 和 curmx, 其长度为 nums2 的长度, premx[j]和 curmx[j]分别表示 i-1,j 及之前的最大 dp 值, 以及当前正在计算的 i,j 及之前的最大 dp 值
- 那么自然上面的 mx 就是 premx[j-1]了
- 而 curmx[j] 则可以根据当前的 dp 值以及上一个 curmx[j-1]进行更新
- 然后每次循环到新的 i 时就将 curmx 的值复制到 premx 即可
- 这样我们就做到了只需要两重循环计算出结果
复杂度
- 时间复杂度 O(MN): 只需要二重遍历, M 和 N 分别表示 nums1 和 nums2 的长度
- 空间复杂度 O(MN): 使用了大小为 MN 的 dp 数组
代码
Python 3
class Solution:
def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
dp = collections.defaultdict(int)
n1, n2 = len(nums1), len(nums2)
res = -float('inf')
premx = [-float('inf')] * n2
curmx = [-float('inf')] * n2
for i in range(n1):
# 复制premx的值
premx = curmx[::]
for j in range(n2):
v = nums1[i] * nums2[j]
# dp转移公式, 注意考虑j-1<0和premx[j - 1] < 0的特殊情况
dp[i,
j] = v if j - 1 < 0 or premx[j - 1] < 0 else v + premx[j -
1]
res = max(res, dp[i, j])
# 更新curmx
curmx[j] = max(curmx[j], dp[i, j])
if j - 1 >= 0:
curmx[j] = max(curmx[j], curmx[j - 1])
return res
C++
class Solution {
public:
int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
int res = INT_MIN;
int n1 = nums1.size();
int n2 = nums2.size();
vector<int> curmx(n2, INT_MIN);
for (int i = 0; i < n1; ++i) {
auto premx = curmx;
for (int j = 0; j < n2; ++j) {
int value = nums1[i] * nums2[j];
if (j > 0 && premx[j - 1] > 0) {
value += premx[j - 1];
}
res = max(res, value);
curmx[j] = max(curmx[j], value);
if (j > 0) {
curmx[j] = max(curmx[j], curmx[j - 1]);
}
}
}
return res;
}
};
大家可以在下面这些地方找到我~😊
我的公众号: 每日精选算法题, 欢迎大家扫码关注~😊