统计不同子序列的个数(带限制版本)
字数 6050 2025-12-12 13:05:48

统计不同子序列的个数(带限制版本)

题目描述

给定一个字符串 s 和一个目标字符串 t,你需要计算在 s 的子序列中等于 t 的不同子序列的个数。注意,这里的“子序列”指的是从原始字符串中删除一些字符(也可以不删除)后,剩下的字符在不改变相对顺序的情况下组成的新字符串。即使两个子序列在 s 中占据的位置(下标)不同,但只要构成的字符串相同,就算作相同的子序列。你需要统计的是等于 t 的、不同的子序列个数。

输入格式:两个字符串 s 和 t,长度分别为 n 和 m,其中 1 <= m <= n <= 1000。
输出格式:一个整数,表示 s 的子序列中等于 t 的子序列个数。由于结果可能很大,请返回结果对 10^9 + 7 取模的值。

例如:
s = "rabbbit", t = "rabbit"
输出:3
解释:s 中有三种方式可以得到 "rabbit":

  1. rabb b it
  2. ra b bbit
  3. rab b bit
    (用空格和加粗表示选取的字符位置)

解题过程

这是一个经典的序列匹配问题,但要求统计不同的匹配方式(即使匹配位置不同,只要对应相同的子序列就算一种)。由于我们统计的是不同的子序列(即匹配的字符串相同就算一种),所以这实际上问的是:s 中有多少种不同的方法(选取字符的位置集合)可以组成 t。由于我们统计的是不同的位置选择,即使最终得到的字符串相同,但选取的位置不同,也算作不同的子序列。然而,题目中“不同的子序列”指的是字符串不同,而不是选取的位置不同。但在这个经典问题中,通常我们计算的是不同的位置选择的数量,即有多少种不同的下标选择方式,使得选出的字符序列恰好等于 t。题目描述中“不同的子序列”可能有些歧义,但在算法竞赛和面试中,这个问题通常定义为“统计 s 的子序列中等于 t 的个数”,而“个数”是指不同的选取方式的数量(即使得到的字符串相同,但选取的下标组合不同也算不同的方式)。为了明确,我们采用标准定义:求 s 中有多少种不同的下标选择方案,使得选出的字符按顺序连起来等于 t

1. 问题分析

我们有两个字符串:

  • s[1..n] (假设索引从1开始,方便讲解)
  • t[1..m]

我们需要从 s 中选出一些字符(保持原有顺序),形成一个长度为 m 的子序列,使其恰好等于 t。问有多少种选法。

2. 暴力思路

一种最直接的方法是枚举 s 中所有长度为 m 的子序列(通过递归或回溯),检查它是否等于 t。这样的子序列有 C(n, m) 个,当 n 较大时(如 1000)无法承受。我们需要一个高效的方法。

3. 动态规划状态定义

这是一个典型的“两个序列”匹配问题,可以使用二维动态规划。定义状态 dp[i][j] 表示:考虑 s 的前 i 个字符(s[1..i])和 t 的前 j 个字符(t[1..j]),在 s[1..i] 的子序列中等于 t[1..j] 的个数。

最终答案就是 dp[n][m]。

4. 状态转移方程

我们考虑如何从已知的状态推导出 dp[i][j]。分两种情况讨论,取决于我们是否使用 s 的第 i 个字符来匹配 t 的第 j 个字符。

  • 情况1:不使用 s[i] 来匹配 t[j]
    这意味着我们在 s[1..i-1] 中就已经找到了所有匹配 t[1..j] 的方式。所以这部分的数量是 dp[i-1][j]。

  • 情况2:使用 s[i] 来匹配 t[j]
    这有一个前提:s[i] 必须等于 t[j]。如果相等,那么我们可以用 s[i] 去匹配 t[j],然后问题就转化为:在 s[1..i-1] 中找子序列匹配 t[1..j-1]。所以这部分的数量是 dp[i-1][j-1](前提是 s[i] == t[j])。

