力扣第190场周赛
作为我为数不多能全做出来的一次周赛,简单分析一下题目和思路吧。我还是太菜了!
本次周赛链接位于:weekly-contest-190
这次周赛跟往常一样,包括分数和难度递增的四道题:
1· 检查单词是否为句中其他单词的前缀
本题要求给一个字符串 sentence 作为句子并指定检索词为 searchWord ,其中句子由若干用 单个空格 分隔的单词组成
请你检查检索词 searchWord 是否为句子 sentence 中任意单词的前缀。
如果 searchWord 是某一个单词的前缀,则返回句子 sentence 中该单词所对应的下标(下标从 1 开始)。
如果 searchWord 是多个单词的前缀,则返回匹配的第一个单词的下标(最小下标)。
如果 searchWord 不是任何单词的前缀,则返回 -1 。
字符串 S 的 「前缀」是 S 的任何前导连续子字符串。
根据题意,我们需要判断给定的searchWord是sentence中第几个单词的前缀,返回编号,如果有多个就返回最小的,如果不是任何一个单词的下标就返回-1.
分析
本题是签到题,还是很简单的,不过对C++选手可能涉及到字符串空格分割。
C++代码如下:
bool isFront(string tem, string s) {
if (tem.size() < s.size()) return false;
int i = 0;
while (i < s.size()) {
if (tem[i] == s[i])++i;
else return false;
}
return true;
}
int isPrefixOfWord(string sentence, string searchWord) {
stringstream ss(sentence);
vector<string>vs;
string tmp;
int i = 0;
while (ss >> tmp) {
if (isFront(tmp, searchWord)) return i+1;
else ++i;
}
return -1;
}
这里采用stringstream的方式从sentence中依次读取被空格分隔的每个单词,并通过计数器记录这是第几个。注意题目给的例子中返回值下标从1开始,而不是0,所以从零计数要返回i+1。遍历所有单词都没有跳出并返回的那就是不存在,返回-1即可。
bool isFront(string tem, string s) 函数用于判断字符串 s 是否为字符串 tem 的前缀。首先s的长度如果超过tem那显然不可能是前缀;然后依次遍历s和tem,判断从头开始的字符是不是一一对应相同的,是的话就返回true,中间出现不同的情况就返回false。
这道题很简单,不过C++分词部分相比于其他语言的现成方法稍麻烦一点。stringstream的方式还是很好用的。
2· 定长子串中元音的最大数目
给你字符串 s 和整数 k 。
请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。
英文中的 元音字母 为(a, e, i, o, u)。
如:
输入:s = “abciiidef”, k = 3
输出:3
解释:子字符串 “iii” 包含 3 个元音字母。
分析
本题规定了k是小于等于s长度的,因此特殊情况就不讨论了。k>s.size()的情况也很简单,就找到s包含的全部元音字符数量就行了。不过不是题目要求的部分。
题目要求长度为k的子串所包含的最大元音字母数,其实就是遍历全部的长度为k的子串,并记录最大的元音字母数即可。当然也不需要真的对每个字串从头查找,这是比较经典的滑动窗口的应用。
C++代码如下:
int maxVowels(string s, int k) {
int count = 0, maxC = 0, start = -1,end=start+k,len=s.size();
set<char> v{ 'a','e','i','o','u' };
for (int i = 0; i < k; ++i) {
if (v.count(s[i])) ++count;
}
maxC = count;
++start;
++end;
while (end < len) {
if (v.count(s[end])) ++count;
if (v.count(s[start])) --count;
if (count > maxC) maxC = count;
++start;
++end;
}
return maxC;
}
首先集合v记录所有元音字母,避免一个个if判断,而且只有5个,对计算量增加不大。
这里用start和end标记滑窗,[start+1, end]是当前滑窗内容,每次start加一,end也加一,表示右侧新增一个而左侧减少一个,维护count作为当前窗口内的元音字母数量,并在窗口移动过程中记录最大值。
也是比较简单的题目。不过自己还是花了一些时间,主要是滑窗的初始化、计算、停止条件之类的,看来最近还是题做少了,害!
3· 二叉树中的伪回文路径
给你一棵二叉树,每个节点的值为 1 到 9 。我们称二叉树中的一条路径是 「伪回文」的,当它满足:路径经过的所有节点值的排列中,存在一个回文序列。
请你返回从根到叶子节点的所有路径中 伪回文 路径的数目。
本题要求一颗二叉树的“伪回文”路径数目,伪回文定义为路径上的值存在一个回文的排列,而且节点值有限定在1-9之间。所以其实对于每条路径,统计出现的每个数字的数量,如果全是偶数或有一个奇数那一定可以排列成回文的序列;如果超过1个数字出现次数为奇数就排不成了,所以考虑遍历所有路径并统计每个数字出现数量,遇到叶节点就判断,如果满足数字的数量最多只有1个奇数的条件,就在结果上加一。
这道题我采用了递归的方法,遍历每条路径。
C++代码如下:
bool canPali(vector<int>&count) {
int n = 0;
for (auto c : count) {
if (c % 2 == 1)++n;
if (n >= 2) return false;
}
if (n <= 1) return true;
}
void pathRec(TreeNode* root, vector<int>&count,int*ans) {
if (root == nullptr)
return;
count[root->val]++;
if (root->left == nullptr&& root->right == nullptr) {
if (canPali(count)) (*ans)++;
}
else {
if (root->left) {
pathRec(root->left, count,ans);
count[root->left->val]--;
}
if (root->right) {
pathRec(root->right, count, ans);
count[root->right->val]--;
}
}
}
int pseudoPalindromicPaths(TreeNode* root) {
vector<int>count(10, 0);
int ans = 0;
pathRec(root, count, &ans);
return ans;
}
count数组是一个长度为10的数组,分别记录0-9出现的次数,count[i] 就是i出现的次数。之所以长度为10,是为了方便的直接用 node->val作为index访问,如果长度为9下标范围是0-8还需要人为-1.尽管长度是10但是由于按照题意0这个数没出现过一直是0所以不影响能否回文的判断。
bool canPali(vector&count)函数用于判断count数组是不是能形成回文序列。就是简单粗暴的遍历并记录出现次数为奇数的数字有几个。
void pathRec(TreeNode* root, vector&count,int*ans)是核心的递归函数,对于传入的树节点root,首先记录其数值放入count,然后判断是不是叶节点(左右子节点都为空),如果是那说明找到了一条路径,判断是不是伪回文,是的话对整形指针ans指向的整数加一。这里没有用全局变量而是传入指针,因为之前听说LeetCode多个样例测试时全局变量可能出错(也就是全局变量定义时一定要初始化为0吧我猜),但其实效果是一样的。
如果不是叶节点,就分别将左右节点(若存在)加入路径并继续查找下去。这里要注意传入的count是引用,如果遍历了左子树那左子节点就加入count了,并且显然这不属于右子树的路径,所以遍历完左边,要先将左子节点的值踢出去在遍历右子树。
4· 两个子序列的最大点积
给你两个数组 nums1 和 nums2 。
请你返回 nums1 和 nums2 中两个长度相同的 非空 子序列的最大点积。
数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5] 是 [1,2,3,4,5] 的一个子序列而 [1,5,3] 不是。
本题给定两个数组,分别选取等长的子序列并求两个子序列点积(也就是对应位置相乘再相加)的最大值。
分析
这道题一开始想了递归的路子,但是超时了,反过来一想,其实动态规划就可以搞定。根据点积定义,我们可以将题目转化为两个数组选取若干对点,这些点的乘积加起来最大,那么每个点都可以讨论要不要选取。
本题思路可以看做是二维的dp,两个数组长度分别是m和n,设计m*n的二维数组dp,其中
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示第一个数组0-i和第二个数组0-j两个子数组能得到的非空子序列最大点积。
那么对于
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j],它的最大值一定出自一下几种情况:
1·
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j],也就是新增加的nums1[i]不足以加入所选取的子序列;
2·
d
p
[
i
]
[
j
−
1
]
dp[i][j-1]
dp[i][j−1],也就是新增加的nums2[j]不足以加入所选取的子序列;
3·
d
p
[
i
−
1
]
[
j
−
1
]
dp[i-1][j-1]
dp[i−1][j−1],新加入的两个数乘积是负的还比
d
p
[
i
−
1
]
[
j
−
1
]
dp[i-1][j-1]
dp[i−1][j−1]小,加进去反而减小了总的点积
4·
n
u
m
s
1
[
i
]
∗
n
u
m
s
2
[
j
]
nums1[i] * nums2[j]
nums1[i]∗nums2[j],前面的是负的,加上反而变小,所以单独取新加入的2个数
5·
d
p
[
i
−
1
]
[
j
−
1
]
+
n
u
m
s
1
[
i
]
∗
n
u
m
s
2
[
j
]
dp[i-1][j-1] + nums1[i] * nums2[j]
dp[i−1][j−1]+nums1[i]∗nums2[j],新加入一对点有增加。
所以
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]取上述情况最大值即可。
C++代码如下:
int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>>dp(nums1.size(),vector<int>(nums2.size()));
dp[0][0] = nums1[0] * nums2[0];
int maxC = dp[0][0];
for (int i = 1; i < nums1.size(); ++i)dp[i][0] = max(dp[i - 1][0], nums1[i] * nums2[0]);
for (int j = 1; j < nums2.size(); ++j)dp[0][j] = max(dp[0][j-1], nums1[0] * nums2[j]);
for (int i = 1; i < nums1.size(); ++i) {
for (int j = 1; j < nums2.size(); ++j) {
int tmp = nums1[i] * nums2[j];
dp[i][j] = max(max(dp[i - 1][j - 1]+tmp ,max(tmp, dp[i - 1][j - 1])), max(dp[i - 1][j], dp[i][j - 1]));
if (dp[i][j] > maxC) maxC = dp[i][j];
}
}
return maxC;
}
初始化的时候对于i=0的情况,意味着只考虑数组1 的第一个数,那能得到的最大点积,就是这个数跟另一个数组讨论范围内所有能得到的最大乘积(此时子序列长度只能为1)。j=0的情况同理。之后就按照上述情况逐一更新 d p [ i ] [ j ] dp[i][j] dp[i][j]并记录最大值即可。