leetcode2055. 蜡烛之间的盘子 - 前缀和
这道题目作为比较单纯的前缀和题目,不需要额外的一些知识,只需要了解前缀和数组的生成与使用即可,并且也有一定的难度(难度分1819),是一个比较好的前缀和例题。
题干
给你一个长桌子,桌子上盘子和蜡烛排成一列。给你一个下标从 0 开始的字符串 s
,它只包含字符 '*'
和 '|'
,其中 '*'
表示一个 盘子 ,'|'
表示一支 蜡烛 。
同时给你一个下标从 0 开始的二维整数数组 queries
,其中 queries[i] = [lefti, righti]
表示 子字符串 s[lefti...righti]
(包含左右端点的字符)。对于每个查询,你需要找到 子字符串中 在 两支蜡烛之间 的盘子的 数目 。如果一个盘子在 子字符串中 左边和右边 都 至少有一支蜡烛,那么这个盘子满足在 两支蜡烛之间 。
- 比方说,
s = "||**||**|*"
,查询[3, 8]
,表示的是子字符串"*||***\**\***|"
。子字符串中在两支蜡烛之间的盘子数目为2
,子字符串中右边两个盘子在它们左边和右边 都 至少有一支蜡烛。
请你返回一个整数数组 answer
,其中 answer[i]
是第 i
个查询的答案。
示例 1:
输入:s = "||***|", queries = [[2,5],[5,9]]
输出:[2,3]
解释:
- queries[0] 有两个盘子在蜡烛之间。
- queries[1] 有三个盘子在蜡烛之间。
示例 2:
输入:s = "||||||*", queries = [[1,17],[4,5],[14,17],[5,11],[15,16]]
输出:[9,0,0,0,0]
解释:
- queries[0] 有 9 个盘子在蜡烛之间。
- 另一个查询没有盘子在蜡烛之间。
提示:
3 <= s.length <= 105
s 只包含字符 '*' 和 '|' 。
1 <= queries.length <= 105
queries[i].length == 2
0 <= lefti <= righti < s.length
思路
假设有一个例子字符串s
= '**|***|*'
,需要查询q1
至q2
有多少盘子在两个蜡烛之间
那么我们只需要知道:
- 从
q1
到q2
总共有多少盘子totalPlants
q1
开始从左往右数,第一个蜡烛的位置p1
q2
开始从右往左数,第一个蜡烛的位置p2
之后使用totalPlants - (p1 - q1) - (q2 - p2)
,剔除不在蜡烛包围内的盘子数,就可以得到被蜡烛包围的盘子数。
编码思路
- 如何得到
totalPlants
?
使用前缀和统计得到前缀和数组suffixPlate
,使用suffixPlate[q2] - (suffixPlate[q1 - 1] ?? 0)
即可得到q1
到q2
之间的盘子数量 - 如何得到
p1
、p2
的位置?- 对于
p2
,可以使用一个数组lFirstCandle
,数组长度与字符串s
的长度一样,lFirstCandle[i]
表示s[i]
的左边最近的蜡烛的位置。这样我们只需要查询lFirstCandle[q2]
,就能得到p2
的值了 - 对于
p1
,可以使用一个数组rFirstCandle
,数组长度与字符串s
的长度一样,rFirstCandle[i]
表示s[i]
的右边最近的蜡烛的位置。这样我们只需要查询lFirstCandle[q1]
,就能得到p1
的值了
- 对于
- 边界情况:
q1
、q2
之间只有一根蜡烛或者没有蜡烛q1
、q2
之间只有一根蜡烛会使得totalPlants - (p1 - q1) - (q2 - p2)
结果为0,直接返回0即可q1
、q2
之间没有蜡烛会使得totalPlants - (p1 - q1) - (q2 - p2)
的结果为负数,此时返回0即可
代码&gpt分析
function platesBetweenCandles(s: string, queries: number[][]): number[] {
let lFirstCandle: number[] = [];
let rFirstCandle: number[] = [];
let prefixPlate: number[] = [];
// 构建前缀和数组
for (const char of s) prefixPlate.push((prefixPlate.at(-1) ?? 0) + (char === "*" ? 1 : 0));
let l = 0;
let lCandlePosition = -1;
let r = s.length - 1;
let rCandlePosition = s.length;
// 缓存左右侧最近的蜡烛的下标
while (s[l]) {
if (s[l] === "|") lCandlePosition = l;
if (s[r] === "|") rCandlePosition = r;
lFirstCandle.push(lCandlePosition);
rFirstCandle[r] = rCandlePosition;
l++;
r--;
}
return queries.map(([l, r]) => {
let totalPlants = prefixPlate[r] - (prefixPlate[l - 1] ?? 0);
// 考虑负数情况下的边界情况
return Math.max(0, totalPlants - (r - lFirstCandle[r]) - (rFirstCandle[l] - l));
});
}
算法逻辑
该算法的核心目的是在一个由蜡烛 (|
) 和盘子 (*
) 组成的字符串中,根据给定的查询,快速计算两根蜡烛之间盘子的数量。通过预处理构建前缀和数组和左右最近蜡烛位置数组,将每次查询的复杂度从 O(n)O(n)O(n) 降低到 O(1)O(1)O(1)。
变量解释
lFirstCandle
: 数组,用于存储每个字符左侧最近的蜡烛的索引。如果当前位置没有蜡烛,则存储上一个蜡烛的位置。rFirstCandle
: 数组,用于存储每个字符右侧最近的蜡烛的索引。如果当前位置没有蜡烛,则存储下一个蜡烛的位置。prefixPlate
: 前缀和数组,用于存储从字符串开头到当前位置盘子 (*
) 的累计数量。lCandlePosition
: 临时变量,用于记录遍历过程中左侧最近的蜡烛的位置。rCandlePosition
: 临时变量,用于记录遍历过程中右侧最近的蜡烛的位置。l
和r
: 指针变量,分别从左向右和从右向左遍历字符串。
关键代码解析
-
构建前缀和数组:
javascript 复制代码 for (const char of s) prefixPlate.push((prefixPlate.at(-1) ?? 0) + (char === "*" ? 1 : 0));
这段代码通过判断每个字符是否是盘子 (
*
),计算当前累计的盘子数量,并存储到前缀和数组中。如果字符是蜡烛 (|
),前缀和保持不变。 -
缓存左右侧最近蜡烛的下标:
javascript 复制代码 while (s[l]) { if (s[l] === "|") lCandlePosition = l; if (s[r] === "|") rCandlePosition = r; lFirstCandle.push(lCandlePosition); rFirstCandle[r] = rCandlePosition; l++; r--; }
双指针分别从字符串两端向中间遍历,记录每个字符左侧和右侧最近的蜡烛位置,并将其存储到
lFirstCandle
和rFirstCandle
数组中。 -
处理每个查询:
javascript 复制代码 return queries.map(([l, r]) => { let totalPlants = prefixPlate[r] - (prefixPlate[l - 1] ?? 0); return Math.max(0, totalPlants - (r - lFirstCandle[r]) - (rFirstCandle[l] - l)); });
- 通过前缀和数组计算区间内盘子的总数。
- 减去两端无法被两根蜡烛包围的盘子数量。
- 使用
Math.max(0, ...)
确保结果非负。
性能分析
- 时间复杂度:
- 预处理阶段(前缀和数组和左右最近蜡烛数组):O(n)O(n)O(n),其中 nnn 是字符串长度。
- 查询阶段:每次查询是 O(1)O(1)O(1),总查询复杂度是 O(q)O(q)O(q),其中 qqq 是查询的数量。
- 总体时间复杂度:O(n+q)O(n + q)O(n+q)。
- 空间复杂度:
- 使用了
prefixPlate
、lFirstCandle
和rFirstCandle
,每个数组的大小为 O(n)O(n)O(n)。 - 总空间复杂度为 O(n)O(n)O(n)。
- 使用了
适用场景
-
字符串区间统计问题:需要在多次查询中高效统计特定字符的数量。
-
动态规划问题优化:通过预处理构建前缀和数组或最近位置数组来降低查询复杂度。
-
场景示例:
- 盘子和蜡烛的场景可用于描述线性排列中的区间统计问题,如铁路站点与列车车厢统计。
- DNA 序列中某两段标记之间核苷酸的计数问题。
本文作者:Cat_Catcher
本文链接:https://www.cnblogs.com/CatCatcher/p/18610844
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理