综合两种情况:

  • 如果 s[i] != t[j],那么我们只能不使用 s[i],所以 dp[i][j] = dp[i-1][j]。
  • 如果 s[i] == t[j],那么我们有两种选择:用 s[i] 或者不用 s[i]。所以 dp[i][j] = dp[i-1][j] + dp[i-1][j-1]。

5. 边界条件

我们需要初始化 dp 数组,使得转移能够正确进行。

  • 当 j = 0 时,t 的前 0 个字符是空串。空串是任何字符串的子序列,而且只有一种方式(什么都不选)。所以对于任意 i (0 <= i <= n),dp[i][0] = 1。
  • 当 i = 0 且 j > 0 时,s 是空串,而 t 非空,显然无法匹配。所以 dp[0][j] = 0 (j > 0)。
  • 当 i=0, j=0 时,两个空串匹配,dp[0][0] = 1。

6. 递推计算

我们可以按照 i 从 0 到 n,j 从 0 到 m 的顺序计算 dp[i][j]。注意,当 i < j 时,s 的前 i 个字符长度小于 t 的前 j 个字符,显然不可能匹配,dp[i][j] = 0。但这其实在递推中会自然得出(因为 dp[i-1][j] 和 dp[i-1][j-1] 可能为0)。

伪代码:

定义 dp[0..n][0..m] 初始化为0
for i from 0 to n:
    dp[i][0] = 1
for i from 1 to n:
    for j from 1 to m:
        dp[i][j] = dp[i-1][j]
        if s[i] == t[j]:
            dp[i][j] = dp[i][j] + dp[i-1][j-1]
        结果取模
返回 dp[n][m]

7. 示例推演

以 s = "rabbbit", t = "rabbit" 为例(长度 n=7, m=6)。为直观,我们用字符数组,索引从1开始。
s: 1:r 2:a 3:b 4:b 5:b 6:i 7:t
t: 1:r 2:a 3:b 4:b 5:i 6:t

初始化 dp[i][0]=1, dp[0][j]=0 (j>0)。

i=1 (s[1]='r'):
j=1: s[1]=='r' -> dp[1][1] = dp[0][1] + dp[0][0] = 0+1=1
j>1: 均为0

i=2 (s[2]='a'):
j=1: s[2]!='r' -> dp[2][1]=dp[1][1]=1
j=2: s[2]=='a' -> dp[2][2]=dp[1][2]+dp[1][1]=0+1=1
j>2: 0

i=3 (s[3]='b'):
j=1: !='r' -> dp[3][1]=dp[2][1]=1
j=2: !='a' -> dp[3][2]=dp[2][2]=1
j=3: =='b' -> dp[3][3]=dp[2][3]+dp[2][2]=0+1=1
j=4: !='b' (注意t[4]是'b')? 不,t[4]是'b',但此时j=4,s[3]=='b'==t[4]? 不对,t[4]是第4个字符,但我们在j=4时,比较的是t[4]='b',s[3]=='b'相等,但dp[2][3]和dp[2][4]? 我们按照公式,当s[i]==t[j]时,用dp[i-1][j-1]。这里j=4,s[3]==t[4]成立,所以dp[3][4]=dp[2][4]+dp[2][3]=0+0=0。因为dp[2][4]和dp[2][3]都是0。
继续计算...

为了节省篇幅,我们直接给出最终dp表的一部分关键值:
dp[3][3]=1 (匹配"rab")
dp[4][4]=? 当i=4,s[4]='b',j=4,t[4]='b'相等,dp[4][4]=dp[3][4]+dp[3][3]=0+1=1
dp[5][4]=? i=5,s[5]='b', j=4,t[4]='b',dp[5][4]=dp[4][4]+dp[4][3]=1+1=2
dp[6][5]=? i=6,s[6]='i', j=5,t[5]='i',dp[6][5]=dp[5][5]+dp[5][4]=0+2=2
dp[7][6]=? i=7,s[7]='t', j=6,t[6]='t',dp[7][6]=dp[6][6]+dp[6][5]=0+2=2? 等一下,这里dp[6][6]应该是0,因为t[6]='t',s[6]='i'不等,所以dp[6][6]=dp[5][6]=0。然后dp[6][5]=2,所以dp[7][6]=0+2=2?但答案是3。我们发现计算有误。

