Leetcode:Longest Palindromic Substring分析和实现
问题大意是在给定字符串中查找最长的回文子串,所谓的回文就是依据中间位置对称的字符串,比如abba,aba都是回文。
这个问题初一看,非常简单,但是会很快发现那些简单的思路都会带来O(n^3)级别的时间复杂度,n为传入字符串的实际长度。比如说下面的代码中的暴力枚举
for(i = 0; i < n; i = i + 1)
for(j = i + 1; j < n; j = j + 1)
ti = i, tj = j
while(ti < tj && s[ti] == s[tj])
ti = ti + 1, tj = tj -1
其中s表示传入的字符串,其复杂度如下:

显然上面代码的时间复杂度是O(n^3)级别的。
我一开始使用的算法是动态规划算法,利用一个二维数组p,其中p[i][j]表示以i,j为边界的子字符串是否为回文,显然p[i][j] = s[i] == s[j] && p[i + 1][j - 1]。
p is a matrix with specified size nxn //p是一个nxn的矩阵
initialize all cell of p with 0 //将p的每一个单元格初始化为0
mi = 0, mj = 0
for(i = 0; i < n; i = i + 1)
for(j = i + 1; j < n; j = j + 1)
if(get(p, i, j) == 1 && j - i > mj - mi)
mj = j, mi = i
且get函数的定义如下
get(p, i, j)
if(p[i][j] != 0)
return p[i][j]
if(j <= i)
p[i][j] = 1
else
p[i][j] = p[i] == p[j] && get(p, i + 1, j - 1)
return p[i][j]
注意到get方法的时间复杂度除了倒数第二行代码都应该是常数,因此,而每次第二行代码的执行都会设置矩阵p的某个单元格(且不会再次设置),而又由于p总共只有n^2个单元格,故在对i和j进行完整的嵌套循环时,倒数第二行代码总共执行了不超过n^2,除去循环的次数n(n-1)/2,得到的平摊的时间复杂度只是常数级别的。
故我们可以将get方法视作是O(1)时间复杂度的方法,而在嵌套的for循环中共调用了n(n-1)/2次get方法,故整段代码的时间复杂度为O(n(n-1)/2)=O(n^2)。当然空间复杂度也因为矩阵的存在为O(n^2)。
看上去优化了很多没有,确实相较与第一个问题优化了很多,但是还远远不够,提交这段代码只会得到TLE,时间超限。
更合适的算法只需要O(n)的时空复杂度就可以解决这个问题,这就是Manacher算法。
首先假设我们用数组p记录这样的数据,p[i]表示以i为中心的最长回文子串的双翼的宽度,比如aba,中b字符的双翼宽度就为1。而a的双翼宽度均为0。显然以i为中心的最长回文字串的长度应该为1+2*p[i]。称p[i]+i为以i为中心的最长回文子串的拓展右边界,简称为i的拓展右边界。
由于p[0]=0是毋庸置疑的,假设我们已经计算出了p[0],...,p[i-1],那么对于p[i],我们可以有个快速的计算方法。记c为0,...,i-1中拓展右边界最大的元素。记w表示p[c],记cRb为c的拓展右边界,而cLb为c的拓展左边界(即c-p[c]),记j表示与i按照c镜像对其的元素,即i-c=c-j,故j=2c-i。详细见下图:

已知s[cLb~cRb]是最大回文,故能保证s[cLb-1]!=s[cRb+1]。当i<cRb时,我们可以从j的性质推断出i的性质。若p[j]<j-cLb,即以j为中心的最长回文被包含在c代表的最长回文中,那么可以保证p[i]=p[j]。这是为什么呢?
假设p[i]>p[j],就意味着s[i-p[j]-1]=s[i+p[j]+1]的成立由于i-p[j]-1=2c-j-p[j]-1>2c-j-j+cLb-1>cLb,而且显然i-p[j]-1<cRb。而i+p[j]+1=2c-j+p[j]+1<2c-j+j-cLb+1=2c-cLb+1=cRb+1,故能保证i-p[j]-1与i+p[j]+1完整地落在区间[cLb,cRb]之间。显然有j-p[j]-1与i+p[j]+1是按c有镜像关系的,而j+p[j]+1与i-p[j]-1也是镜像对齐的。因此我们可以利用回文的定义得到s[j-p[j]-1]=s[i+p[j]+1]=s[i-p[j]-1]=s[j+p[j]+1],故p[j]小于某个以j为中心的回文的宽度,这与p[j]保存最长回文的宽度这一定义相悖,故假设不成立。而对于p[i]<p[j]的情况也可以做同样的反证。因此综上所述,可以保证当i<cRb且p[j]<j-cLb时有p[i]=p[j]。
继续对其它情况进行探讨,当i<cRb且p[j]>j-cLb时,即j的拓展左边界已经突破了[cLb,cRb]的限制。这时候可以保证p[i]=cRb-i。这又是为什么呢?
假设p[i]>cRb-i,那么就有s[cRb+1]=s[2*i-cRb-1](依旧是回文的定义),而又因为2*i-cRb-1=2*i-(2*c-cLb)-1=2*(i-c)+cLb-1>=2+cLb-1=cLb+1,因此能保证s[2*i-cRb-1]落在[cLb,cRb]中。由于s[cLb~cRb]是回文,因此s[2c-2i+cRb+1]=s[2*i-cRb-1](2c-2i+cRb+1与2*i-cRb-1以c镜像对齐)。继续由于p[j]>j-cLb,因此有2c-2i+cRb+1=2c-2*(2c-j)+(2c-cLb)+1=2j-cLb+1<j+p[j]+1<=j+p[j],即2c-2i+cRb+1落在[j-p[j],j+p[j]]中,利用回文和镜像关系最终得到p[2*j-(2c-2i+cRb+1)]=p[2c-2i+cRb+1],而2*j-(2c-2i+cRb+1)=i+j-(2c-cLb)-1=cLb-1。因此依据等价关系的传递性可以得到s[cRb+1]=s[cLb-1],因此s[cLb-1~cRb+1]是比s[cLb~cRb]更长的以c为中心的回文字串,这与p[c]的定义不一致,故假设不成立。对于p[i]<cRb-i也可以做类似验证。综上所述,当i<cRb且p[j]>j-cLb时,p[i]=cRb-i。
而对于i<cRb且p[j]=j-cLb的情况,我们只能保证p[i]>=cRb-i,再也不能做更多的保证了。证明过程与上述类似,利用回文的对称性和i和j的镜像关系就可以了。
而当i>=cRb我们则不能做任何保证。可以直接以i为中心,向两端拓展,直到遇到不匹配的字符才停止。
p = int[n]
c = 0, p[0] = 0, cRb = 0
for(i = 1; i < n; i = i + 1)
leastw = 0
if(i < cRb)
j = 2 * c - i
if(p[j] < j-cLb)
p[i] = p[j]
continue
else if(p[j] > j-cLb)
p[i] = j-cLb
continue
else
leastw = j - cLb
for(lb = i - leastw, rb = i + leastw; lb >= 1 && rb < n - 1 && s[lb - 1] == s[rb + 1]; lb = lb - 1, rb = rb + 1);
p[i] = rb - i
c = i, cRb = i + p[i]
这样我们就计算出了p的所有值。细心的读者可能会发现,这样取出的所有的回文的长度都是奇数,但是偶数宽度的回文也有可能是最长的回文。
是的,所以针对这道问题,我们需要利用一个小技巧。在组成的s的每一个字符之间填充空白字符(用什么字符都可以,但是要统一,两端也要填充)。比如对于abba,可以填充为$a$b$b$a$。对这样的字符串计算p,可以发现无论相邻原字符之间距离为2,而相邻填充字符之间的距离也均为2。而这意味着原字符只会和原字符做等值判断,而填充字符也只会和填充字符做等值判断,而且可以保证以i(i为[0,2n+1]之间的任意整数)为中心的回文两端字符均为填充字符,这是因为若两端为原字符,显然可以再扩展1的长度。
而最后实际的取出的回文应该为$...$,只要去除字符0,2,...即可得到最终的回文字符串。
最后讨论一下Manacher算法的时间复杂度和空间复杂度。由于分配了长度为n的p数组,以及创建了相当于原字符串两倍的增强字符串,因此空间复杂度为O(n)。而时间复杂度上,由于内部有两个循环,内嵌的循环每次循环都将当前的最大的向右拓展值增加1,而最多只能增加到2n+1,因此执行内嵌循环所需要的总时间为O(2n+1)。而对于外部循环,在移除内循环的情况下,显然每次执行的时间花费都是常数,因此外部循环的总的时间复杂度为O(n),加总即可得到总的时间复杂度为O(2n+1)+O(n)=O(n)。
最后代码提供给有需要的人:

