前缀和这个技巧,本身不难,但真正到面试或者周赛里,能不能在读完题之后第一时间想到它,差别很大。力扣2055题《蜡烛之间的盘子》就是一个非常典型的例子:字符串里用 * 表示盘子,用 | 表示蜡烛,然后给你一堆查询,每次给一个子串区间 [l, r],问这个区间里“被两根蜡烛夹在中间”的盘子到底有多少个。
我第一次看这题,第一反应是“这肯定要枚举区间,然后判断每个盘子左右有没有蜡烛”,但这样肯定超时。真正上手之后才发现,这题的难点不在于前缀和本身,而是怎么把“夹在蜡烛中间”翻译成坐标、怎么处理区间左右边界的各种情况。这篇文章我会把完整思考过程拆开讲一遍:先分析题意,再做预处理设计,再讲查询阶段怎么写,最后会列几个我在提交时反复踩过的边界坑。整个方案可以在 O(n + q) 时间内通过,适合刚接触前缀和、或者想查漏补缺的人参考。
1. 题目拆解:区间内夹在蜡烛之间的盘子
1.1 原题在说什么
先还原一下题目场景。假设输入字符串 s = "*|*|**|*",这里面:
*是盘子|是蜡烛
现在给一个查询区间,比如 [1, 6],对应子串是 "|*|**|"。从视觉上看,这个子串内部有三根蜡烛,位置分别在 1、3、6。那这个查询的答案应该是多少?
不是 3,也不是 2?等等,这里要看清楚题目的定义:盘子必须“被两根蜡烛夹在中间”。所以你需要找到一对蜡烛,并且盘子位于这一对蜡烛之间。最直观的做法是把区间内第一根出现的蜡烛当作左边界,把最后一根出现的蜡烛当作右边界,然后统计这两个边界之间的盘子。
拿 [1, 6] 来说:
- 从左往右看到第一根蜡烛是下标 1
- 从右往左看到第一根蜡烛是下标 6
- 它们之间的盘子在下标 2、4、5 三个位置
去掉边界本身,位置 2、4、5 里,2 是 *,4 是 *,5 是 *,所以答案是 3。但有一个特殊点:如果区间里根本没有蜡烛,或者只有一根蜡烛,那答案就是 0。
1.2 为什么不能直接暴力
最容易想到的暴力做法是:对于每个查询 [l, r],先从 l 往后扫描找到第一根蜡烛,再从 r 往前扫描找到第一根蜡烛,然后在这两个边界之间遍历一次,数盘子数量。
假设字符串长度是 n,查询数量是 q,最坏情况下每个查询都要遍历整个区间,时间复杂度是 O(nq)。当 n 和 q 都到 10^5 的时候,这个量级是秒不动的,甚至会超时到怀疑人生。
所以这题需要做“预处理”。核心需求只有两个:
- 快速知道某个区间里有多少个盘子,这正好是前缀和最擅长的场景。
- 快速知道某个位置左边/右边最近的蜡烛在哪里,这个可以提前用两次线性扫描记录下来。
两个需求都解决之后,每个查询就能在常数时间内完成,整体复杂度降到 O(n + q)。这道题也从“看起来很奇怪”变成了“标准的区间计数题”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三个预处理数组分别解决什么问题
2.1 先用前缀和解决“任意区间有多少盘子”
所谓前缀和,简单说就是用一个数组记录“从开头到当前位置的累积量”。这里我们用前缀和来记录*的数量,蜡烛不计入。
我习惯定义成:
code复制plate_prefix[i] = 字符串前 i 个字符中 '*' 的数量
注意这个定义和不少人的习惯不一样。plate_prefix[0] = 0,plate_prefix[1] 表示只包含下标 0 的字符里有多少个 *。这么写的好处是,可以很方便地求任意连续区间的盘子数量。
建立前缀和数组的代码大概是这样的:
python复制n = len(s)
plate_prefix = [0] * (n + 1)
for i, ch in enumerate(s):
plate_prefix[i + 1] = plate_prefix[i] + (1 if ch == '*' else 0)
举个小例子:
code复制s = "*|*|**|*"
plate_prefix = [0, 1, 1, 2, 2, 3, 4, 4, 5]
这里 plate_prefix[5] = 3,表示下标 0 到 4 的 5 个字符里,* 的总数是 3。你可以验证一下:前 5 个字符是 *|*|*,其中盘子是下标 0、2、4,确实是 3 个。
如果我想知道原字符串中下标 a 到 b 之间有多少个盘子,就用:
code复制plate_prefix[b + 1] - plate_prefix[a]
这是前缀和最基本的区间减法。不过在这道题里,我们真正要算的区间并不是查询给的原区间,而是“两根蜡烛边界之间”的区间。所以查询阶段不能直接用 [l, r] 去减,得先确定左右两根蜡烛的位置。
2.2 一次性扫描出每个位置“左边最近的蜡烛”
第一个辅助数组用
