6.5. 最长连续回文子序列

题目描述

「连续回文子序列」指形如 a、aba、abcba、abcdcba、... 、abcde...z...edcba 回文子序列。给定一个仅由小写字母组成的字符串 origin,求 origin 中最长「连续回文子序列」的长度,以及 origin 中等于该长度的「连续回文子序列」的个数。若 origin 中不存在「连续回文子序列」请返回 [0,0]。

注意:由于最长「连续回文子序列」的个数可能很多,所以你还需要将其对 1000000007 取模。

示例 1:

输入:origin = "ababcbaa"
输出:[5,6]
解释:"ababcbaa" 中最长的连续回文子序列为 "abcba","ababcbaa" 中长度等于 5 的连续回文子序列有 6 个。

示例 2:

输入:origin = "abcdefedcba"
输出:[11,1]

示例 3:

输入:origin = "zxcvbnmqis"
输出:[0,0]

提示:

  • 1 <= origin.length <= 10^5
  • origin[i] 仅为小写字母

问题分析

什么是"连续回文子序列"?

连续回文子序列必须满足以下特征:

  1. 连续字母:由连续的字母组成,如 a、aba、abcba、abcdcba
  2. 回文性质:正读反读相同
  3. 完整扩展:必须能从某个中心字符向外完全扩展到字符'a'

关键理解: 连续回文子序列的形式为:

  • a (中心为'a',长度1)
  • aba (中心为'b',向外扩展到'a',长度3)
  • abcba (中心为'c',向外扩展到'a',长度5)
  • abcdcba (中心为'd',向外扩展到'a',长度7)

例如:

  • ✅ "a" - 单字符回文
  • ✅ "aba" - 从'b'扩展到'a'
  • ✅ "abcba" - 从'c'扩展到'a'
  • ❌ "bcb" - 无法扩展到'a'
  • ❌ "ace" - 不是连续字母

算法核心思想

第一步:预处理字符位置

构建两个二维数组来快速查找字符位置:

  • pre[i][c]:位置i之前字符c的最近出现位置
  • next[i][c]:位置i之后字符c的最近出现位置

第二步:寻找最大可扩展字符

遍历每个位置,检查以该位置字符为中心能否向外完全扩展到字符'a'。

第三步:动态规划计数

使用一维DP数组计算符合条件的回文子序列数量。

Java实现

import java.util.Arrays;

class Solution {
    public static final int MOD = 1000000007;

    public int[] solve(String origin) {
        char[] s = origin.toCharArray();
        int n = s.length;
        
        // 预处理:构建字符位置查找表
        int[][] pre = new int[n][26];   // pre[i][c]: 位置i之前字符c的最近位置
        int[][] next = new int[n][26];  // next[i][c]: 位置i之后字符c的最近位置
        
        // 初始化为-1表示不存在
        for (int[] temp : pre)
            Arrays.fill(temp, -1);
        for (int[] temp : next)
            Arrays.fill(temp, -1);
        
        // 构建pre数组:从左到右扫描
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < 26; j++)
                pre[i][j] = pre[i-1][j];  // 继承前一位置的信息
            pre[i][s[i-1] - 'a'] = i - 1;  // 更新前一个字符的位置
        }
        
        // 构建next数组:从右到左扫描
        for (int i = n - 2; i >= 0; i--) {
            for (int j = 0; j < 26; j++)
                next[i][j] = next[i+1][j];  // 继承后一位置的信息
            next[i][s[i+1] - 'a'] = i + 1;  // 更新后一个字符的位置
        }
        
        // 寻找最大可扩展字符
        int idx = -1;  // 最大可扩展字符的编号(0='a', 1='b', ...)
        
        for (int i = 0; i < n; i++) {
            boolean flag = true;
            int now = s[i] - 'a';  // 当前字符编号
            int ptr1 = i;  // 向右扩展指针
            int ptr2 = i;  // 向左扩展指针
            
            // 检查能否向外扩展到字符'a'
            for (int j = now - 1; j >= 0; j--) {
                ptr1 = next[ptr1][j];  // 向右寻找字符j
                ptr2 = pre[ptr2][j];   // 向左寻找字符j
                if (ptr1 == -1 || ptr2 == -1) {
                    flag = false;  // 无法找到,扩展失败
                    break;
                }
            }
            
            if (!flag)
                continue;
            
            idx = Math.max(idx, now);  // 更新最大可扩展字符
        }
        
        if (idx == -1)
            return new int[]{0, 0};  // 无法构成连续回文子序列
        
        // 动态规划计数
        int ans = idx * 2 + 1;  // 回文序列长度
        long[] dp = new long[ans];  // dp[i]: 构建到回文序列第i个位置的方案数
        
        for (int i = 0; i < n; i++) {
            int now = s[i] - 'a';
            if (now > idx)
                continue;  // 超出范围的字符跳过
            
            if (idx == 0) {
                // 特殊情况:只有字符'a'
                if (now == idx)
                    dp[0]++;
            } else {
                if (now == 0) {
                    // 字符'a'可以放在首位或末位
                    dp[0]++;
                    dp[ans - 1] = (dp[ans - 1] + dp[ans - 2]) % MOD;
                } else {
                    // 其他字符的状态转移
                    dp[now] = (dp[now] + dp[now - 1]) % MOD;
                    if (now != idx) {
                        // 对称位置的状态转移
                        dp[ans - now - 1] = (dp[ans - now - 1] + dp[ans - now - 2]) % MOD;
                    }
                }
            }
        }
        
        return new int[]{ans, (int)dp[ans - 1]};
    }
}

