匹配子序列的单词数 图解题解
这道题到底在问什么
- 输入
- s="abcde", words=["a","bb","acd","ace"]
- 输出
- 3(a、acd、ace 是子序列;bb 不是)
最优解:一步一步想明白
- 3记住「字符分桶记下标 → 每个字符二分找第一个比 cur 大的位置 → cur 往后跳」,下面每帧都在套它。
- 4先一遍扫 s="abcde",把每个字符出现的位置记进桶里:a 在下标 0、b 在 1、c 在 2、d 在 3、e 在 4。这张桶表建一次、所有单词共用,这正是它比每词重扫 s 省时的地方。
- 5轮到单词 "a"。游标 cur 从 −1 开始,表示还没用掉 s 里任何位置。接下来逐个字符,在它的桶里找第一个比 cur 大的下标。
- 6匹配 'a':在它的桶 [0] 里,第一个比 cur=-1 大的下标是 0。把 cur 跳到 0,表示用 s 第 0 位的 'a' 接上了。s 上高亮往后推进。
- 7"a" 的每个字符都在 s 里按顺序找到了落点(0),它是 s 的子序列,答案加一,现在是 1。
- 8轮到单词 "bb"。游标 cur 从 −1 开始,表示还没用掉 s 里任何位置。接下来逐个字符,在它的桶里找第一个比 cur 大的下标。
- 9匹配 'b':在它的桶 [1] 里,第一个比 cur=-1 大的下标是 1。把 cur 跳到 1,表示用 s 第 1 位的 'b' 接上了。s 上高亮往后推进。
- 10要匹配 'b',去它的桶 [1] 里找第一个比当前 cur=1 大的下标。桶里最大的也才 1,没有比 1 更靠后的了。这个字符接不上,单词 "bb" 失败。
- 11"bb" 没能走完,不是子序列,答案保持 1。注意它失败不是因为缺字符种类,而是同一个字符在 s 里不够用、排不出递增的位置。
- 12轮到单词 "acd"。游标 cur 从 −1 开始,表示还没用掉 s 里任何位置。接下来逐个字符,在它的桶里找第一个比 cur 大的下标。
- 13匹配 'a':在它的桶 [0] 里,第一个比 cur=-1 大的下标是 0。把 cur 跳到 0,表示用 s 第 0 位的 'a' 接上了。s 上高亮往后推进。
- 14匹配 'c':在它的桶 [2] 里,第一个比 cur=0 大的下标是 2。把 cur 跳到 2,表示用 s 第 2 位的 'c' 接上了。s 上高亮往后推进。
- 15匹配 'd':在它的桶 [3] 里,第一个比 cur=2 大的下标是 3。把 cur 跳到 3,表示用 s 第 3 位的 'd' 接上了。s 上高亮往后推进。
- 16"acd" 的每个字符都在 s 里按顺序找到了落点(0 → 2 → 3),它是 s 的子序列,答案加一,现在是 2。
- 17轮到单词 "ace"。游标 cur 从 −1 开始,表示还没用掉 s 里任何位置。接下来逐个字符,在它的桶里找第一个比 cur 大的下标。
- 18匹配 'a':在它的桶 [0] 里,第一个比 cur=-1 大的下标是 0。把 cur 跳到 0,表示用 s 第 0 位的 'a' 接上了。s 上高亮往后推进。
- 19匹配 'c':在它的桶 [2] 里,第一个比 cur=0 大的下标是 2。把 cur 跳到 2,表示用 s 第 2 位的 'c' 接上了。s 上高亮往后推进。
- 20匹配 'e':在它的桶 [4] 里,第一个比 cur=2 大的下标是 4。把 cur 跳到 4,表示用 s 第 4 位的 'e' 接上了。s 上高亮往后推进。
- 21"ace" 的每个字符都在 s 里按顺序找到了落点(0 → 2 → 4),它是 s 的子序列,答案加一,现在是 3。
- 22四个单词判完:a、acd、ace 是子序列,bb 不是,共 3 个。回头看,我们只建了一次位置桶,每个单词靠「在桶里二分找第一个比 cur 大的下标」一路往后跳,没有为每个词重新扫一遍 s。
⚠️ 容易写错的地方
✗ 错:找第一个 ≥ cur 的下标
✓ 对:要找第一个严格大于 cur 的下标
子序列里每个字符要用 s 里更靠后的新位置,等于 cur 会重复用同一个位置
✗ 错:每个单词都从头线扫 s
✓ 对:预处理分桶 + 二分跳位
单词很多时反复扫 s 是 O(words×|s|);分桶后每字符只二分一次
✗ 错:把子序列当成子串
✓ 对:子序列只要求顺序、不要求连续
acd 在 abcde 里不连续,但按顺序挑得出,仍是子序列
完整代码(Python / C++ / Java)
Python
from typing import List
from bisect import bisect_right
class 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 ansC++
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
int numMatchingSubseq(string s, vector<string>& words) {
vector<vector<int>> pos(26);
for (int i = 0; i < (int)s.size(); ++i) pos[s[i]-'a'].push_back(i);
int ans = 0;
for (auto &w : words) {
int cur = -1;
bool ok = true;
for (char c : w) {
auto &arr = pos[c-'a'];
auto it = upper_bound(arr.begin(), arr.end(), cur);
if (it == arr.end()) { ok = false; break; }
cur = *it;
}
if (ok) ans++;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int numMatchingSubseq(String s, String[] words) {
List<Integer>[] pos = new ArrayList[26];
for (int i = 0; i < 26; i++) pos[i] = new ArrayList<>();
for (int i = 0; i < s.length(); i++) pos[s.charAt(i) - 'a'].add(i);
int ans = 0;
for (String w : words) {
int cur = -1;
boolean ok = true;
for (char c : w.toCharArray()) {
List<Integer> arr = pos[c - 'a'];
int j = Collections.binarySearch(arr, cur + 1);
if (j < 0) j = -j - 1;
if (j == arr.size()) { ok = false; break; }
cur = arr.get(j);
}
if (ok) ans++;
}
return ans;
}
}复杂度
时间
O(|s| + Σ|w|·log|s|)
预处理扫 s 一遍 O(|s|);每个单词每个字符在桶里二分一次 O(log|s|)
空间
O(|s|)
位置桶总共存 |s| 个下标
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 匹配子序列的单词数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如果 words 非常多,还有没有比「分桶 + 二分」更对路的解法?+
有一种「按 s 字符推进」的桶解法:把每个单词按它当前等待的首字符分到 26 个桶里,然后顺序扫 s,遇到字符 c 就把 c 桶里所有单词各前进一个字符、再放进它们新首字符的桶;走到词尾的就计数。它对 s 只扫一遍,总复杂度 O(|s| + Σ|w|),省掉了二分的 log,特别适合 words 极多的场景。
这道题和「判断单个字符串是否子序列(LC392)」什么关系?+
LC392 是单次判断,双指针 O(|s|) 就够;但当要对同一个 s 判很多个单词时,每次都双指针重扫 s 太亏,于是预处理 s 的位置桶、把每次判断降到 O(|w|·log|s|),这正是本题相对 LC392 的升级点。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 匹配子序列的单词数 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。