青训营X豆包MarsCode - 395| 豆包MarsCode AI 刷题
氦嗨氦,之前都滑,嗯滑,硬滑,range从中等到困难,已经三期了,都解析的差不多啦~
今天刷到的这题,眼前一亮,其实也很经典,精髓就在于如何破解题目类型和信息。你学费了吗?
经典的“其实也很经典”,无效表达了属于是
不说俏皮话了,题号 395 - 小J的好串问题。
题目原文
小M有一个只包含
'0'和'1'的字符串,她想知道有多少个子串是“好串”。一个子串如果是好串,那么它的所有前缀中,'0'的数量严格大于'1'的数量。
题目抽象分析
如何破局?
- 一个子串被定义为“好串”的条件是:该子串的所有前缀中,
'0'的数量严格大于'1'的数量。 - “前缀?“ = ”信息“!
题目类型
不如来看看“前缀和”:
核心思想是通过预处理,将数组或字符串的前缀信息存储起来,从而在后续的查询和计算中能够快速获取某个区间的信息。
- 前缀和的特性在于它能够快速计算出任意区间的某种累积值。
整体思路
为了找到”好串“,
问题的关键,在于关键的问题
- 通过计算前缀和来跟踪每个位置上
'0'和'1'的数量差异。 - then 使用一个栈来找出每个位置之后的第一个违反“好串”规则的位置。
- 那么,通过计算每个可能的起始位置到其下一个违反规则的位置之间的子串数量,即可approach答案。
代码分块解析
前缀和计算
步骤一:初始化前缀和数组
- 创建一个长度为
n + 1的数组prefix,用来存储前缀和。
步骤二:计算前缀和
- 遍历字符串,根据字符是
'0'还是'1'来更新前缀和数组。
栈辅助查找
步骤三:初始化栈和违规位置数组
- 使用一个栈
stack来帮助我们找到每个位置之后的第一个违规位置。 - 初始化
next_violation数组,其元素默认为n + 1。
步骤四:寻找违规位置
- 从后向前遍历字符串,使用栈来记录每个位置之后的第一个违规位置。
计数好串
步骤五:统计好串数量
- 遍历前缀和数组,对于每个位置,计算以该位置为起始的好串数量。
解题步骤
初始化前缀和数组
- 初始化
prefix数组,长度为n + 1。
计算前缀和
- 对于每个字符,更新
prefix数组。
寻找违规位置
- 使用栈来记录每个位置之后的第一个违规位置。
统计好串数量
- 对于每个可能的起始位置,计算好串的数量。
代码部分
def solution(t: str) -> int:
n = len(t)
# Compute prefix balances
prefix = [0] * (n + 1)
for i in range(1, n + 1):
if t[i - 1] == '0':
prefix[i] = prefix[i - 1] + 1
else:
prefix[i] = prefix[i - 1] - 1
# Find next violation for each position using a stack
stack = []
next_violation = [n + 1] * (n + 1)
for i in range(n, -1, -1):
while stack and prefix[stack[-1]] > prefix[i]:
stack.pop()
if stack:
next_violation[i] = stack[-1]
else:
next_violation[i] = n + 1
stack.append(i)
# Count good substrings
count = 0
for i in range(1, n + 1):
if t[i - 1] == '0':
j = next_violation[i - 1]
if j > n:
j = n + 1
count += j - i
return count
if __name__ == '__main__':
print(solution(t = "100") == 3)
print(solution(t = "10010") == 6)
print(solution(t = "010101") == 3)
复杂度分析
- 时间复杂度:整个算法的时间复杂度为
O(n),前缀和计算、单调栈应用和子串计数三个步骤的时间复杂度均为O(n)。 - 空间复杂度:空间复杂度为
O(n),主要用于存储前缀和数组prefix和单调栈的结果next_violation。
总结
这题为何是好题,是因为怎么看出来通过前缀和与栈的结合,提供了一个高效解决“好串”问题的方法。
- 通过前缀和,能够快速判断前缀中
'0'和'1'的数量关系, - 而栈则帮助我们找到了每个位置之后的第一个违规位置。这种方法不仅优化了判断过程,也使得代码逻辑更加清晰。