@LeetCode最长回文子串--Longest Palindromic Substring[C++]

@LeetCode最长回文子串--Longest Palindromic Substring[C++]

  • 问题描述
  • 解决方法及复杂度分析
    • Manacher算法
  • 程序实现

问题描述

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

示例1:

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

示例2:

输入:“cbbd”
输出:“bb”

解决方法及复杂度分析

Manacher算法

首先,把输入的字符串 S 转换成另一个字符串 T,转换方法是在字符中间插入字符 #

例如:S = “abaaba”, T = “#a#b#a#a#b#a#”。

为了发现最长回文子串,我们需要从每个 T i T_i Ti 开始拓展,拓展后的子串 T i − d . . . T i + d T_{i-d}...T_{i+d} Tid...Ti+d 形成回文。可以知道, d d d 是以 T i T_i Ti 为中心的回文的长度。

我们利用数组 P 存储每个以 T i T_i Ti 为中心的回文的长度。最长回文子串长度是数组 P 中最大值。

利用上述例子,从左到右填充数组 P

T = # a # b # a # a # b # a #
P = 0 1 0 3 0 1 6 1 0 3 0 1 0

通过数组 P,我们可以得到最长回文子串是 “abaaba”。数组 P 的最大值: P 6 = 6 P_6 = 6 P6=6

现在,想象在回文 “abaaba” 的中心划一条想象的垂线。数组 P 中的值关于这条线对称。同样回文 “aba” 也是这样。这个对称性质是在满足一定条件下才成立。

接下来,我们讨论一个更复杂的例子,这个字符串包含一些重叠的回文。例子如下:
@LeetCode最长回文子串--Longest Palindromic Substring[C++]_第1张图片

图1-1 样例字符串示意图

上图中,将字符串 S = “babcbabcbaccba” 转换为 T。此时数组 P 已经完成一部分。实线表明回文 “abcbabcba” 的中心 C。两条虚线表明相对于中心的左边(L)和右边®。指数 i i i i ′ i\prime i 是关于中心 C 对称。

当指数 i = 13 i=13 i=13 时,如果计算 P [ 13 ] P[13] P[13],应该看关于回文中心 C 对称的指数 i ′ i\prime i,此时 i ′ = 9 i\prime =9 i=9
@LeetCode最长回文子串--Longest Palindromic Substring[C++]_第2张图片

图1-2 计算数组 P 示意图

上图中,绿色实线覆盖区域表示两个以 i i i i ′ i\prime i 为中心的回文。 i i i i ′ i\prime i 是关于中心 C 的镜像指数。根据对称性, P [ i ] = P [ i ′ ] = 1 P[i]=P[i\prime]=1 P[i]=P[i]=1,即 P [ 13 ] = 1 P[13]=1 P[13]=1
@LeetCode最长回文子串--Longest Palindromic Substring[C++]_第3张图片

图1-3 计算数组 P 特殊情况示意图

现在,计算 P [ 15 ] P[15] P[15]。如果仍然依据对称性(之前说过对称性在一定条件下才能满足), P [ 15 ] = P [ 7 ] = 7 P[15]=P[7]=7 P[15]=P[7]=7。但这显然不对。如果我们以 T 15 T_{15} T15 为中心拓展,仅能形成 “a#b#c#b#a” 回文,这比按照对称性得到的长度要短。
@LeetCode最长回文子串--Longest Palindromic Substring[C++]_第4张图片

图1-4 计算数组 P 特殊情况示意图

如上图所示,依据 C 为中心的对称性,绿色实线表示两侧必须匹配的区域。红色实线表示两侧可能不匹配的区域。绿色虚线表示穿过中心的区域。

很明显,由两条绿色实线表示区域中的两个子字符串必须完全匹配。穿过中心的区域(由绿色虚线表示)也是对称的。注意, P [ i ′ ] = 7 P[i\prime]=7 P[i]=7,它向左拓展越过回文的左边界(L),所以它不再符合回文的对称性质。我们现在知道 P [ i ] ≥ 5 P[i]\ge 5 P[i]5,为了确定 P [ i ] P[i] P[i] 的值,必须向右拓展超过右边界®进行字符匹配。因此,可以知道 P [ 21 ] ≠ P [ 1 ] P[21]\ne P[1] P[21]̸=P[1],结论是 P [ i ] = 5 P[i]=5 P[i]=5

算法的关键步骤如下。
i f   P [ i ′ ] ≤ R − i , t h e n   P [ i ] ← P [ i ′ ] e l s e   P [ i ] ≥ P [ i ′ ] . if\ P[i\prime]\le R-i,\\ then\ P[i]\gets P[i\prime]\\ else\ P[i]\ge P[i\prime]. if P[i]Ri,then P[i]P[i]else P[i]P[i].

最后一步是确定如何同时移动 CR 的位置。方法如下:

如果以 i i i 为中心的回文向右拓展穿过 R,更新 C i i i,并将 R 拓展到新回文的右边。

复杂度分析

  • 时间复杂度: O ( N ) O(N) O(N)
    – 扩展R(内部while循环)最多需要N个步骤,定位和测试每个中心也需要总共N个步骤。因此,该算法保证最多完成2 * N步,给出线性时间解。

程序实现

	class Solution {
	public:
		string longestPalindrome(string s) {
			string T = preProcess(s);
			int n = T.length();
			int *p = new int[n];
			int C = 0, R = 0;
			for(int i = 1; i < n - 1; i++) {
				int i_mirror = 2 * C - i;	//equals to i = C - (i - C)
				p[i] = (R > i) ? min(R - i, p[i_mirror]) : 0;

				// Attempt to expand palindrome centered at i
				while(T[i + 1 + p[i]] == T[i - 1 - p[i]])
					p[i]++;
				
				// If palindrome centered at i expand past R,
				// adjust center based on expanded palindrome
				if(i + p[i] > R) {
					C = i;
					R = i + p[i];
				}
			}

			// Find the maximum element in P
			int maxLen = 0;
			int centerIndex = 0;
			for(int i = 1; i < n - 1; i++) {
				if(p[i] > maxLen) {
					maxLen = p[i];
					centerIndex = i;
				}
			}
			delete[] p;
			return s.substr((centerIndex - 1 - maxLen) / 2, maxLen);
		}
		// Transform S into T
		// ^ and $ signs are sentinels appended to each end to avoid bounds checking
		string preProcess(string s) {
			int n = s.length();
			if(!n) return "^$";
			string ret = "^";
			for(int i = 0; i < n; i++)
				ret += "#" + s.substr(i, 1);
			
			ret += "#$";
			return ret;
		}
	};

@ 山东·威海 2019.01.30

你可能感兴趣的:(Leetcode,LeetCode,Algorithm)