仔细检查,t的长度是6,我们的dp表应该是dp[7][6]。实际上,在i=7时,s[7]='t'匹配t[6]='t',所以dp[7][6]=dp[6][6] + dp[6][5]。dp[6][6]表示不用s[7]时,s[1..6]匹配t[1..6]的数量,这个应该是0(因为s[1..6]="rabbbi",最后是i不是t)。dp[6][5]表示s[1..6]匹配t[1..5]的数量,我们之前算得是2。所以dp[7][6]=0+2=2,但答案是3,说明dp[6][5]应该为3才对。

我们重新仔细推导一遍:

建立dp表,行i从0到7,列j从0到6。

初始化:
dp[i][0]=1 for all i.
dp[0][j]=0 for j>0.

i=1, s[1]=r
j=1: s[1]==t[1]? r==r -> dp[1][1]=dp[0][1]+dp[0][0]=0+1=1
j=2..6: dp[1][j]=dp[0][j]=0 (因为s[1]!=t[j] for j>=2)

i=2, s[2]=a
j=1: s[2]!=t[1] -> dp[2][1]=dp[1][1]=1
j=2: s[2]==t[2]? a==a -> dp[2][2]=dp[1][2]+dp[1][1]=0+1=1
j=3..6: dp[2][j]=dp[1][j]=0

i=3, s[3]=b
j=1: !=r -> dp[3][1]=dp[2][1]=1
j=2: !=a -> dp[3][2]=dp[2][2]=1
j=3: s[3]==t[3]? b==b -> dp[3][3]=dp[2][3]+dp[2][2]=0+1=1
j=4: s[3]==t[4]? b==b -> dp[3][4]=dp[2][4]+dp[2][3]=0+0=0
j=5: !=i -> dp[3][5]=dp[2][5]=0
j=6: !=t -> dp[3][6]=dp[2][6]=0

i=4, s[4]=b
j=1: !=r -> dp[4][1]=dp[3][1]=1
j=2: !=a -> dp[4][2]=dp[3][2]=1
j=3: s[4]==t[3]? b==b -> dp[4][3]=dp[3][3]+dp[3][2]=1+1=2
j=4: s[4]==t[4]? b==b -> dp[4][4]=dp[3][4]+dp[3][3]=0+1=1
j=5: !=i -> dp[4][5]=dp[3][5]=0
j=6: !=t -> dp[4][6]=dp[3][6]=0

i=5, s[5]=b
j=1: !=r -> dp[5][1]=dp[4][1]=1
j=2: !=a -> dp[5][2]=dp[4][2]=1
j=3: s[5]==t[3]? b==b -> dp[5][3]=dp[4][3]+dp[4][2]=2+1=3
j=4: s[5]==t[4]? b==b -> dp[5][4]=dp[4][4]+dp[4][3]=1+2=3
j=5: !=i -> dp[5][5]=dp[4][5]=0
j=6: !=t -> dp[5][6]=dp[4][6]=0

i=6, s[6]=i
j=1: !=r -> dp[6][1]=dp[5][1]=1
j=2: !=a -> dp[6][2]=dp[5][2]=1
j=3: !=b -> dp[6][3]=dp[5][3]=3
j=4: !=b -> dp[6][4]=dp[5][4]=3
j=5: s[6]==t[5]? i==i -> dp[6][5]=dp[5][5]+dp[5][4]=0+3=3
j=6: !=t -> dp[6][6]=dp[5][6]=0

i=7, s[7]=t
j=1: !=r -> dp[7][1]=dp[6][1]=1
j=2: !=a -> dp[7][2]=dp[6][2]=1
j=3: !=b -> dp[7][3]=dp[6][3]=3
j=4: !=b -> dp[7][4]=dp[6][4]=3
j=5: !=i -> dp[7][5]=dp[6][5]=3
j=6: s[7]==t[6]? t==t -> dp[7][6]=dp[6][6]+dp[6][5]=0+3=3

最终 dp[7][6] = 3,与答案一致。

8. 空间优化

