最长特殊序列 II 图解题解
这道题到底在问什么
- 输入
- strs=["aba","cdc","eae"]
- 输出
- 3 (三个串互不为对方子序列,最长 3)
- 输入
- strs=["aaa","aaa","aa"]
- 输出
- -1 (aaa 互为子序列,aa 也被包含)
最优解:为什么这么做
一句话答案:LeetCode 522 最长特殊序列 II:在字符串列表里找最长的、不是其它任何串子序列的那个串——逐个当候选,用双指针判它是否被别人包含,全扛住就用长度刷新答案,时间 O(n²·L)、空间 O(1)。
特殊序列凭什么算特殊,要返回什么
给一个字符串列表 strs,要找出里面最长的「特殊序列」的长度。特殊序列指某个串独有的子序列,也就是它不是其它任何串的子序列。子序列就是删掉原串里若干字符、剩下的按原顺序拼起来,不要求连续。题面例子 strs=["aba","cdc","eae"] 三个串互不为子序列,答案是最长的 3;strs=["aaa","aaa","aa"] 每个串都被别人包含,返回 -1。
为什么不能直接挑最长的串交上去
直觉是把最长的串直接交上去:它最长,总不会是更短串的子序列。可这招碰上重复串就漏。strs=["aaa","aaa","aa"] 里最长的是 "aaa",但另一个 "aaa" 和它一模一样、互为子序列,谁都不独有,长度并列第一也照样淘汰。长度只是候选资格,真正要判的是有没有被别人包含,得挨个比。
怎么判一个串是不是另一个串的子序列
判断「s 是不是 t 的子序列」用双指针,两个下标各扫一个串。i 指 s、j 指 t,都从 0 起。看 t[j]:正好是 s 当前要找的 s[i],这一位就对上,i 挪一格找下一个;对不上,i 不动,只让 j 右移接着找。t 扫完时若 i 已走到 s 末尾(i 等于 s 的长度),说明 s 每个字符都按顺序在 t 里出现过,s 就是 t 的子序列。一个串只要被任意别的串包含就出局,所以两两各判一次即可。
整个流程是怎么把答案挑出来的
主流程两层循环:外层把每个串轮流当候选 s,内层拿它和其余串 t 逐个判子序列,要跳过自己(下标 i≠j,因为任何串都是自身的子序列,不跳过必被自己判掉)。s 被某个 t 包含就没戏,换下一个候选;一路比到底没被任何串包含,它就是特殊序列,用长度更新答案最大值。全部走完,答案初值 -1 要么被刷新过、要么原样保持。
题面两组数据,逐个候选试一遍
先看 strs=["aba","cdc","eae"]。候选 "aba" 比 "cdc":c、d、c 里找不到打头的 a,i 停在 0,不是子序列;再比 "eae",只有中间那个 a 对上一个,走完 i 没到 3,也不是。都没包含它,"aba" 特殊,答案 3;另外两个同理各自独有。
再看 strs=["aaa","aaa","aa"]。第一个 "aaa" 比第二个 "aaa",三个 a 全对上、i 走到 3,被包含淘汰;第二个同样被第一个吃掉;"aa" 比 "aaa" 也全中被包含。三个全出局,答案保持 -1。
跳过自己没写、子序列错当子串,会怎样
内层忘了 i≠j 这道跳过自己的关卡,每个串都会先被自己判成子序列,没有一个能活下来,答案永远卡在 -1。把「子序列」错当成连续「子串」也常见:"aa" 是 "aba" 的子序列(取第 0、2 位两个 a),却不是连续子串,用子串的判法会把本该淘汰的串误放进来。别被「最长」带偏,两个相同的最长串会互相抵消,长度再大也不算数。
复杂度上,n 个串两两配对 O(n²),每对再做一次 O(L) 子序列扫描(L 为串长),合起来 O(n²·L);只用 i、j 两个下标和一个记分变量,空间 O(1)。
▶ 动画逐步走查(共 30 步)——想跟着动画一帧帧对照就展开
- 3记住「逐个串去比,被别人包含就淘汰,扛过所有人就特殊」,下面每一帧都在套它。
- 4轮到第 0 个串 "aba" 当候选。逐个拿别的串去试:只要它是某个串的子序列就被淘汰,全扛住才算特殊。
- 5t 的第 0 位 "c" 不是候选要找的 "a",i 指针不动,j 继续右移找下一个。
- 6t 的第 1 位 "d" 不是候选要找的 "a",i 指针不动,j 继续右移找下一个。
- 7t 的第 2 位 "c" 不是候选要找的 "a",i 指针不动,j 继续右移找下一个。
- 8整个 "cdc" 扫完了,"aba" 没能被它完整包含,"aba" 不是 "cdc" 的子序列。继续拿下一个串来比。
- 9t 的第 0 位 "a" 正好等于候选要找的 "a",命中!候选已匹配 1/3 个字符。
- 10t 的第 1 位 "b" 正好等于候选要找的 "b",命中!候选已匹配 2/3 个字符。
- 11t 的第 2 位 "a" 正好等于候选要找的 "a",命中!候选已匹配 3/3 个字符。"aba" 被 "aba" 完整包含了。
- 12候选 "aba" 被 "aba" 完整包含,它不是独有的,整串标红淘汰。
- 13轮到第 1 个串 "cdc" 当候选。逐个拿别的串去试:只要它是某个串的子序列就被淘汰,全扛住才算特殊。
- 14t 的第 0 位 "a" 不是候选要找的 "c",i 指针不动,j 继续右移找下一个。
- 15t 的第 1 位 "b" 不是候选要找的 "c",i 指针不动,j 继续右移找下一个。
- 16t 的第 2 位 "a" 不是候选要找的 "c",i 指针不动,j 继续右移找下一个。
- 17整个 "aba" 扫完了,"cdc" 没能被它完整包含,"cdc" 不是 "aba" 的子序列。继续拿下一个串来比。
- 18又一个 "aba",和刚才比过的那个完全相同,"cdc" 的第一个字符 "c" 在里面照样找不到,直接跳过。
- 19t 的第 0 位 "a" 不是候选要找的 "c",i 指针不动,j 继续右移找下一个。
- 20t 的第 1 位 "b" 不是候选要找的 "c",i 指针不动,j 继续右移找下一个。
- 21整个 "ab" 扫完了,"cdc" 没被它完整包含。所有对手都比完了,没人包含 "cdc",准备判它是特殊序列。
- 22候选 "cdc" 扛过了所有对手,谁也包不住它,它就是特殊序列!整串标绿,用它的长度 3 刷新答案为 3。
- 23轮到第 2 个串 "aba" 当候选。逐个拿别的串去试:只要它是某个串的子序列就被淘汰,全扛住才算特殊。
- 24t 的第 0 位 "a" 正好等于候选要找的 "a",命中!候选已匹配 1/3 个字符。
- 25t 的第 1 位 "b" 正好等于候选要找的 "b",命中!候选已匹配 2/3 个字符。
- 26t 的第 2 位 "a" 正好等于候选要找的 "a",命中!候选已匹配 3/3 个字符。"aba" 被 "aba" 完整包含了。
- 27候选 "aba" 被 "aba" 完整包含,它不是独有的,整串标红淘汰。
- 28轮到第 3 个串 "ab" 当候选。逐个拿别的串去试:只要它是某个串的子序列就被淘汰,全扛住才算特殊。
- 29t 的第 0 位 "a" 正好等于候选要找的 "a",命中!候选已匹配 1/2 个字符。
- 30t 的第 1 位 "b" 正好等于候选要找的 "b",命中!候选已匹配 2/2 个字符。"ab" 被 "aba" 完整包含了。
- 31候选 "ab" 被 "aba" 完整包含,它不是独有的,整串标红淘汰。
- 32回看全程:两个 "aba" 互为子序列、"ab" 是 "aba" 的子序列,全被淘汰;只有 "cdc" 谁也包不住,它就是最长特殊序列,答案 3。
⚠️ 容易写错的地方
✗ 错:直接取最长的串当答案
✓ 对:最长串若和别的串相等或被包含也会被淘汰
两个相同的最长串互为子序列,谁都不特殊
✗ 错:比较时漏了跳过自己
✓ 对:内层 j 必须 i ≠ j
任何串都是自己的子序列,不跳过会把所有串都误判成非特殊
✗ 错:把「子序列」当成「子串」
✓ 对:子序列可跳着删字符,不要求连续
aa 是 aba 的子序列(取第 0、2 位的 a),但不是连续子串
完整代码(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 findLUSlength(self, strs: List[str]) -> int:
def check(s: str, t: str):
i = j = 0
while i < len(s) and j < len(t):
if s[i] == t[j]:
i += 1
j += 1
return i == len(s)
ans = -1
for i, s in enumerate(strs):
for j, t in enumerate(strs):
if i != j and check(s, t):
break
else:
ans = max(ans, len(s))
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:
int findLUSlength(vector<string>& strs) {
int ans = -1;
int n = strs.size();
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 (int i = 0, j; i < n; ++i) {
int x = strs[i].size();
for (j = 0; j < n; ++j) {
if (i != j && check(strs[i], strs[j])) {
x = -1;
break;
}
}
ans = max(ans, x);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int findLUSlength(String[] strs) {
int ans = -1;
int n = strs.length;
for (int i = 0, j; i < n; ++i) {
int x = strs[i].length();
for (j = 0; j < n; ++j) {
if (i != j && check(strs[i], strs[j])) {
x = -1;
break;
}
}
ans = Math.max(ans, x);
}
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(n² · L)
n 个串两两比较,每次子序列判定 O(L)
空间
O(1)
只用双指针 i、j 和记分变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长特殊序列 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和「最长特殊序列 I」(LC521)差在哪?+
LC521 只有两个串,答案直接是较长串的长度(两串不等时),因为更长的串绝不可能是更短串的子序列,只有两串相等才返回 -1,比一下长度就完事。LC522 有多个串、还可能重复或互相包含,长度第一也可能被另一个相同的串抵消掉,所以必须把每个串都拿去和其余串逐个用双指针判子序列,不能只看长度。
先按长度排序能不能加速?+
能少走一些冤枉路。按长度从大到小排,第一个不被任何更长或等长串包含的串就是答案,找到即可提前结束,实际数据上常常快不少。但最坏情况下(比如所有串互不包含)每对仍要比、每比一次还要 O(L) 判子序列,总量级不变,还是 O(n²·L),排序本身的 O(n log n) 也盖不过它。
为什么只用两两比较就够,不用看三个串的组合?+
因为「特殊」的定义是「不被任何单个串包含」,判定的最小单位就是一对串。一个候选串要么被某个具体的串包含(那它当场出局),要么不被任何一个串包含(那它就是特殊的)。三个串一起看并不会产生新的包含关系——s 若既不是 t1 的子序列、也不是 t2 的子序列,t1 和 t2 合起来也变不出能包含 s 的第三种情况。所以逐对判定已经覆盖了全部可能。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长特殊序列 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。