C#实现

using System;

public class Solution {
    public const int MOD = 1000000007;

    public int[] Solve(string origin) {
        char[] s = origin.ToCharArray();
        int n = s.Length;
        
        // 预处理:构建字符位置查找表
        int[,] pre = new int[n, 26];   // pre[i,c]: 位置i之前字符c的最近位置
        int[,] next = new int[n, 26];  // next[i,c]: 位置i之后字符c的最近位置
        
        // 初始化为-1表示不存在
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < 26; j++) {
                pre[i, j] = -1;
                next[i, j] = -1;
            }
        }
        
        // 构建pre数组:从左到右扫描
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < 26; j++)
                pre[i, j] = pre[i-1, j];  // 继承前一位置的信息
            pre[i, s[i-1] - 'a'] = i - 1;  // 更新前一个字符的位置
        }
        
        // 构建next数组:从右到左扫描
        for (int i = n - 2; i >= 0; i--) {
            for (int j = 0; j < 26; j++)
                next[i, j] = next[i+1, j];  // 继承后一位置的信息
            next[i, s[i+1] - 'a'] = i + 1;  // 更新后一个字符的位置
        }
        
        // 寻找最大可扩展字符
        int idx = -1;  // 最大可扩展字符的编号(0='a', 1='b', ...)
        
        for (int i = 0; i < n; i++) {
            bool flag = true;
            int now = s[i] - 'a';  // 当前字符编号
            int ptr1 = i;  // 向右扩展指针
            int ptr2 = i;  // 向左扩展指针
            
            // 检查能否向外扩展到字符'a'
            for (int j = now - 1; j >= 0; j--) {
                ptr1 = next[ptr1, j];  // 向右寻找字符j
                ptr2 = pre[ptr2, j];   // 向左寻找字符j
                if (ptr1 == -1 || ptr2 == -1) {
                    flag = false;  // 无法找到,扩展失败
                    break;
                }
            }
            
            if (!flag)
                continue;
            
            idx = Math.Max(idx, now);  // 更新最大可扩展字符
        }
        
        if (idx == -1)
            return new int[]{0, 0};  // 无法构成连续回文子序列
        
        // 动态规划计数
        int ans = idx * 2 + 1;  // 回文序列长度
        long[] dp = new long[ans];  // dp[i]: 构建到回文序列第i个位置的方案数
        
        for (int i = 0; i < n; i++) {
            int now = s[i] - 'a';
            if (now > idx)
                continue;  // 超出范围的字符跳过
            
            if (idx == 0) {
                // 特殊情况:只有字符'a'
                if (now == idx)
                    dp[0]++;
            } else {
                if (now == 0) {
                    // 字符'a'可以放在首位或末位
                    dp[0]++;
                    dp[ans - 1] = (dp[ans - 1] + dp[ans - 2]) % MOD;
                } else {
                    // 其他字符的状态转移
                    dp[now] = (dp[now] + dp[now - 1]) % MOD;
                    if (now != idx) {
                        // 对称位置的状态转移
                        dp[ans - now - 1] = (dp[ans - now - 1] + dp[ans - now - 2]) % MOD;
                    }
                }
            }
        }
        
        return new int[]{ans, (int)dp[ans - 1]};
    }
}

算法详解

1. 预处理阶段 - 构建位置查找表

目的: 快速查找每个位置前后字符的最近出现位置

// 构建pre数组:记录每个位置之前字符的最近位置
for (int i = 1; i < n; i++) {
    for (int j = 0; j < 26; j++)
        pre[i][j] = pre[i-1][j];  // 继承前一位置的信息
    pre[i][s[i-1] - 'a'] = i - 1;  // 更新前一个字符的位置
}

// 构建next数组:记录每个位置之后字符的最近位置
for (int i = n - 2; i >= 0; i--) {
    for (int j = 0; j < 26; j++)
        next[i][j] = next[i+1][j];  // 继承后一位置的信息
    next[i][s[i+1] - 'a'] = i + 1;  // 更新后一个字符的位置
}

2. 扩展检查阶段 - 寻找最大可扩展字符

核心思想: 对每个位置的字符,检查能否向外完全扩展到字符'a'

