题目描述
今天我来分享一道字符串题目,涉及对字符串中所有子序列的判断。题目要求我们计算字符串中所有“好字符串”的数量。一个“好字符串”定义为不包含任何长度大于等于2的回文子串。我们需要输出符合条件的子序列数量,且结果需要对 取模。
例如,给定字符串 "aba",它的所有子序列中,只有 "aa" 和 "aba" 是回文子串,其他的子序列是“好字符串”。最终输出应该是 5。
示例
示例 1:
输入:
"aba"
输出:
5
解释:
所有子序列:{a, b, a, ab, ba, aba},其中 ab、ba、aba 是回文子串,剩下的子序列是好字符串。最终结果为 5。
示例 2:
输入:
"aaa"
输出:
3
解释:
所有子序列:{a, a, a, aa, aa, aaa},其中 aa 和 aaa 是回文子串,剩下的子序列是好字符串。最终结果为 3。
暴力解法
暴力解法的基本思路是枚举所有可能的子序列,然后检查这些子序列是否包含回文子串。我们可以利用二进制掩码(bitmask)来表示每个子序列,进而检查是否为回文。对于每个子序列,判断它是否含有回文子串。如果不含有回文子串,则它是一个有效的好字符串。
暴力解法的核心步骤如下:
- 使用二进制掩码生成所有可能的子序列。
- 对于每个子序列,判断它是否含有回文子串。
- 如果没有回文子串,则将其计入结果。
下面是暴力解法的代码实现:
public class Main {
// 判断字符串是否含有回文子串
public static boolean hasPalindrome(String s) {
int len = s.length();
// 遍历所有子串,检查是否为回文
for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) {
if (isPalindrome(s, i, j)) {
return true; // 如果找到回文,直接返回true
}
}
}
return false; // 没有回文子串
}
// 判断s[i...j]是否是回文
public static boolean isPalindrome(String s, int i, int j) {
while (i < j) {
if (s.charAt(i) != s.charAt(j)) {
return false; // 如果两端字符不相等,返回false
}
i++;
j--;
}
return true; // 回文
}
// 计算“好字符串”的数量
public static int solution(String s) {
int n = s.length();
int mod = 1000000007; // 取模常数
int count = 0; // 统计有效子序列的数量
// 枚举所有子序列
for (int mask = 1; mask < (1 << n); mask++) {
StringBuilder sb = new StringBuilder();
// 根据mask选取子序列
for (int j = 0; j < n; j++) {
if ((mask & (1 << j)) != 0) {
sb.append(s.charAt(j));
}
}
// 判断该子序列是否是“好字符串”
if (!hasPalindrome(sb.toString())) {
count = (count + 1) % mod; // 计数符合条件的子序列
}
}
return count; // 返回结果
}
public static void main(String[] args) {
System.out.println(solution("aba") == 5); // 预期输出 5
System.out.println(solution("aaa") == 3); // 预期输出 3
System.out.println(solution("ghij") == 15); // 预期输出 15
}
}
解法分析
暴力解法的时间复杂度是 ,其中 是所有子序列的数量, 是判断回文子串的时间复杂度。显然,随着字符串长度的增加,暴力解法的计算时间会迅速增加,因此我们需要优化这个解法。
优化解法
优化的关键是减少回文检查的次数。我们可以使用动态规划(DP)提前计算出每个子串是否是回文子串,避免在检查每个子序列时再次进行回文判断。具体思路如下:
- 动态规划预处理回文子串:使用一个二维 DP 数组
dp[i][j]表示从字符串的第i个字符到第j个字符是否是回文子串。这样我们就可以在枚举子序列时直接判断是否包含回文子串,而不需要再次遍历。 - 使用二进制掩码枚举所有子序列:通过二进制掩码生成子序列时,我们可以利用已经计算好的回文信息快速判断每个子序列是否符合要求。
优化后的解法代码如下:
public class Main {
public static int solution(String s) {
int n = s.length();
int mod = 1000000007; // 取模常数
int count = 0; // 统计符合条件的子序列数量
// dp[i][j] 表示s[i...j]是否是回文
boolean[][] dp = new boolean[n][n];
// 初始化dp数组:单个字符一定是回文,两个相同字符也是回文
for (int i = 0; i < n; i++) {
dp[i][i] = true; // 单个字符是回文
if (i < n - 1 && s.charAt(i) == s.charAt(i + 1)) {
dp[i][i + 1] = true; // 两个相同字符是回文
}
}
// 使用动态规划填充dp数组
for (int len = 3; len <= n; len++) { // 从长度3开始处理
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
dp[i][j] = s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1];
}
}
// 枚举所有子序列
for (int mask = 1; mask < (1 << n); mask++) {
StringBuilder sb = new StringBuilder();
// 根据mask选取子序列
for (int j = 0; j < n; j++) {
if ((mask & (1 << j)) != 0) {
sb.append(s.charAt(j));
}
}
// 判断该子序列是否是“好字符串”
boolean isGood = true;
for (int i = 0; i < sb.length(); i++) {
for (int j = i + 1; j < sb.length(); j++) {
if (dp[i][j]) {
isGood = false;
break;
}
}
if (!isGood) break;
}
if (isGood) {
count = (count + 1) % mod;
}
}
return count; // 返回结果
}
public static void main(String[] args) {
System.out.println(solution("aba") == 5); // 预期输出 5
System.out.println(solution("aaa") == 3); // 预期输出 3
System.out.println(solution("ghij") == 15); // 预期输出 15
}
}
代码解释
-
回文判断优化:使用二维
dp数组来记录每个子串是否是回文。dp[i][j] = true表示从位置i到位置j的子串是回文。通过动态规划填充这个数组,初始化时,单个字符和两个相同字符是回文。然后使用状态转移方程填充更长的子串。 -
子序列生成:使用二进制掩码(bitmask)生成所有可能的子序列。对于每个子序列,通过
dp数组判断是否包含回文子串,如果没有回文子串,则计入结果。 -
效率提升:通过预处理回文信息,避免了每次判断子序列时都进行重复的回文检查,显著提高了效率。
知识总结
通过这道题目,我总结了以下几点:
- 动态规划:我们通过使用动态规划来提前计算回文子串的状态,避免了暴力解法中多次判断回文的问题。
- 空间优化:通过使用二维
dp数组提前处理所有子串的回文状态,优化了空间和时间复杂度。 - 二进制掩码:利用二进制掩码枚举所有子序列是一种常见的技巧,能够有效地遍历所有子序列并对其进行操作。