题目描述
「连续回文子序列」指形如 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] 仅为小写字母
问题分析
什么是"连续回文子序列"?
连续回文子序列必须满足以下特征:
- 连续字母:由连续的字母组成,如 a、aba、abcba、abcdcba
- 回文性质:正读反读相同
- 完整扩展:必须能从某个中心字符向外完全扩展到字符'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):
- 寻找字符'b'(编号1)的左右位置
- 寻找字符'a'(编号0)的左右位置
- 如果都能找到,说明可以构成"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)