// 以当前字符为中心,向外扩展
for (int j = now - 1; j >= 0; j--) {
    ptr1 = next[ptr1][j];  // 向右寻找字符j
    ptr2 = pre[ptr2][j];   // 向左寻找字符j
    if (ptr1 == -1 || ptr2 == -1) {
        flag = false;  // 无法找到对应字符,扩展失败
        break;
    }
}

示例: 对于字符'c'(编号2):

  1. 寻找字符'b'(编号1)的左右位置
  2. 寻找字符'a'(编号0)的左右位置
  3. 如果都能找到,说明可以构成"abcba"

3. 动态规划计数阶段

DP状态定义: dp[i] 表示构建到回文序列第i个位置的方案数

回文序列结构: 对于最大可扩展字符idx,回文序列为:

a b c ... idx ... c b a
0 1 2 ... idx ... ans-3 ans-2 ans-1

状态转移方程:

if (now == 0) {
    // 字符'a'可以放在首位或末位
    dp[0]++;  // 放在首位
    dp[ans - 1] = (dp[ans - 1] + dp[ans - 2]) % MOD;  // 放在末位
} else {
    // 其他字符的状态转移
    dp[now] = (dp[now] + dp[now - 1]) % MOD;  // 当前位置
    if (now != idx) {
        // 对称位置的状态转移
        dp[ans - now - 1] = (dp[ans - now - 1] + dp[ans - now - 2]) % MOD;
    }
}

示例分析

示例1:"ababcbaa" → [5,6]

Step 1: 预处理 构建pre和next数组,记录字符位置信息。

Step 2: 扩展检查

  • 位置4的字符'c'(编号2)可以扩展:
    • 向右找'b':位置5
    • 向左找'b':位置1
    • 向右找'a':位置6
    • 向左找'a':位置0
  • 成功扩展到'a',最大可扩展字符idx=2

Step 3: DP计数

  • 回文长度:ans = 2*2+1 = 5
  • 回文结构:a b c b a
  • 统计所有可能的选择方案,结果为6种

示例3:"zxcvbnmqis" → [0,0]

所有字符都无法扩展到字符'a',因此返回[0,0]。


复杂度分析

时间复杂度: O(n × 26) = O(n)

  • 预处理:O(n × 26)
  • 扩展检查:O(n × 26)
  • DP计数:O(n)

空间复杂度: O(n × 26) = O(n)

  • 预处理数组:O(n × 26)
  • DP数组:O(26)
### 计算最长回文子序列的动态规划方法 要解决这个问题,可以采用 **动态规划 (Dynamic Programming)** 的方式来求解。以下是详细的分析和实现: #### 动态规划的核心思路 设 `dp[left][right]` 表示字符串从索引 `left` 到 `right` 能够构成的最长回文子序列的最大长度。 1. 如果字符串两端字符相同 (`s[left] == s[right]`),则它们可以作为回文的一部分,因此有: \[ dp[left][right] = dp[left+1][right-1] + 2 \] 2. 如果字符串两端字符不同,则需要分别考虑去掉左端或右端的情况,取两者中的最大值: \[ dp[left][right] = \max(dp[left+1][right], dp[left][right-1]) \] 最终目标是计算整个字符串范围内的最长回文子序列长度,即 `dp[0][n-1]`,其中 `n` 是字符串的长度[^2]。 --- #### 边界条件 当子串只有一个字符时(即 `left == right`),显然这个单字符本身就是一个回文子序列,所以有: \[ dp[i][i] = 1, \quad \text{对于所有的 } i \] 当子串为空时(即 `left > right`),此时不存在有效的子序列,故其长度为零。 --- #### 时间复杂度与空间复杂度 时间复杂度为 \(O(n^2)\),因为我们需要填充一个大小为 \(n \times n\) 的二维数组。 空间复杂度同样为 \(O(n^2)\),用于存储中间状态的结果。 --- #### Python 实现代码 下面是基于上述理论的具体实现代码: ```python def longest_palindromic_subsequence(s: str) -> int: n = len(s) if n == 0: return 0 # 创建 DP 数组并初始化 dp = [[0] * n for _ in range(n)] # 单个字符都是回文,长度为1 for i in range(n): dp[i][i] = 1 # 填充 DP 表格 for length in range(2, n + 1): # 子串长度从2到n for left in range(n - length + 1): right = left + length - 1 if s[left] == s[right]: dp[left][right] = dp[left + 1][right - 1] + 2 else: dp[left][right] = max(dp[left + 1][right], dp[left][right - 1]) # 返回整个字符串的最长回文子序列长度 return dp[0][n - 1] # 测试案例 if __name__ == "__main__": test_string = "bbbab" result = longest_palindromic_subsequence(test_string) print(f"输入: {test_string}, 输出: {result}") # 应输出4 ``` 此代码实现了动态规划算法,并通过测试用例验证了正确性[^3]。 --- #### 结果解释 以上程序会输出给定字符串 `"bbbab"` 中最长回文子序列的长度为 `4`,这对应于子序列 `"bbbb"`。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值