package cn.dalt.leetcode;
/**
* Created by dalt on 2017/6/7.
*/
public class LongestPalindromicSubstring2 {
public String longestPalindrome(String s) {
if (s.length() <= 1) {
return s;
}
char[] buf = new char[s.length() * 2 + 1];
for (int i = 0, bound = buf.length; i < bound; i += 2) {
buf[i] = ' ';
}
for (int i = 1, bound = buf.length; i < bound; i += 2) {
buf[i] = s.charAt(i >> 1);
}
int blen = buf.length;
int blenMinusOne = blen - 1;
int[] width = new int[blen];
int mxWidth = 0;
int mxCenter = 0;
int mxRightBound = mxCenter + mxWidth; //The right bound include
width[0] = 0;
int bestCenter = 0;
for (int i = 1; i < blen; i++) {
int leastwidth = 0;
//If locate at the circle organized by current rightest palindromic substring
if (i < mxRightBound) {
int j = 2 * mxCenter - i;
int remain = mxRightBound - i;
if (width[j] > remain) {
width[i] = remain;
continue;
} else if (width[j] < remain) {
width[i] = width[j];
continue;
} else {
leastwidth = remain;
}
}
int rbound = i + leastwidth;
int lbound = i - leastwidth;
while (rbound < blenMinusOne && lbound > 0 && buf[rbound + 1] == buf[lbound - 1]) {
rbound++;
lbound--;
}
width[i] = rbound - i;
//Change the rightest palindromic substring if needed
mxCenter = i;
mxWidth = width[i];
mxRightBound = i + mxWidth;
if (mxWidth > width[bestCenter]) {
bestCenter = i;
}
}
int longestlength = width[bestCenter];
int startPoint = (bestCenter - longestlength + 1) / 2;
return s.substring(startPoint, startPoint + longestlength);
}
}
Leetcode:Longest Palindromic Substring分析和实现的更多相关文章
- [LeetCode] Longest Palindromic Substring 最长回文串
Given a string S, find the longest palindromic substring in S. You may assume that the maximum lengt ...
- Leetcode Longest Palindromic Substring
Given a string S, find the longest palindromic substring in S. You may assume that the maximum lengt ...
- LeetCode:Longest Palindromic Substring 最长回文子串
题目链接 Given a string S, find the longest palindromic substring in S. You may assume that the maximum ...
- [LeetCode] Longest Palindromic Substring(manacher algorithm)
Given a string S, find the longest palindromic substring in S. You may assume that the maximum lengt ...
- C++ leetcode Longest Palindromic Substring
明天就要上课了,再过几天又要见班主任汇报项目进程了,什么都没做的我竟然有一种迷之淡定,大概是想体验一波熬夜修仙的快乐了.不管怎么说,每天还是要水一篇博文,写一个LeetCode的题才圆满. 题目:Gi ...
- Leetcode: Longest Palindromic Substring && Summary: Palindrome
Given a string s, find the longest palindromic substring in s. You may assume that the maximum lengt ...
- Leetcode: Longest Palindromic Substring. java
Given a string S, find the longest palindromic substring in S. You may assume that the maximum lengt ...
- LeetCode——Longest Palindromic Substring
Given a string S, find the longest palindromic substring in S. You may assume that the maximum lengt ...
- [LeetCode]Longest Palindromic Substring题解(动态规划)
Longest Palindromic Substring: Given a string s, find the longest palindromic substring in s. You ma ...
随机推荐
- 1 秒杀系统模拟基础实现,使用DB实现
本文根据动脑学院的一节类似的课程,改编实现.分别使用DB和redis来完成. 隔离的解释 业务隔离:将秒杀业务独立出来,尽量不与其他业务关联,以减少对其他业务的依赖性.譬如秒杀业务只保留用户id,商品 ...
- Android开发技巧——写一个StepView
在我们的应用开发中,有些业务流程会涉及到多个步骤,或者是多个状态的转化,因此,会需要有相关的设计来展示该业务流程.比如<停车王>应用里的添加车牌的步骤. 通常,我们会把这类控件称为&quo ...
- C++11中提供了std::bind
再来看看std::bind C++11中提供了std::bind.bind()函数的意义就像它的函数名一样,是用来绑定函数调用的某些参数的. bind的思想实际上是一种延迟计算的思想,将可调用对象保存 ...
- vs无法调试 ,还没有为该文档加载任何符号
如何:启用非托管代码调试 位于“项目设计器”的“调试”页上的“非托管代码调试”属性确定是否支持本机代码调试. 如果要调用 COM 对象,或启动调用您的项目的.以本机代码编写的自定义程序,并且需要调 ...
- vim 插件使用
a.vim的安装相当简单,下载a.vim后丢进Vim插件目录(一般为~/.vim/plugin),必要时再重启一下Vim就可以使用了. 头/源文件切换命令 :A 头文件/源文件切换 :AS 分割窗后并 ...
- 进程(并发,并行) join start 进程池 (同步异步)
一.背景知识 顾名思义,进程即正在执行的一个过程.进程是对正在运行程序的一个抽象.进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一.操作系统的其他所有 ...
- 处理SQL Server 异常常用步骤
处理SQL Server 异常常用步骤 SQL Server常见的问题主要是SQL问题造成,常见的主要是CPU过高和阻塞. 一.CPU过高的问题 1.查询系统动态视图查询执行时间长的sql语句 WIT ...
- Google全球服务器根域名的IP地址
Google全球服务器根域名的IP地址 目前GOOGLE正与ZF继续谈判,此谈判结果将直接影响该公司在中国的存在此前,由于曾出现Google服务器被墙,而致使众网友只能FQ搜索的窘境而目前尚不知道若G ...
- tensorflow学习笔记(三十九):双向rnn
tensorflow 双向 rnn 如何在tensorflow中实现双向rnn 单层双向rnn 单层双向rnn (cs224d) tensorflow中已经提供了双向rnn的接口,它就是tf.nn.b ...
- 关于OPC自动化接口编程(OPCDAAuto.dll)几点注意问题
为了能够在工作中方便的应用OPC和充分的理解OPC的开发流程.内部机制,这两天正在研究开发OPC客户端程序,一般我们开发OPC客户端程序有以下几种方式: (1) 使用OPCNetAPI,需 ...