最长回文子串(longest-palindromic-substring)——动态规划DP

题目

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:

输入: "cbbd"
输出: "bb"

代码

解法 1: 暴力破解

暴力求解,列举所有的子串,判断是否为回文串,保存最长的回文串。

	// 暴力解法
	public String longestPalindrome(String s) {
	    String ans = "";
	    int max = 0;
	    int len = s.length();
	    for (int i = 0; i < len; i++)
	        for (int j = i + 1; j <= len; j++) {
	            String test = s.substring(i, j);
	            if (isPalindromic(test) && test.length() > max) {
	                ans = s.substring(i, j);
	                max = Math.max(max, ans.length());
	            }
	        }
	    return ans;
	}
	
	private boolean isPalindromic(String s) {
		int len = s.length();
		for (int i = 0; i < len / 2; i++) {
			if (s.charAt(i) != s.charAt(len - i - 1)) {
				return false;
			}
		}
		return true;
	}

说明,暴力解法可以优化一下,遍历时只记录begin和maxLen,最后返回时再截取字符串。减少一定的性能消耗。

解法2:中心扩散

  • 枚举所有可能的回文子串的中心位置
  • 中心位置可能是一个字符(奇数回文子串),也有可能是两个相邻的字符(偶数回文子串)。
  • 记录最长回文子串的相关变量

奇数回文子串

在这里插入图片描述

偶数回文子串

在这里插入图片描述

	public String longestPalindrome(String s) {
		int len = s.length();
		if (len < 2) {
			return s;
		}

		int maxLen = 1;
		int begin = 0;

		char[] charArray = s.toCharArray();
		for (int i = 0; i < len - 1; i++) {
			int oddLen = expandAroundCenter(charArray, i, i);//奇数回文子串,比如bab,中心为a
			int evenLen = expandAroundCenter(charArray, i, i + 1);//偶数回文子串,比如baab,中心为aa
			
			int curMaxLen = Math.max(oddLen, evenLen);
			if (curMaxLen > maxLen) {
				maxLen = curMaxLen;
				// 根据maxLen和下标 i 计算出 begin
				// 这一步要在纸上画图发现规律
				begin = i - (maxLen - 1) / 2;
			}
		}
		return s.substring(begin, begin + maxLen);
	}

	/**
	 * 
	 * @param charArray 原始字符串的字符数组
	 * @param left 起始左边界
	 * @param right 起始右边界
	 * @return 回文串的长度
	 */
	private int expandAroundCenter(char[] charArray, int left, int right) {
		//当left = right的时候,回文中心是一个字符,回文中心是一个字符,回文串的长度是奇数
		//当right = left+1的时候,此时回文中心两个字符,回文串的长度是偶数
	    int L = left, R = right;
	    int len = charArray.length;
	    while (L >= 0 && R < len && charArray[L] == charArray[R]) {
	        L--;
	        R++;
	    }
	    //跳出while循环时,恰好满足s.charAt(i) != s.charAt(j),
	    //回文串的长度是 R - L + 1 - 2 = R - L - 1
	    return R - L - 1;
	}

再说一下begin = i - (maxLen - 1) / 2这句。
奇数回文串
在这里插入图片描述
偶数回文串
在这里插入图片描述
公式
在这里插入图片描述
这里 / \mathbf{/} /表示下取整。

解法3 : 动态规划

方法一中,存在大量的重复计算工作,例如当 s=“abcba” 时, 对于子串 “bcb” 和 子串 “abcba”, 分别进行了2次完整的计算,来检测该子串是否是回文串。

很明显的是,对于 s=“abcba” , 在已知 "bcb"是回文串的情况下,要判断 "bcb"是否是回文串的话,只需要判断两边的*位置的字符是否相等即可。 我们定义 P(i,j) 表示 s[i,j]是否是回文串,若s[i,j]是回文串,则P(i,j)=true,否则为false. 则有下面的递推公式成立:

P[i,j] =  p(i+1,j-1) && ( s[i]==s[j] ) 

