动态规划 算法(四)

647. 回文子串

问题描述

给定一个字符串 s,计算字符串中回文子串的数量。不同起始位置或结束位置的子串,即使内容相同也被视为不同的子串。

示例

输入: "abc"
输出: 3
解释: 三个回文子串: "a", "b", "c"

输入: "aaa"
输出: 6
解释: 六个回文子串: "a", "a", "a", "aa", "aa", "aaa"

算法思路

方法一:动态规划(DP 数组)

  1. 状态定义
    • dp[i][j] 表示子串 s[i..j] 是否为回文串。
  2. 状态转移
    • s[i] != s[j] 时:dp[i][j] = false
    • s[i] == s[j] 时:
      • 若子串长度 ≤ 2(j - i < 2),则为回文串。
      • 否则取决于内部子串:dp[i][j] = dp[i+1][j-1]
  3. 遍历顺序
    • 按子串长度从小到大遍历,确保状态转移时 dp[i+1][j-1] 已计算。

方法二:中心扩展法(空间优化)

  1. 核心思想
    遍历所有可能的回文中心(单个字符或两个字符之间),从中心向两边扩展,判断是否形成回文子串。

  2. 中心点计算

    • 长度为 n 的字符串有 2n-1 个中心:
      • n 个单字符中心(如 "a" 的中心是 a
      • n-1 个双字符中心(如 "aa" 的中心在两个 a 之间)
  3. 扩展规则

    • 设中心索引为 center(范围 [0, 2n-2]
    • 左边界 left = center / 2
    • 右边界 right = left + center % 2
    • 向两边扩展:left--right++,直到字符不匹配或越界

代码实现

方法一:动态规划(DP 数组)

class Solution {
    public int countSubstrings(String s) {
        if (s == null || s.isEmpty()) return 0;
        
        int n = s.length();
        boolean[][] dp = new boolean[n][n]; // dp[i][j] 表示 s[i..j] 是否为回文串
        int count = 0; // 回文子串计数
        
        // 初始化:所有长度为 1 的子串都是回文串
        for (int i = 0; i < n; i++) {
            dp[i][i] = true;
            count++;
        }
        
        // 按子串长度从小到大遍历(从 2 到 n)
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1; // 子串结束位置
                
                // 首尾字符相同
                if (s.charAt(i) == s.charAt(j)) {
                    // 子串长度 ≤ 2 或 内部子串是回文串
                    if (len == 2 || dp[i + 1][j - 1]) {
                        dp[i][j] = true;
                        count++;
                    } else {
                        dp[i][j] = false;
                    }
                } else {
                    dp[i][j] = false; // 首尾不同,不是回文
                }
            }
        }
        return count;
    }
}

方法二:中心扩展法(空间优化)

class Solution {
    public int countSubstrings(String s) {
        if (s == null || s.isEmpty()) return 0;
        
        int n = s.length();
        int count = 0;
        
        // 遍历所有可能的中心点(共 2n-1 个)
        for (int center = 0; center < 2 * n - 1; center++) {
            // 根据中心点计算左右起始位置
            int left = center / 2;
            int right = left + center % 2; // 奇数中心:left=right;偶数中心:right=left+1
            
            // 从中心向两边扩展
            while (left >= 0 && right < n && s.charAt(left) == s.charAt(right)) {
                count++;  // 找到一个新的回文子串
                left--;   // 向左扩展
                right++;  // 向右扩展
            }
        }
        return count;
    }
}

算法分析

  • 时间复杂度
    • 两种方法均为 O(n²),动态规划嵌套两层循环,中心扩展法遍历 2n-1 个中心点。
  • 空间复杂度
    • 动态规划:O(n²),需要二维 DP 数组。
    • 中心扩展:O(1),仅使用常数空间。

算法过程

输入s = "abc"

  1. 动态规划
    • 初始化:dp[0][0]=dp[1][1]=dp[2][2]=true(计数=3)
    • 长度 2:
      • [0,1]:“ab” → 'a'!='b' → 不计
      • [1,2]:“bc” → 'b'!='c' → 不计
    • 长度 3:
      • [0,2]:“abc” → 'a'!='c' → 不计
    • 结果:3

输入s = "aaa"

  1. 中心扩展
    • 中心点 0(left=0, right=0):扩展 → [0,0]="a"(计数=1)
    • 中心点 1(left=0, right=1):扩展 → [0,1]="aa"[-1,2]停止(计数=2)
    • 中心点 2(left=1, right=1):扩展 → [1,1]="a"[0,2]="aaa"(计数=4)
    • 中心点 3(left=1, right=2):扩展 → [1,2]="aa"[0,3]停止(计数=5)
    • 中心点 4(left=2, right=2):扩展 → [2,2]="a"(计数=6)
    • 结果:6

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1: 标准示例
    String s1 = "abc";
    System.out.println("Test 1: " + solution.countSubstrings(s1)); // 3
    
    // 测试用例2: 连续相同字符
    String s2 = "aaa";
    System.out.println("Test 2: " + solution.countSubstrings(s2)); // 6
    
    // 测试用例3: 单字符
    String s3 = "a";
    System.out.println("Test 3: " + solution.countSubstrings(s3)); // 1
    
    // 测试用例4: 空字符串
    String s4 = "";
    System.out.println("Test 4: " + solution.countSubstrings(s4)); // 0
    
    // 测试用例5: 较长回文串
    String s5 = "ababa";
    System.out.println("Test 5: " + solution.countSubstrings(s5)); // 9
}

关键点

  1. 动态规划核心
    • 状态定义:dp[i][j] 表示子串 s[i..j] 是否为回文。
    • 状态转移:首尾字符相同且内部子串为回文(或长度≤2)。
  2. 中心扩展核心
    • 中心点类型:字符位置(奇长度)和字符间隙(偶长度)。
    • 扩展条件:左右字符相同则继续扩展。
  3. 边界处理
    • 空字符串直接返回 0。
    • 单字符字符串有 1 个回文子串。

常见问题

  1. 动态规划中为什么按长度遍历?
    确保计算 dp[i][j] 时,其依赖的子问题 dp[i+1][j-1] 已解决。

  2. 中心扩展法如何避免重复计数?
    每个中心点独立扩展,自然覆盖所有不重复的回文子串。

  3. 两种方法如何选择?

    • 动态规划:思路直接,但空间占用高。
    • 中心扩展:空间效率高,代码更简洁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值