最长回文子序列 图解题解
这道题到底在问什么
- 输入
- s="bbbab"
- 输出
- 4
最优解:为什么这么做
一句话答案:LeetCode 516 最长回文子序列:dp[i][j] 记 s[i..j] 的最长回文子序列长度,两端相等就取内部 +2、不等就丢一端取较大,按区间从短到长填表。时间 O(n²)、空间 O(n²)。
可跳字符的回文子序列,怎么算最长
给一个字符串 s,挑出一个正着读、倒着读都一样的子序列,要它尽可能长,返回长度。子序列可跳字符、但相对顺序不变,比如 bbbab 挑出 bbbb(跳过中间的 a)是长度 4 的回文。这题最容易和最长回文子串(LeetCode 5)弄混:子串要连续,子序列可跳着挑。
为什么不能把所有子序列列出来数
最直接是把所有子序列列出来逐个查,留最长的回文。可长度 n 的串有 2^n 个子序列,n 稍大就数不完(大 O 记号描述规模变大时操作数怎么涨,这里 O(2^n))。它们大量重叠、同一小段被反复重验,把每段答案存下来只算一次就是动态规划(算过的子问题记下别重算)。
为什么状态要盯一个区间 dp[i][j]
回文判断天然从两头往中间收,状态就盯一个区间。定义 dp[i][j] 为子串 s[i..j] 的最长回文子序列长度,i 左端、j 右端,答案是 dp[0][n-1]。最短区间是单字符 dp[i][i]=1(一个字母自己就是回文),是长区间的基石;空区间(i 大于 j)记 0。
两端相等为什么 +2、不等为什么丢一端取大
算 dp[i][j] 只看两端 s[i] 和 s[j]。相等时它俩能当回文的最外层,把里面 s[i+1..j-1] 的最长回文包起来、两头各加一个,dp[i][j]=dp[i+1][j-1]+2。不等时它俩没法同时当两头,只能丢一个——丢左端剩 dp[i+1][j],丢右端剩 dp[i][j-1],取大:dp[i][j]=max(dp[i+1][j], dp[i][j-1])。
填表方向有坑:用到的 dp[i+1][j-1]、dp[i+1][j]、dp[i][j-1] 都比区间 [i,j] 更短。不能按行列顺序填,得按区间长度从短到长填——短的先算好、长的才有值可用。等价写法是外层 i 从大到小、内层 j 从小到大。
拿 bbbab 把这张表一格格填出来
下标 0 到 4 是 b、b、b、a、b。先铺对角线(表里从左上到右下、行号等于列号那条线),五个单字符 dp[i][i]=1。长度 2:dp[0][1] 相等,内部空记 0、+2 得 2;dp[1][2] 同理得 2;dp[2][3] 不等取 max(1,1)=1;dp[3][4] 不等取 1。
长度 3:dp[0][2] 相等得 dp[1][1]+2=3;dp[1][3] 不等取 max(dp[2][3]=1, dp[1][2]=2)=2;dp[2][4] 相等得 3。长度 4:dp[0][3] 不等取 max(2, 3)=3;dp[1][4] 相等得 dp[2][3]+2=3。长度 5:dp[0][4] 相等得 dp[1][3]+2=4,右上角就是答案 bbbb。
填表顺序填反、长度 2 漏 +2 会怎样
上三角约 n²/2 个格子、每格一次比较,时间 O(n²);存整张二维表,空间 O(n²),压成一维滚动数组(只留最近一行、循环覆盖旧值)可降到 O(n)。两个边界别弄错:填表顺序按行列填会读到还没算的长区间,必须按区间长度递增;长度 2 的区间相等时内部空记 0、+2 得 2,别漏这个 +2。
▶ 动画逐步走查(共 27 步)——想跟着动画一帧帧对照就展开
- 3核心就这一个表:行是左端 i、列是右端 j。我们按「区间从短到长」填,短区间先算好,长区间才有依赖可用。
- 4先铺对角线:每个单字符 'b' 自己就是一个回文,dp[0][0] = 1。这是最短区间,作为后面长区间的基石。
- 5先铺对角线:每个单字符 'b' 自己就是一个回文,dp[1][1] = 1。这是最短区间,作为后面长区间的基石。
- 6先铺对角线:每个单字符 'b' 自己就是一个回文,dp[2][2] = 1。这是最短区间,作为后面长区间的基石。
- 7先铺对角线:每个单字符 'a' 自己就是一个回文,dp[3][3] = 1。这是最短区间,作为后面长区间的基石。
- 8先铺对角线:每个单字符 'b' 自己就是一个回文,dp[4][4] = 1。这是最短区间,作为后面长区间的基石。
- 9两端 'b' 和 'b' 相等:把它俩套在外面,里面 s[1..0] 的最长回文(0)直接 +2。两端一对相等字符,能让回文加长 2。
- 10落子:dp[0][1] = 2。区间 s[0..1]("bb")的最长回文子序列长度。
- 11两端 'b' 和 'b' 相等:把它俩套在外面,里面 s[2..1] 的最长回文(0)直接 +2。两端一对相等字符,能让回文加长 2。
- 12落子:dp[1][2] = 2。区间 s[1..2]("bb")的最长回文子序列长度。
- 13两端 'b' 和 'a' 不相等:这俩没法同时当回文的两头,只能丢一个——丢左端看 dp[3][3]=1,丢右端看 dp[2][2]=1,取大的。
- 14落子:dp[2][3] = 1(取了较大的那条路)。
- 15两端 'a' 和 'b' 不相等:这俩没法同时当回文的两头,只能丢一个——丢左端看 dp[4][4]=1,丢右端看 dp[3][3]=1,取大的。
- 16落子:dp[3][4] = 1(取了较大的那条路)。
- 17两端 'b' 和 'b' 相等:把它俩套在外面,里面 s[1..1] 的最长回文(1)直接 +2。两端一对相等字符,能让回文加长 2。
- 18落子:dp[0][2] = 3。区间 s[0..2]("bbb")的最长回文子序列长度。
- 19两端 'b' 和 'a' 不相等:这俩没法同时当回文的两头,只能丢一个——丢左端看 dp[2][3]=1,丢右端看 dp[1][2]=2,取大的。
- 20落子:dp[1][3] = 2(取了较大的那条路)。
- 21两端 'b' 和 'b' 相等:把它俩套在外面,里面 s[3..3] 的最长回文(1)直接 +2。两端一对相等字符,能让回文加长 2。
- 22落子:dp[2][4] = 3。区间 s[2..4]("bab")的最长回文子序列长度。
- 23两端 'b' 和 'a' 不相等:这俩没法同时当回文的两头,只能丢一个——丢左端看 dp[1][3]=2,丢右端看 dp[0][2]=3,取大的。
- 24落子:dp[0][3] = 3(取了较大的那条路)。
- 25两端 'b' 和 'b' 相等:把它俩套在外面,里面 s[2..3] 的最长回文(1)直接 +2。两端一对相等字符,能让回文加长 2。
- 26落子:dp[1][4] = 3。区间 s[1..4]("bbab")的最长回文子序列长度。
- 27两端 'b' 和 'b' 相等:把它俩套在外面,里面 s[1..3] 的最长回文(2)直接 +2。两端一对相等字符,能让回文加长 2。
- 28落子:dp[0][4] = 4。区间 s[0..4]("bbbab")的最长回文子序列长度。
- 29右上角 dp[0][4] = 4,就是整个串 "bbbab" 的最长回文子序列长度(即 "bbbb",跳过中间的 a)。
⚠️ 容易写错的地方
✗ 错:按行/列顺序填
✓ 对:按区间长度从短到长填
dp[i][j] 依赖内部更短区间,短的必须先算好
✗ 错:len==2 相等忘记 +2
✓ 对:内部为空记 0,再 +2 得 2
两个相同字符本身就是长度 2 的回文
✗ 错:和「最长回文子串」混淆
✓ 对:子序列可跳字符、不要求连续
子串要求连续,转移与边界都不同
完整代码(Python / C++ / Java)
Python
def longestPalindromeSubseq(s):
n = len(s)
dp = [[0]*n for _ in range(n)]
for i in range(n): dp[i][i] = 1
for L in range(2, n+1):
for i in range(0, n-L+1):
j = i + L - 1
if s[i] == s[j]:
dp[i][j] = (dp[i+1][j-1] if i+1<=j-1 else 0) + 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
return dp[0][n-1]C++
int longestPalindromeSubseq(string s){
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for(int i = 0; i < n; i++) dp[i][i] = 1;
for(int L = 2; L <= n; L++)
for(int i = 0; i + L - 1 < n; i++){
int j = i + L - 1;
if(s[i] == s[j]) dp[i][j] = (i+1<=j-1 ? dp[i+1][j-1] : 0) + 2;
else dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
return dp[0][n-1];
}Java
int longestPalindromeSubseq(String s){
int n = s.length();
int[][] dp = new int[n][n];
for (int i = 0; i < n; i++) dp[i][i] = 1;
for (int L = 2; L <= n; L++)
for (int i = 0; i + L - 1 < n; i++) {
int j = i + L - 1;
if (s.charAt(i) == s.charAt(j))
dp[i][j] = (i+1<=j-1 ? dp[i+1][j-1] : 0) + 2;
else dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
}
return dp[0][n-1];复杂度
时间
O(n²)
上三角约 n²/2 格,每格 O(1)
空间
O(n²)
整张二维表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长回文子序列 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么必须按区间长度从短到长填,不能逐行填?+
dp[i][j] 依赖 dp[i+1][j-1]、dp[i+1][j]、dp[i][j-1],这三个都是比区间 [i,j] 更短的区间。按区间长度从小到大填,能保证用到它们时早已算好。若按行从上到下、从左到右填,算 dp[i][j] 时 dp[i+1] 那一行还没填,读到的是空值,结果全错。等价的正确写法是外层 i 从 n-1 递减到 0、内层 j 从 i+1 递增,本质也是先短后长。
这题能优化空间吗?+
能。dp[i][j] 只用到下一行 i+1 和本行左边 j-1 的值,更早的行再也用不上。把二维表压成一行、用一维数组就地覆盖,再配一个临时变量存住 dp[i+1][j-1] 那个即将被覆盖的左下角值,空间就从 O(n²) 降到 O(n),时间仍是 O(n²)。
和最长回文子串(LeetCode 5)到底差在哪?+
最长回文子串要求选出的字符在原串里连续、中间不能跳;最长回文子序列允许跳字符、只保留相对顺序。要求不同转移也不同:子串常用中心扩展、或 dp[i][j] 记 s[i..j] 是否为回文的布尔值,子序列用 dp[i][j] 记最长回文长度做区间 DP。bbbab 里子串答案是 bbb 或 bab(长度 3),子序列答案是 bbbb(长度 4),可跳字符让子序列往往更长。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长回文子序列 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。