通过删除字母匹配到字典里最长单词 图解题解
这道题到底在问什么
- 输入
- s="abpcplea", dictionary=["ale","apple","monkey","plea"]
- 输出
- "apple"
最优解:为什么这么做
一句话答案:LeetCode 524 通过删除字母匹配到字典里最长单词:对字典每个词用双指针判它是不是 s 的子序列,在是子序列的词里挑最长、同长取字典序小,时间 O(d·n)。
从 s 删掉几个字母,够到字典里哪个词
给一个字符串 s 和一串候选词 dictionary,问:把 s 里某些字母删掉、剩下的保持原来的先后顺序,能拼出字典里哪个词,要最长的那个;多个一样长就取字典序最小的,都够不到就返回空串。题面例子 s="abpcplea"、dictionary=["ale","apple","monkey","plea"],答案 "apple"。
把 s 所有删法都列出来,为什么行不通
很自然会想到把 s 的所有删法穷举出来:s 长 n,每个字母删或不删两种选择,共 2^n 种结果,n 到 20 就是一百多万条,再和字典比对撑不住。换个方向:不列 s 能变成什么,而是拿字典每个词,直接问它能不能由 s 删字母得到。
「删字母能得到 word」翻成一句什么话
把 s 删几个字母得到 word,等价于说 word 是 s 的子序列——word 的每个字母都能在 s 里按先后顺序对上,中间隔多远都行,但次序不能乱。判子序列有个标准双指针写法:i 指 word 当前要找的字母、j 从头扫 s,s[j] 和 word[i] 相等就算对上一个、i 往后挪一位,不管相不相等 j 每轮都右移。等 i 走到 word 末尾,说明每个字母都按顺序找齐,word 就是子序列。
方向别弄反:i 走候选词 word、j 走源串 s,因为是从 s 里删字母得到 word;也只在相等时推进 i,光看字母都在 s 里出现过不够,次序乱了照样删不出来。
一个词判完是子序列,接下来比什么
判定只解决单个词,外面还要挑答案。遍历字典每个候选,判是不是子序列;是的话再和手里存的最优比:更长就替换,一样长就比字典序、更小才替换。全部试完,手里剩的就是最长且字典序最小的。别碰到第一个子序列就返回——后面可能有更长的,或同样长却更靠前的。
abpcplea 配四个候选,apple 怎么胜出
ale:找 a,第 0 位对上、i 到 1;找 l 跳过 b、p、c、p,第 5 位对上、i 到 2;找 e,第 6 位对上、i 到 3 走完,是子序列,比空答案长,先存 ale。
apple:a 对上第 0 位,p 跳过 b 对上第 2 位,第二个 p 跳过 c 对上第 4 位,l 对上第 5 位,e 对上第 6 位,i 走完 5 个字母,是子序列且比 ale 长,答案换 apple。monkey:首字母 m 扫遍 s 都没有,i 卡在 0,淘汰。
plea:p 跳过 a、b 对上第 2 位,l 对第 5 位,e 对第 6 位,a 对第 7 位,i 走完 4 个字母,也是子序列——但只有 4 个,没比存着的 apple(5 个)长,答案不变。四个试完,apple 胜出。
全军覆没、同长打平,这些角落别漏
复杂度上,设 s 长 n、字典 d 个词,每个候选双指针扫一遍 s 是 O(n),合计 O(d·n);只用两个指针加一个答案串,额外空间 O(1)。
几个角落容易漏。没有一个词是子序列时,答案是初始空串,记得给初值。两个候选一样长又都合格,得按字典序取小,返回先遇到的会错。比 s 还长的候选,i 走不到末尾,自然判成非子序列,不必先比长度。
▶ 动画逐步走查(共 33 步)——想跟着动画一帧帧对照就展开
- 3记住口诀:内层双指针判子序列,外层比长度、平局比字典序。下面每帧都在套它。
- 4上面这排是源串 s 的每个字母。我们要逐个拿字典里的候选词,放进来用双指针扫 s,看它能不能整词对上。绿色代表对上的字母,蓝色代表扫过但没用上的。
- 5轮到字典第 1 个候选 ale(长度 3)。两个指针都从头开始:i 指它的第一个字母 a,j 从 s 的第 0 位往右扫,去把这些字母依次找出来。
- 6s 第 0 位是 a,正好等于 ale 当前要找的 a,命中!这一格变绿,i 前进到 1。下一个要找 l。
- 7s 第 1 位是 b,不等于现在要找的 l,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 1。
- 8s 第 2 位是 p,不等于现在要找的 l,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 1。
- 9s 第 3 位是 c,不等于现在要找的 l,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 1。
- 10s 第 4 位是 p,不等于现在要找的 l,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 1。
- 11s 第 5 位是 l,正好等于 ale 当前要找的 l,命中!这一格变绿,i 前进到 2。下一个要找 e。
- 12s 第 6 位是 e,正好等于 ale 当前要找的 e,命中!这一格变绿,i 前进到 3。ale 的字母全部对上了。
- 13ale 是子序列,而且比之前的最优更长(或同长更靠前),刷新最优 = ale。
- 14轮到字典第 2 个候选 apple(长度 5)。两个指针都从头开始:i 指它的第一个字母 a,j 从 s 的第 0 位往右扫,去把这些字母依次找出来。
- 15s 第 0 位是 a,正好等于 apple 当前要找的 a,命中!这一格变绿,i 前进到 1。下一个要找 p。
- 16s 第 1 位是 b,不等于现在要找的 p,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 1。
- 17s 第 2 位是 p,正好等于 apple 当前要找的 p,命中!这一格变绿,i 前进到 2。下一个要找 p。
- 18s 第 3 位是 c,不等于现在要找的 p,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 2。
- 19s 第 4 位是 p,正好等于 apple 当前要找的 p,命中!这一格变绿,i 前进到 3。下一个要找 l。
- 20s 第 5 位是 l,正好等于 apple 当前要找的 l,命中!这一格变绿,i 前进到 4。下一个要找 e。
- 21s 第 6 位是 e,正好等于 apple 当前要找的 e,命中!这一格变绿,i 前进到 5。apple 的字母全部对上了。
- 22apple 是子序列,而且比之前的最优更长(或同长更靠前),刷新最优 = apple。
- 23轮到候选 monkey,它要找的第一个字母是 m。可整条 s 从头扫到尾,a、b、p、c、p、l、e、a 里没有一个 m,i 一直卡在 0。
- 24monkey 的第一个字母都凑不齐,自然不是子序列,淘汰。最优依旧是 apple。
- 25轮到字典第 4 个候选 plea(长度 4)。两个指针都从头开始:i 指它的第一个字母 p,j 从 s 的第 0 位往右扫,去把这些字母依次找出来。
- 26s 第 0 位是 a,不等于现在要找的 p,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 0。
- 27s 第 1 位是 b,不等于现在要找的 p,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 0。
- 28s 第 2 位是 p,正好等于 plea 当前要找的 p,命中!这一格变绿,i 前进到 1。下一个要找 l。
- 29s 第 3 位是 c,不等于现在要找的 l,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 1。
- 30s 第 4 位是 p,不等于现在要找的 l,这一格用不上,标蓝跳过,j 继续右移。i 仍然停在 1。
- 31s 第 5 位是 l,正好等于 plea 当前要找的 l,命中!这一格变绿,i 前进到 2。下一个要找 e。
- 32s 第 6 位是 e,正好等于 plea 当前要找的 e,命中!这一格变绿,i 前进到 3。下一个要找 a。
- 33s 第 7 位是 a,正好等于 plea 当前要找的 a,命中!这一格变绿,i 前进到 4。plea 的字母全部对上了。
- 34plea 是子序列,但它没有比当前最优 apple 更长、平局也没更靠前,最优不变。
- 35四个候选都试完。ale 是子序列但只有 3 个字母,apple 是子序列有 5 个,monkey 淘汰,plea 是子序列但只有 4 个。最长的就是 apple,绿色高亮的就是它在 s 里对上的那 5 个字母。答案 apple。
⚠️ 容易写错的地方
✗ 错:把方向判反:去看 s 是不是 word 的子序列
✓ 对:判 word 是不是 s 的子序列
是从 s 里删字母得到 word,所以 i 走 word、j 走 s
✗ 错:只要字母都在 s 里出现就算对
✓ 对:必须按顺序、用双指针,i 只在相等时前进
子序列要求相对顺序一致,不能打乱
✗ 错:找到第一个子序列就返回
✓ 对:要遍历完所有候选,维护最长且字典序最小
后面可能有更长的,或同长更靠前的
✗ 错:平局时随便返回一个
✓ 对:同长必须比字典序,取更小
题目明确要求同长返回字典序最小
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def findLongestWord(self, s: str, dictionary: List[str]) -> str:
def check(s: str, t: str) -> bool:
m, n = len(s), len(t)
i = j = 0
while i < m and j < n:
if s[i] == t[j]:
i += 1
j += 1
return i == m
ans = ""
for t in dictionary:
if check(t, s) and (len(ans) < len(t) or (len(ans) == len(t) and ans > t)):
ans = t
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
string findLongestWord(string s, vector<string>& dictionary) {
string ans = "";
auto check = [&](const string& s, const string& t) {
int m = s.size(), n = t.size();
int i = 0;
for (int j = 0; i < m && j < n; ++j) {
if (s[i] == t[j]) {
++i;
}
}
return i == m;
};
for (auto& t : dictionary) {
int a = ans.size(), b = t.size();
if (check(t, s) && (a < b || (a == b && ans > t))) {
ans = t;
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public String findLongestWord(String s, List<String> dictionary) {
String ans = "";
for (String t : dictionary) {
int a = ans.length(), b = t.length();
if (check(t, s) && (a < b || (a == b && t.compareTo(ans) < 0))) {
ans = t;
}
}
return ans;
}
private boolean check(String s, String t) {
int m = s.length(), n = t.length();
int i = 0;
for (int j = 0; i < m && j < n; ++j) {
if (s.charAt(i) == t.charAt(j)) {
++i;
}
}
return i == m;
}
}复杂度
时间
O(d · n)
d 个候选,每个用双指针扫一遍长 n 的 s
空间
O(1)
只用两个指针 + 一个答案串,不开额外结构
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 通过删除字母匹配到字典里最长单词 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和「判断子序列」那道有什么关系?+
内核就是「判断子序列」那道题:双指针判 word 是不是 s 的子序列。本题在它外面套一层,对字典每个候选各判一次,再维护一个「最长且字典序最小」的答案。认出这个内核,从「删字母得到目标词」到「判子序列」的这类题就都通了。
为什么 i 只在字母相等时才前进,j 却每轮都走?+
j 每轮右移,是在源串 s 里一个个往后看有没有能用的字母;i 只在对上时前进,是因为 word 的字母必须按顺序找到——当前字母还没在 s 里找到,就不能急着找下一个。要是不管对不对都推进 i,就变成只查字母有没有出现、不管先后了,次序乱掉的词会被错判成子序列。
字典很大、要反复查很多候选,单次判定怎么优化?+
可以预处理 s:为每个字母建一个「它在 s 里出现的所有下标」的列表。判某个候选时顺着它的字母,用二分查找(有序里每次砍一半的找法)在下标列表里跳到 s 中下一个能用的位置,把单次判定从 O(n) 降到 O(候选长度 × log n)。候选远短于 s、又要反复查很多次时,这个预处理才划算。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 通过删除字母匹配到字典里最长单词 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。