这是我参与11月更文挑战的第5天,活动详情查看2021最后一次更文挑战
链接
周赛一共四道题,今天先带来前两道题的解析。
题目
分割+双指针
解析
一次遍历中把符合部分要求的片段分隔出来,然后对每个片段进行子字符串的计算。对每个片段的计算使用了双指针法,即使用双指针left和right从left=0时开始遍历right,用字典vowelCountDict(python中用defaultdict最方便)对元音字母个数进行记录,并用zeroCount变量记录尚未出现的元音字母(当字母计数由0变成1时zeroCount-1)。当zeroCount为0时,计算right指针到元音片段结尾的距离,此即为以left为开头的符合要求的子字符串个数。然后left+=1,判断right位置是否合法(vowelCountDict是否有项从1变成了0)并更新right位置、加上新的子字符串个数;注意在里层循环中也要判断right位置是否超过末尾。
代码
我的解法
class Solution:
def countVowelSubstrings(self, word: str) -> int:
vowels = ['a', 'e', 'i', 'o', 'u']
p = 0
self.vowelStrCount, self.word = 0, word
while p < len(word):
if word[p] in vowels:
nextP = p + 1
while nextP < len(word) and word[nextP] in vowels:
nextP += 1
if nextP - p >= 5:
self.queryVowelStrs(p, nextP)
p = nextP + 1
else:
p += 1
return self.vowelStrCount
def queryVowelStrs(self, p, nextP):
vowelCount = defaultdict(int)
zeroCount = 5
left, right = p, p
while right < nextP:
while right < nextP and zeroCount > 0:
vowelCount[self.word[right]] += 1
if vowelCount[self.word[right]] == 1:
zeroCount -= 1
right += 1
if zeroCount != 0:
break
while zeroCount == 0:
self.vowelStrCount += nextP - right + 1
vowelCount[self.word[left]] -= 1
if vowelCount[self.word[left]] == 0:
zeroCount += 1
left += 1
return
更简洁的分割+双指针
解析
思路和上述相同。Go语言的strings.FieldsFunc配合strings.ContainsRune能很方便的在string中分隔出符合要求的片段,然后也是双指针+用来充当字典使用的数组实现对字串进行O(n)的遍历。不同的是统计符合要求字串个数的视角是以每个right指针来看,left指针最多能到哪个位置即可以添加片段开始到left指针位置的个数。
代码
func countVowelSubstrings(word string) (ans int) {
for _, s := range strings.FieldsFunc(word, func(r rune) bool { return !strings.ContainsRune("aeiou", r) }) { // 分割出仅包含元音的字符串
cnt := ['v']int{}
l := 0
for _, ch := range s {
cnt[ch]++
for cnt[s[l]] > 1 { // 双指针,仅当该元音个数不止一个时才移动左指针
cnt[s[l]]--
l++
}
if cnt['a'] > 0 && cnt['e'] > 0 && cnt['i'] > 0 && cnt['o'] > 0 && cnt['u'] > 0 { // 必须包含全部五种元音
ans += l + 1
}
}
}
return
}
另外的暴力解法
别的解法基本都是用两个变量实现O(n^2)的子字符串遍历。遍历中可以借助字典判断元音字母个数、并在遇到非元音字母时剪枝break(见这个回答);也可以利用python自带的set判断是否符合只有元音字母这一条件(见这里),但是这种的复杂度就超过了O(n^2)(不确定是否是O(n^3),这跟Python中获取字符串set的复杂度有关) 。
一个超时的解法
思路
对每个位置的字符,若其是元音字符,则根据其位置、判断其会出现在每个长度的子字符串中多少次:该长度的子字符串开头能出现的最右位置min(n-j, i) 减去能出现的最左位置 max(0, i-j+1)。但是由于数据有10^5但是要对每个位置和每个长度都遍历一遍需要O(n^2)的复杂度所以就超时了。
代码
class Solution:
def countVowels(self, word: str) -> int:
n, ans = len(word), 0
for i in range(0, n):
if word[i] in ['a', 'e', 'i', 'o', 'u']:
for j in range(1, n+1):
ans += min(n-j, i) - max(0, i-j+1) + 1
return ans
乘法优化O(n)的加法
解析
将上面那个遍历加每个长度的加法过程用一个乘法过程替代:考虑所有包含word[i]的word[l...r],当且仅当0<=l<=i以及i<=r<=len(word)-1时成立,所以包括word[i]的word[l...r]有(i+1)*(len(word)-i)个,可以以O(n)的复杂度求出结果。
代码
class Solution:
def countVowels(self, word: str) -> int:
n, ans = len(word), 0
for i in range(0, n):
if word[i] in ['a', 'e', 'i', 'o', 'u']:
ans += (i+1) * (n-i)
return ans
基础DP
解析
对所有i+1个以word[i]结尾的子字符串来说,word[i]若不为元音则和所有i个以word[i-1]结尾的子字符串中元音个数相同,word[i]若为元音则在所有i+1个子字符串中都多出了一个元音字母,所以设置一个prev变量表示以word[i-1]结尾的子字符串中的元音个数然后遍历一遍字符串根据情况更新其值,再求出所有位置上prev的总和即为结果。
代码
class Solution:
def countVowels(self, word: str) -> int:
n, prev, ans = len(word), 0, 0
for i in range(0, n):
if word[i] in ['a', 'e', 'i', 'o', 'u']:
prev += i+1
ans += prev
return ans