【LeetCode 5】 Longest Palindromic Substring【M】

求所给字符串中最长的回文子串(所给字符串只包含小写字母),并返回这个最长的回文子串


1.动态规划算法(DP)

定义数组 dp:先根据所给字符串 s 的长度来申请创建一个二维的 boolean 型数组 dpdp[i][j] 为 true 则表示在字符串 ss[i]s[j] 是字符串 s 的一个回文子串(包含头尾)。

如何进行迭代:
1.若 s[i] != s[j],则 dp[i][j] = false;否则2
2.若 s[i] == s[j],则 dp[i][j] = dp[i+1][j-1];

说明:回文串是对于中心点对称的,所以只需要检验最左(s[i])和最右(s[j])的2个字符串是否相等,若不等则这之间(s[i] 到 s[j])的子串不可能形成回文子串;若相等,则 dp[i][j] 等于dp[i+1][j-1] 的值,完成了迭代。

如何赋予初值:
对于 i >= j ,dp[i][j] = true;否则 dp[i][j] = false;

说明:合法的 i,j 应该满足 i < j,否则没有实际意义,所以我们把所有合法的 i,j 对应的 dp[i][j] 设置为 false,其他的就设置为 true,这种设置初值的方式是DP中常用的。

Java代码如下:

public String longestPalindrome_DP(String s) { //动态规划方法
		int len = s.length();
		if(len == 0) {
			return "";
		}
		if(len == 1) {
			return s;
		}
		
		boolean[][] dp = new boolean[len][len];
		for(int i = 0; i < len; i++) {
			for(int j = 0; j < len; j++) {
				if(i >= j) {
					dp[i][j] = true;
				} else {
					dp[i][j] = false;
				}
			}
		}//初始化
		int rf = 0, rt = 0;
		int k; // 回文子串的长度-1
		int maxLen = 1;
		for(k = 1; k < len; k++) {
			for(int i = 0; i+k<len; i++) {
				int j = i + k;
				if(s.charAt(i) != s.charAt(j)) {
					dp[i][j] = false;
				} else {
					dp[i][j] = dp[i+1][j-1];
					if(dp[i][j]) {
						if(k + 1 > maxLen) {
							maxLen = k + 1;
							rf = i;
							rt = j;
						}
					}
				}
			}
		}
		return s.substring(rf, rt+1);		
	} //longestPalindrome_DP

在leetcode上提交代码时会报 Time Limit Exceeded 的错误,该DP的时间复杂度为O(n*n)。笔者也不知道leetcode上的运行时间限制是多少,最后一个测试用例 s 是1000个‘a’。


2.Manacher算法(马拉车算法)

定义:char 型数组 temp,后续操作是基于数组 temp 的。
定义:int 型数组 pp[i] 表示以i为中心的(包含 temp[i] 这个字符)回文串半径长(包含头尾)。数组 p 从 p[2] 开始有实际意义。

首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串 s 的每相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用‘#’号,我们将扩展后的元素放在数组 temp 中。在数组 temp 中我们将 temp[0] 设置为‘@’,防止在后续的操作中越界,见下表。

序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | … |
----| —|---|—|
s | s[0]|s[1]|s[2]|s[3]|s[4]|s[5]|s[6]|…|
temp | @ |#|temp[2]=s[0]|#|temp[4]=s[1]|#|temp[6]=s[2]|…|


>说明:我们可以得出:s[i] = temp[(i+1)*2]

假设现在要求 p[i] ,这意味着 p[2] ~ p[i-1] 的值已经求得。

定义:maxLen 为在数组 temp 中的位置 i 之前所有回文串中能延伸到的最右端的位置,即 maxLen = max{ p[j] + j }( 0 < j < i )。定义 bound 为 maxLen 对应的点,即 maxLen = bound+p[bound]

此时可以分为两种情况来分析:

1. 当前位置 i > maxLen,那么就先设置 p[i] = 1(字符本身是回文子串),然后再用 while 循环来扩展 p[i]。
2. 当前位置 i < maxLen,这种情况又可分为3个小情况,我们先设当前位置 i 相对于位置 bound (见 maxLen 定义)的对称位置为 i’
: A. 当 i’ 的回文串的最左端在 bound 的回文串的最左端的左边(i’-p[i’] < bound-p[bound]),此时 p[i] = p[bound]+bound-i
B. 当 i’ 的回文串的最左端在 j 的回文串的最左端的右边(i’-p[i’] > bound-p[bound]),此时 p[i] = p[2*bound-i](即 p[i]=p[i’]);
C. 当 i’ 的回文串的最左端与 j 的回文串的最左端相等(i’-p[i’] ==bound-p[bound]),此时先令 p[i] = p[i’],然后用 while 循环来扩展 p[i]。

证明:可根据回文串的对称性来证明 A,B,详细的证明见这里。并且 A,B 这2种情况可以合并为 p[i] = Min(p[2*bound-i], p[bound]+bound-i);(读者可用实际例子求证,详细证明略)

具体代码如下:

public String longestPalindrome_Manacher(String s) {  //Manacher 方法
		int len=0, bound=0, maxlen=0;
		int rf=0, rt=0;
		len = s.length();
		
		char[] temp = new char[2*len+3];
		int[] p = new int[2*len+2];     //存放回文串的半径,从2开始(包含头尾)
		temp[0] = '@';
		for(int i = 1; i <= 2*len; i = i + 2) {
			temp[i] = '#';
			temp[i+1] = s.charAt(i/2);
		}
		temp[2*len+1] = '#';   	//    s[i] = temp[(i+1)*2]
		temp[2*len+2] = '$';
		
		for(int i = 2; i < 2*len+1; i++) {
			if(i < bound+p[bound]) {
				p[i] = Math.min(p[2*bound-i], p[bound]+bound-i);
			} else {
				p[i] = 1;
			}
			while(temp[i-p[i]] == temp[i+p[i]]) {
				p[i]++;
			}
			if(bound+p[bound] < i + p[i]) {
				bound = i;
			}
			if(maxlen < p[i]) {
				maxlen = p[i];
				// 对应到 s 中的序号
				rf = (i - p[i] + 2) / 2 - 1;
				rt = (i + p[i] - 2) / 2 - 1;
			}			
		}		
		return s.substring(rf, rt+1);
	}// longestPalindrome_Manacher

在输入的测试用例 s 是1000个 ‘a’ 的时候,使用动态规划方法,程序的运行时间为 30ms 左右;而使用Manacher算法(马拉车算法),程序的运行时间为 0.6ms 左右。(个人测试结果,仅供参考)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值