观察状态转移方程,dp[i][j] 只依赖于 dp[i-1][j] 和 dp[i-1][j-1]。所以我们可以使用滚动数组,将第一维压缩,只保留一维数组 dp[0..m]。但注意,由于我们需要用到上一行的 dp[j-1],所以在更新 dp[j] 时,应该从 j=m 到 1 逆序更新,以免覆盖上一行的值。

伪代码:

初始化 dp[0..m],dp[0]=1, dp[1..m]=0
for i from 1 to n:
    for j from m downto 1:
        if s[i] == t[j]:
            dp[j] = (dp[j] + dp[j-1]) % MOD
        # 如果不等,dp[j]保持不变(即上一轮的dp[j])

注意:当 s[i] != t[j] 时,dp[j] 保持不变,这正好对应了 dp[i][j] = dp[i-1][j]。因为逆序更新,dp[j] 在本次循环中还没有被更新,所以它仍然是上一轮(i-1)的值。

9. 总结

这个问题的核心在于定义 dp[i][j] 为 s 前 i 个字符中匹配 t 前 j 个字符的子序列个数。状态转移分为使用或不使用 s[i] 两种。初始化时,匹配空串有1种方式。最终通过递推得到结果。空间可以优化到 O(m)。注意取模。

这个算法的时间复杂度是 O(n*m),空间复杂度 O(m),可以处理 n, m 达到几千的问题。