对于上面公式有2个特殊情况,当子串长度为1或2时,上面公式不成立。我们单独分析这两种情况:

若子串长度为1,即 j==i, 则 P[i,j] = P[i,i] = true; 
若子串长为2,即j==i+1, 则 P[i,j] = P[i,i+1] =  ( s[i]==s[i+1] )

所以

  • 状态:dp[i][j]表示子串s[i..j]是否为回文子串
  • 得到状态转移方程:dp[i][j] = (s[i] == s[j]) and dp[i+1][j-1]
  • 边界条件:j-1-(i+1)+1<2,整理得j-i<3
  • 初始化:dp[i][i]=true

代码

    public String longestPalindrome(String s) {
		int n = s.length();
		boolean[][] dp = new boolean[n][n];
		String ans = "";
		for (int k = 0; k < n; ++k) {
			for (int i = 0; i + k < n; ++i) {
				int j = i + k;
				if (k == 0) {
					dp[i][j] = true;
				} else if (k == 1) {
					dp[i][j] = (s.charAt(i) == s.charAt(j));
				} else {
					dp[i][j] = (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]);
				}
				if (dp[i][j] && k + 1 > ans.length()) {
					ans = s.substring(i, i + k + 1);
				}
			}
		}
		return ans;
    }

注意

  • 子串下标为0,子串长度为1,dp[i,i] = true
  • 子串下标为1,子串长度为2,则 dp[i,j] = dp[i,i+1] = ( s[i]==s[i+1] )
  • 子串长度为1,都是True,子串长度为2时,要判断。

解法4:Manacher算法

  • 中心扩散的原理是:
    如果如果当前字符串不是回文串,那以当前字符串为中心的所有串都不是回文串,以此来终止当前字符串的扩散。
  • 动态规划的原理是:
    如果当前字符串首尾字符相同,如果去掉首尾字符剩下的子串不是回文串,那当前字符串肯定不是回文串,以此来终止当前字符串的缩减。
  • 马拉车算法的原理:
    按顺序对每个字符进行中心扩散,利用回文串镜像的特点,找到当前字符在包含它的回文串中的镜像字符,利用镜像字符之前的计算结果,来跳过一些中心扩散的步骤,甚至直接得出结果,以此对中心扩散进行优化。
public String longestPalindrome(String s) {
    if (s == null || s.length() == 0)
        return "";
    // 先拓展,加一个标志位,由于符号任取,和别人一样用 # 吧
    char[] chars = new char[(s.length() << 1) + 1];
    for (int i = 0; i < chars.length; i++)
        chars[i] = (i & 1) == 0 ? '#' : s.charAt((i - 1) >> 1);
    int longestIdx = 1;
    int[] width = new int[chars.length];
    for (int i = 1, mid = 1, right = 1; i < chars.length; i++) {
        // 保证 right 永远大于等于 i,可以保证 i 永远可以对 mid 取到
        // 镜像字符 mirror(至少等于自己)
        if (i > right)
            right = mid = i;
        int left = (mid << 1) - right, mirror = (mid << 1) - i;
        width[i] = Math.min(width[mirror], mirror - left);
        for (int x = i - width[i] - 1, y = i + width[i] + 1; 
             x > -1 && y < chars.length && chars[x] == chars[y]; 
             x--, y++)
            width[i]++;
        longestIdx = width[i] > width[longestIdx] ? i : longestIdx;
        if (i + width[i] > right) {
            right = i + width[i];
            mid = i;
        }
    }
    //  左右必定会扩展到 ‘#’ 的,这样写左边的 # 会对应到原字符串右边的字符,
    // 右边的 # 会对应到原字符串左边的字符。
    int left = (longestIdx - width[longestIdx] + 1) / 2;
    int right = (longestIdx + width[longestIdx] - 1) / 2;
    return s.substring(left, right + 1);
}

资料

最长回文子串(longest-palindromic-substring)
windliang资料
LeetCode 5. Longest Palindromic Substring 最长回文子串 C#
Manacher 算法其实很简单
经典算法问题:最长回文子串之 Manacher 算法

Manacher算法总结

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值