题目描述
思路解析动画文字版
记住「字符分桶记下标 → 每个字符二分找第一个比 cur 大的位置 → cur 往后跳」,下面每帧都在套它。
先一遍扫 s="abcde",把每个字符出现的位置记进桶里:a 在下标 0、b 在 1、c 在 2、d 在 3、e 在 4。这张桶表建一次、所有单词共用,这正是它比每词重扫 s 省时的地方。
轮到单词 "a"。游标 cur 从 −1 开始,表示还没用掉 s 里任何位置。接下来逐个字符,在它的桶里找第一个比 cur 大的下标。
匹配 'a':在它的桶 [0] 里,第一个比 cur=-1 大的下标是 0。把 cur 跳到 0,表示用 s 第 0 位的 'a' 接上了。s 上高亮往后推进。
"a" 的每个字符都在 s 里按顺序找到了落点(0),它是 s 的子序列,答案加一,现在是 1。
轮到单词 "bb"。游标 cur 从 −1 开始,表示还没用掉 s 里任何位置。接下来逐个字符,在它的桶里找第一个比 cur 大的下标。
匹配 'b':在它的桶 [1] 里,第一个比 cur=-1 大的下标是 1。把 cur 跳到 1,表示用 s 第 1 位的 'b' 接上了。s 上高亮往后推进。
要匹配 'b',去它的桶 [1] 里找第一个比当前 cur=1 大的下标。桶里最大的也才 1,没有比 1 更靠后的了。这个字符接不上,单词 "bb" 失败。
"bb" 没能走完,不是子序列,答案保持 1。注意它失败不是因为缺字符种类,而是同一个字符在 s 里不够用、排不出递增的位置。
轮到单词 "acd"。游标 cur 从 −1 开始,表示还没用掉 s 里任何位置。接下来逐个字符,在它的桶里找第一个比 cur 大的下标。
匹配 'a':在它的桶 [0] 里,第一个比 cur=-1 大的下标是 0。把 cur 跳到 0,表示用 s 第 0 位的 'a' 接上了。s 上高亮往后推进。
匹配 'c':在它的桶 [2] 里,第一个比 cur=0 大的下标是 2。把 cur 跳到 2,表示用 s 第 2 位的 'c' 接上了。s 上高亮往后推进。
匹配 'd':在它的桶 [3] 里,第一个比 cur=2 大的下标是 3。把 cur 跳到 3,表示用 s 第 3 位的 'd' 接上了。s 上高亮往后推进。
"acd" 的每个字符都在 s 里按顺序找到了落点(0 → 2 → 3),它是 s 的子序列,答案加一,现在是 2。
轮到单词 "ace"。游标 cur 从 −1 开始,表示还没用掉 s 里任何位置。接下来逐个字符,在它的桶里找第一个比 cur 大的下标。
匹配 'a':在它的桶 [0] 里,第一个比 cur=-1 大的下标是 0。把 cur 跳到 0,表示用 s 第 0 位的 'a' 接上了。s 上高亮往后推进。
匹配 'c':在它的桶 [2] 里,第一个比 cur=0 大的下标是 2。把 cur 跳到 2,表示用 s 第 2 位的 'c' 接上了。s 上高亮往后推进。
匹配 'e':在它的桶 [4] 里,第一个比 cur=2 大的下标是 4。把 cur 跳到 4,表示用 s 第 4 位的 'e' 接上了。s 上高亮往后推进。
"ace" 的每个字符都在 s 里按顺序找到了落点(0 → 2 → 4),它是 s 的子序列,答案加一,现在是 3。
四个单词判完:a、acd、ace 是子序列,bb 不是,共 3 个。回头看,我们只建了一次位置桶,每个单词靠「在桶里二分找第一个比 cur 大的下标」一路往后跳,没有为每个词重新扫一遍 s。
边界:空 s 时非空单词全失败(空单词仍是任何串的子序列、计入)、同一字符在 s 里不够用就失败(bb 即此)。
两个高频追问:words 极多可用「按 s 推进」的桶解到 O(|s|+Σ|w|);本题是 LC392 的多查询升级版。
参考代码
from typing import Listfrom bisect import bisect_rightclass Solution: def numMatchingSubseq(self, s: str, words: List[str]) -> int: pos = [[] for _ in range(26)] for i, ch in enumerate(s): pos[ord(ch) - 97].append(i) ans = 0 for w in words: cur = -1 ok = True for ch in w: arr = pos[ord(ch) - 97] j = bisect_right(arr, cur) if j == len(arr): ok = False break cur = arr[j] ans += ok return ans复杂度
- 时间:O(|s| + Σ|w|·log|s|),预处理扫 s 一遍 O(|s|);每个单词每个字符在桶里二分一次 O(log|s|)
- 空间:O(|s|),位置桶总共存 |s| 个下标
易错点
面试追问把动画讲成自己的话
追问如果 words 非常多,还有没有比「分桶 + 二分」更对路的解法?
追问这道题和「判断单个字符串是否子序列(LC392)」什么关系?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
数组中的最长山脉
LeetCode 845 · 中等 · 沿着 双指针套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题