统计不同子序列的个数(带限制版本) 题目描述 给定一个字符串 s 和一个目标字符串 t,你需要计算在 s 的子序列中等于 t 的不同子序列的个数。注意,这里的“子序列”指的是从原始字符串中删除一些字符(也可以不删除)后,剩下的字符在不改变相对顺序的情况下组成的新字符串。即使两个子序列在 s 中占据的位置(下标)不同,但只要构成的字符串相同,就算作相同的子序列。你需要统计的是等于 t 的、不同的子序列个数。 输入格式 :两个字符串 s 和 t,长度分别为 n 和 m,其中 1 <= m <= n <= 1000。 输出格式 :一个整数,表示 s 的子序列中等于 t 的子序列个数。由于结果可能很大,请返回结果对 10^9 + 7 取模的值。 例如: s = "rabbbit", t = "rabbit" 输出:3 解释:s 中有三种方式可以得到 "rabbit": rabb b it ra b bbit rab b bit (用空格和加粗表示选取的字符位置) 解题过程 这是一个经典的序列匹配问题,但要求统计不同的匹配方式(即使匹配位置不同,只要对应相同的子序列就算一种)。由于我们统计的是不同的子序列(即匹配的字符串相同就算一种),所以这实际上问的是:s 中有多少种 不同 的方法(选取字符的位置集合)可以组成 t。由于我们统计的是不同的 位置选择 ,即使最终得到的字符串相同,但选取的位置不同,也算作不同的子序列。然而,题目中“不同的子序列”指的是字符串不同,而不是选取的位置不同。但在这个经典问题中,通常我们计算的是 不同的位置选择 的数量,即有多少种不同的下标选择方式,使得选出的字符序列恰好等于 t。题目描述中“不同的子序列”可能有些歧义,但在算法竞赛和面试中,这个问题通常定义为“统计 s 的子序列中等于 t 的个数”,而“个数”是指不同的选取方式的数量(即使得到的字符串相同,但选取的下标组合不同也算不同的方式)。为了明确,我们采用标准定义: 求 s 中有多少种不同的下标选择方案,使得选出的字符按顺序连起来等于 t 。 1. 问题分析 我们有两个字符串: s[ 1..n ] (假设索引从1开始,方便讲解) t[ 1..m ] 我们需要从 s 中选出一些字符(保持原有顺序),形成一个长度为 m 的子序列,使其恰好等于 t。问有多少种选法。 2. 暴力思路 一种最直接的方法是枚举 s 中所有长度为 m 的子序列(通过递归或回溯),检查它是否等于 t。这样的子序列有 C(n, m) 个,当 n 较大时(如 1000)无法承受。我们需要一个高效的方法。 3. 动态规划状态定义 这是一个典型的“两个序列”匹配问题,可以使用二维动态规划。定义状态 dp[ i][ j] 表示:考虑 s 的前 i 个字符(s[ 1..i])和 t 的前 j 个字符(t[ 1..j]),在 s[ 1..i] 的子序列中等于 t[ 1..j ] 的个数。 最终答案就是 dp[ n][ m ]。 4. 状态转移方程 我们考虑如何从已知的状态推导出 dp[ i][ j ]。分两种情况讨论,取决于我们是否使用 s 的第 i 个字符来匹配 t 的第 j 个字符。 情况1:不使用 s[ i] 来匹配 t[ j] 这意味着我们在 s[ 1..i-1] 中就已经找到了所有匹配 t[ 1..j] 的方式。所以这部分的数量是 dp[ i-1][ j ]。 情况2:使用 s[ i] 来匹配 t[ j] 这有一个前提:s[ i] 必须等于 t[ j]。如果相等,那么我们可以用 s[ i] 去匹配 t[ j],然后问题就转化为:在 s[ 1..i-1] 中找子序列匹配 t[ 1..j-1]。所以这部分的数量是 dp[ i-1][ j-1](前提是 s[ i] == t[ j ])。 综合两种情况: 如果 s[ i] != t[ j],那么我们只能不使用 s[ i],所以 dp[ i][ j] = dp[ i-1][ j ]。 如果 s[ i] == t[ j],那么我们有两种选择:用 s[ i] 或者不用 s[ i]。所以 dp[ i][ j] = dp[ i-1][ j] + dp[ i-1][ j-1 ]。 5. 边界条件 我们需要初始化 dp 数组,使得转移能够正确进行。 当 j = 0 时,t 的前 0 个字符是空串。空串是任何字符串的子序列,而且只有一种方式(什么都不选)。所以对于任意 i (0 <= i <= n),dp[ i][ 0 ] = 1。 当 i = 0 且 j > 0 时,s 是空串,而 t 非空,显然无法匹配。所以 dp[ 0][ j ] = 0 (j > 0)。 当 i=0, j=0 时,两个空串匹配,dp[ 0][ 0 ] = 1。 6. 递推计算 我们可以按照 i 从 0 到 n,j 从 0 到 m 的顺序计算 dp[ i][ j]。注意,当 i < j 时,s 的前 i 个字符长度小于 t 的前 j 个字符,显然不可能匹配,dp[ i][ j] = 0。但这其实在递推中会自然得出(因为 dp[ i-1][ j] 和 dp[ i-1][ j-1 ] 可能为0)。 伪代码: 7. 示例推演 以 s = "rabbbit", t = "rabbit" 为例(长度 n=7, m=6)。为直观,我们用字符数组,索引从1开始。 s: 1:r 2:a 3:b 4:b 5:b 6:i 7:t t: 1:r 2:a 3:b 4:b 5:i 6:t 初始化 dp[ i][ 0]=1, dp[ 0][ j ]=0 (j>0)。 i=1 (s[ 1 ]='r'): j=1: s[ 1]=='r' -> dp[ 1][ 1] = dp[ 0][ 1] + dp[ 0][ 0 ] = 0+1=1 j>1: 均为0 i=2 (s[ 2 ]='a'): j=1: s[ 2]!='r' -> dp[ 2][ 1]=dp[ 1][ 1 ]=1 j=2: s[ 2]=='a' -> dp[ 2][ 2]=dp[ 1][ 2]+dp[ 1][ 1 ]=0+1=1 j>2: 0 i=3 (s[ 3 ]='b'): j=1: !='r' -> dp[ 3][ 1]=dp[ 2][ 1 ]=1 j=2: !='a' -> dp[ 3][ 2]=dp[ 2][ 2 ]=1 j=3: =='b' -> dp[ 3][ 3]=dp[ 2][ 3]+dp[ 2][ 2 ]=0+1=1 j=4: !='b' (注意t[ 4]是'b')? 不,t[ 4]是'b',但此时j=4,s[ 3]=='b'==t[ 4]? 不对,t[ 4]是第4个字符,但我们在j=4时,比较的是t[ 4]='b',s[ 3]=='b'相等,但dp[ 2][ 3]和dp[ 2][ 4]? 我们按照公式,当s[ i]==t[ j]时,用dp[ i-1][ j-1]。这里j=4,s[ 3]==t[ 4]成立,所以dp[ 3][ 4]=dp[ 2][ 4]+dp[ 2][ 3]=0+0=0。因为dp[ 2][ 4]和dp[ 2][ 3 ]都是0。 继续计算... 为了节省篇幅,我们直接给出最终dp表的一部分关键值: dp[ 3][ 3 ]=1 (匹配"rab") dp[ 4][ 4]=? 当i=4,s[ 4]='b',j=4,t[ 4]='b'相等,dp[ 4][ 4]=dp[ 3][ 4]+dp[ 3][ 3 ]=0+1=1 dp[ 5][ 4]=? i=5,s[ 5]='b', j=4,t[ 4]='b',dp[ 5][ 4]=dp[ 4][ 4]+dp[ 4][ 3 ]=1+1=2 dp[ 6][ 5]=? i=6,s[ 6]='i', j=5,t[ 5]='i',dp[ 6][ 5]=dp[ 5][ 5]+dp[ 5][ 4 ]=0+2=2 dp[ 7][ 6]=? i=7,s[ 7]='t', j=6,t[ 6]='t',dp[ 7][ 6]=dp[ 6][ 6]+dp[ 6][ 5]=0+2=2? 等一下,这里dp[ 6][ 6]应该是0,因为t[ 6]='t',s[ 6]='i'不等,所以dp[ 6][ 6]=dp[ 5][ 6]=0。然后dp[ 6][ 5]=2,所以dp[ 7][ 6 ]=0+2=2?但答案是3。我们发现计算有误。 仔细检查,t的长度是6,我们的dp表应该是dp[ 7][ 6]。实际上,在i=7时,s[ 7]='t'匹配t[ 6]='t',所以dp[ 7][ 6]=dp[ 6][ 6] + dp[ 6][ 5]。dp[ 6][ 6]表示不用s[ 7]时,s[ 1..6]匹配t[ 1..6]的数量,这个应该是0(因为s[ 1..6]="rabbbi",最后是i不是t)。dp[ 6][ 5]表示s[ 1..6]匹配t[ 1..5]的数量,我们之前算得是2。所以dp[ 7][ 6]=0+2=2,但答案是3,说明dp[ 6][ 5 ]应该为3才对。 我们重新仔细推导一遍: 建立dp表,行i从0到7,列j从0到6。 初始化: dp[ i][ 0 ]=1 for all i. dp[ 0][ j ]=0 for j>0. i=1, s[ 1 ]=r j=1: s[ 1]==t[ 1]? r==r -> dp[ 1][ 1]=dp[ 0][ 1]+dp[ 0][ 0 ]=0+1=1 j=2..6: dp[ 1][ j]=dp[ 0][ j]=0 (因为s[ 1]!=t[ j ] for j>=2) i=2, s[ 2 ]=a j=1: s[ 2]!=t[ 1] -> dp[ 2][ 1]=dp[ 1][ 1 ]=1 j=2: s[ 2]==t[ 2]? a==a -> dp[ 2][ 2]=dp[ 1][ 2]+dp[ 1][ 1 ]=0+1=1 j=3..6: dp[ 2][ j]=dp[ 1][ j ]=0 i=3, s[ 3 ]=b j=1: !=r -> dp[ 3][ 1]=dp[ 2][ 1 ]=1 j=2: !=a -> dp[ 3][ 2]=dp[ 2][ 2 ]=1 j=3: s[ 3]==t[ 3]? b==b -> dp[ 3][ 3]=dp[ 2][ 3]+dp[ 2][ 2 ]=0+1=1 j=4: s[ 3]==t[ 4]? b==b -> dp[ 3][ 4]=dp[ 2][ 4]+dp[ 2][ 3 ]=0+0=0 j=5: !=i -> dp[ 3][ 5]=dp[ 2][ 5 ]=0 j=6: !=t -> dp[ 3][ 6]=dp[ 2][ 6 ]=0 i=4, s[ 4 ]=b j=1: !=r -> dp[ 4][ 1]=dp[ 3][ 1 ]=1 j=2: !=a -> dp[ 4][ 2]=dp[ 3][ 2 ]=1 j=3: s[ 4]==t[ 3]? b==b -> dp[ 4][ 3]=dp[ 3][ 3]+dp[ 3][ 2 ]=1+1=2 j=4: s[ 4]==t[ 4]? b==b -> dp[ 4][ 4]=dp[ 3][ 4]+dp[ 3][ 3 ]=0+1=1 j=5: !=i -> dp[ 4][ 5]=dp[ 3][ 5 ]=0 j=6: !=t -> dp[ 4][ 6]=dp[ 3][ 6 ]=0 i=5, s[ 5 ]=b j=1: !=r -> dp[ 5][ 1]=dp[ 4][ 1 ]=1 j=2: !=a -> dp[ 5][ 2]=dp[ 4][ 2 ]=1 j=3: s[ 5]==t[ 3]? b==b -> dp[ 5][ 3]=dp[ 4][ 3]+dp[ 4][ 2 ]=2+1=3 j=4: s[ 5]==t[ 4]? b==b -> dp[ 5][ 4]=dp[ 4][ 4]+dp[ 4][ 3 ]=1+2=3 j=5: !=i -> dp[ 5][ 5]=dp[ 4][ 5 ]=0 j=6: !=t -> dp[ 5][ 6]=dp[ 4][ 6 ]=0 i=6, s[ 6 ]=i j=1: !=r -> dp[ 6][ 1]=dp[ 5][ 1 ]=1 j=2: !=a -> dp[ 6][ 2]=dp[ 5][ 2 ]=1 j=3: !=b -> dp[ 6][ 3]=dp[ 5][ 3 ]=3 j=4: !=b -> dp[ 6][ 4]=dp[ 5][ 4 ]=3 j=5: s[ 6]==t[ 5]? i==i -> dp[ 6][ 5]=dp[ 5][ 5]+dp[ 5][ 4 ]=0+3=3 j=6: !=t -> dp[ 6][ 6]=dp[ 5][ 6 ]=0 i=7, s[ 7 ]=t j=1: !=r -> dp[ 7][ 1]=dp[ 6][ 1 ]=1 j=2: !=a -> dp[ 7][ 2]=dp[ 6][ 2 ]=1 j=3: !=b -> dp[ 7][ 3]=dp[ 6][ 3 ]=3 j=4: !=b -> dp[ 7][ 4]=dp[ 6][ 4 ]=3 j=5: !=i -> dp[ 7][ 5]=dp[ 6][ 5 ]=3 j=6: s[ 7]==t[ 6]? t==t -> dp[ 7][ 6]=dp[ 6][ 6]+dp[ 6][ 5 ]=0+3=3 最终 dp[ 7][ 6 ] = 3,与答案一致。 8. 空间优化 观察状态转移方程,dp[ i][ j] 只依赖于 dp[ i-1][ j] 和 dp[ i-1][ j-1]。所以我们可以使用滚动数组,将第一维压缩,只保留一维数组 dp[ 0..m]。但注意,由于我们需要用到上一行的 dp[ j-1],所以在更新 dp[ j ] 时,应该从 j=m 到 1 逆序更新,以免覆盖上一行的值。 伪代码: 注意:当 s[ i] != t[ j] 时,dp[ j] 保持不变,这正好对应了 dp[ i][ j] = dp[ i-1][ j]。因为逆序更新,dp[ j ] 在本次循环中还没有被更新,所以它仍然是上一轮(i-1)的值。 9. 总结 这个问题的核心在于定义 dp[ i][ j] 为 s 前 i 个字符中匹配 t 前 j 个字符的子序列个数。状态转移分为使用或不使用 s[ i ] 两种。初始化时,匹配空串有1种方式。最终通过递推得到结果。空间可以优化到 O(m)。注意取模。 这个算法的时间复杂度是 O(n* m),空间复杂度 O(m),可以处理 n, m 达到几千的问题。