Longest Palindromic Substring

原题链接 Longest Palindromic Substring

意思是找到最长的回文子串,注意子串和子序列的区别


蛮力法就将每个可能的子串都找出来判断是否是回文子串,找最大的那一个,速度上慢的吓人,光找子串的速度就到O(n2)了,所以此处需要找到一个简单的方法找到回文子串。
既然回文子串是从中心开始向两边延伸时,左右两边相同,那么就可以从每个字符开始都向两边延伸,看能找到的最长回文子串是多长就行了。

这就出现了一个问题,回文子串的中心是什么地方,对于abcba来说,中心是c。那么对于abccba呢,中心是cc中间的位置,这个位置没有明确的下标。二者的区别在于一个数量上一个是奇数,一个是偶数。为了统一,这里有一种技巧,就是将源字符串s统一转化成奇数个数,方法是虚拟的加上一些字符,注意是虚拟加,实际上并不改变源s,假设s为babad,那么加上虚拟字符的s是#b#a#b#a#d#,这样,不管什么字符串全都是奇数个数了,总数为2 * s.size() + 1,而这里还有一个巧妙的地方,就是加上虚拟字符后的字符的索引除以2正好是在源s中没有加上虚拟字符的索引,这里把加上虚拟字符后的s成为extend_s

#   b   #   a   #   b   #   a   #   d   #   extend_s
0   1   2   3   4   5   6   7   8   9   10  extend_s的下标索引
0   0   1   1   2   2   3   3   4   4   5   除以2后的结果b   a   b   a   d   s
0   1   2   3   4   s的下标索引

所以在程序中遍历extend_s时只需要除以2,就可得得到s中对应位置的字符。

扩展s是为了解决奇数偶数的问题,这个问题存在在什么地方呢。比方说要找的结果是abccba,那么在切割的时候是从cc中间切分的,而对于abcba而言,切分是在c字符上切分的,这个还好说,切分的位置有正常的索引下标,可是abccba的切分位置是中间,下标是x.5?,不存在的。所以扩展后的s刚好解决的就是这个难题

a   b   c   c   b   a   源字符串s
0   1   2   3   4   5   下标索引|切分位置#   a   #   b   #   c   #   c   #   b   #   a   #   扩展字符串extend_s
0   1   2   3   4   5   6   7   8   9   10  11  12  下标索引|切分位置
-----------------------------------------------------------------a   b   c   b   a   源字符串s
0   1   2   3   4   下标索引|切分位置#   a   #   b   #   c   #   b   #   a   #   扩展字符串extend_s
0   1   2   3   4   5   6   7   8   9   10  下标索引|切分位置

通过扩展,不管是奇数个数的字符串还是偶数个数的字符串,都有一个明确的下标表示切分位置,这个位置正是回文子串的中心。程序就简单多了,从extend_s的每一个字符开始,向两边扩展,相等则继续扩展,不相等则停止扩展。需要注意的是,程序中遍历的仍然是源字符串s,只是在下标上做了点手脚,所以区分#还是字母的方法就看下标是奇数还是偶数,#的下标永远是偶数。代码如下

class Solution {
public:string longestPalindrome(string s) {if(s.size() <= 1)return s;string ans("");for(int i = 0; i < 2 * s.size() + 1; ++i){   /* 以extend_s[i]为中心,向两边扩展,这个extend_s[i]既有可能是#,也有可能是字母 */find_palindrome(i - 1, i + 1, s, ans);}return ans;}
private:void find_palindrome(int lhs_idx, int rhs_idx, std::string& s, std::string& ans){/* 判断边界 */while(lhs_idx >= 0 && rhs_idx < 2 * s.size() + 1){/* #的下标永远是偶数,所以判断是否是奇数,然后比较s中的两个字符 */if(lhs_idx % 2 != 0 && rhs_idx % 2 != 0 && s[lhs_idx / 2] != s[rhs_idx / 2]){break;}--lhs_idx;++rhs_idx;}/* * 这是为了解决边界0的问题,因为如果找到左边界(0)时仍然相等,那么lhs_idx会是-1* 因为找到的回文子串应该是s[lhs_idx / 2 + 1, rhs_idx / 2 - 1]* 由于lhs_idx是-1,-1/2==0,正常结果应该是s[0, rhs_idx / 2 - 1],* 会导致lhs_idx / 2 + 1 == 1,不为0,使结果长度变小* 下面会说另一种解决办法*/if(lhs_idx == -1)lhs_idx = -2;if(static_cast<int>(ans.size()) < rhs_idx / 2 - lhs_idx / 2 - 1){ans = s.substr(lhs_idx / 2 + 1, rhs_idx / 2 - lhs_idx / 2 - 1);}}
};

因为左边界会有使结果长度变小的风险,上述程序中手动将lhs_idx从-1改为-2使lhs_idx/2为-1不为0。另一种解决办法是在extend_s头部再添加一个字符@代表头部,也可以解决上述问题。



Palindromic Substrings

原题链接Palindromic Substrings

和上面的题差不多,找所有可能的回文子串的数量,即使回文子串一样,但是在源s中的起止位置不同,也算作不同的回文子串,方法和上面一样,也需要添加虚拟字符,记得数个数。

class Solution {
public:int countSubstrings(string s) {int cnt = 0;for(int i = 0; i < 2 * s.size() + 1; ++i){/* 如果不是虚拟字符,+1 */if(i % 2 != 0){++cnt;}find_palindrome(i - 1, i + 1, s, cnt);}return cnt;}private:void find_palindrome(int lhs_idx, int rhs_idx, std::string& s, int& cnt){while(lhs_idx >=0 && rhs_idx < 2 * s.size() + 1){if(lhs_idx % 2 != 0 && rhs_idx % 2 != 0){if(s[lhs_idx / 2] == s[rhs_idx / 2])++cnt;elsebreak;}--lhs_idx;++rhs_idx;}}
};


Longest Palindromic Subsequence

原题链接Longest Palindromic Subsequence

和上面不同的是,这个是求最长的回文子序列,子序列是将源字符串中的任意多个字符删除后生成的新字符串,结果串中挨着的两个字符在源字符串中不一定是挨着的,因为二者中间可能有被删掉的字符。


这个就不能用上述方法解决了,因为如果s[lhs_idx / 2] != s[rhs_idx / 2],那么s[lhs_idx / 2]还有可能和lhs_idx / 2前面的某个字符相等,构成子序列,所以上述方法行不通。
涉及到子序列的题可以这样想,假设len[i, j]表示范围[i, j]内最长的回文子序列长度,那么有两种情况

  1. s[i] == s[j],那么len[i, j]一定等于2 + len[i + 1, j - 1]
  2. s[i] != s[j],那么len[i, j] = max(len[i + 1, j], len[i, j - 1])

典型的动态规划,直接上代码(这里采用非递归方式)

class Solution {
public:int longestPalindromeSubseq(string s) {if(s.size() <= 1)return s.size();std::vector<std::vector<int> > dp(s.size(), std::vector<int>(s.size(), 0));for(int i = 0; i < s.size(); ++i)dp[i][i] = 1;/* * 递归的动态规划是从最大范围递归到单个字符然后逐层返回* 非递归就是实现逐层返回的方法,从单个字符扩展到最大范围*/for(int i = s.size() - 1; i >= 0; --i){for(int j = i + 1; j < s.size(); ++j){if(s[i] == s[j]){dp[i][j] = 2;if(i + 1 <= j - 1)dp[i][j] += dp[i + 1][j - 1];}else{dp[i][j] = std::max(dp[i][j - 1], dp[i + 1][j]);}}}return dp[0][s.size() - 1];}
};

设计到子序列的问题,或者可以转换成上述两种可能的问题都是动态规划的典型问题,动态规划在算法中使用频率很高,常见问题是IO背包,很重要!!!!



Shortest Palindrome

原题链接Shortest Palindrome

意思是给定一个源字符串s,在s的头部添加尽可能少的字符,使s成为一个回文字符串。
既然在头部添加,那么s[0]一定是扩展后的中心吗,不一定,题中第一个例子就是反例。
那么在头部前面添加的一定就是源字符串s较后面的几个字符,这样才能达到最少,也就是说需要找到源s中从头开始最长的回文子串,然后将后面的多余部分添加到头部,只有这样才有可能是最小的。

a a c e c a a  a|分割,前面是s最长的回文子串,且从头开始
只需要把多出的a也添加到头部即可a b c c b a  d a f g h|分割,理由同上  

所以要求就转换成了,寻找源字符串s最长的回文子串,要求这个子串从源s的起始位置开始。
这就很麻烦了,既不能使用上面的方法从每个字符开始向两边扩展,也不能直接从头开始判断每个子串是否是回文串(太麻烦,复杂度过高),因为如果用这种方法,就需要

  1. 固定头部,从尾部开始找第一个和s[0]相等的位置back
  2. 判断s[0, back]是否是个回文串,如果是,结束,如果不是,接着找第二个和s[0]相等的位置

这样来来回回遍历了好几遍,况且如果源s重复度过高(每一个都和s[0]相等),那么遍历的次数就更多了,显然不合适。

解决这一问题用到了KMP算法(第一次见KMP可以求回文串)
首先复习一下KMP吧,先别着急想怎么用KMP解决这道题。
KMP,字符串处理算法,典型的问题是在一个源字符串中判断是否包含某个子串,相比于普通的一个一个比较,KMP可以实现多字符跳过,不需要比较没有的字符,大大提高的运行效率。
KMP算法的核心在于根据源字符串构建前缀数组
前缀数组中的元素含义是上一个和当前字符相等的位置 + 1,同时当前字符的前几个字符也都有这些位置,并且位置不间断一直到1,这个位置+1同时也表示第几个字符(从1开始),举个例子

0   1   2   3   4   5   6   7   8   9   10  下标索引
a   b   c   a   b   c   a   a   b   a   c   源字符串s
0   0   0   1   2   3   4   1   2   1   0   前缀数组/*
解释:
s[0] == a,前面没有字符,初始为0
s[1] == b, 第一个字符a和b不相等,为0
s[2] == c,同上
s[3] == a,和第一个字符a相等,为0 + 1 = 1,表示s[3]前面和自己相等的字符是第1个字符
s[4] == b,和前面s[3]记录的位置1(第1个字符)后面的位置2(第2个字符)b相等,为2
s[5] == c,同上,和第3个字符c相等,为3
s[6] == a,同上,和第4个字符a相等,为4
s[7] == a,和前面s[6]记录的位置4(第4个字符)后面的位置5(第5个字符)b不相等找第4个字符a记录的位置1(第1个字符)后面的位置2(第2个字符)b不相等找第2个字符b记录的位置0(第0个字符)后面的位置1(第1个字符)a相等,为1
s[8] == b,和前面s[7]记录的位置1(第1个字符)后面的位置2(第2个字符)b相等,为2
s[9] == a,和前面s[8]记录的位置2(第2个字符)后面的位置3(第3个字符)c不相等,找第2个字符b记录的位置0(第0个字符)后面的位置1(第1个字符)a相等,为1
s[10]== c,和前面s[9]记录的位置1(第1个字符)后面的位置2(第2个字符)b不相等,找第2个字符b记录的位置0(第0个字符)后面的位置1(第1个字符)a不相等,找第1个字符a记录的位置0(第0个字符)后面的位置1(第1个字符)a不不相等,....有个规定,到达0时如果和第1个字符仍然不相等,记为0
*/

构造的代码就比较容易写了,如下

void treat_prefix(std::string& extend_s, int *prefix)
{int l = 0;prefix[0] = 0;for(int i = 1; i < extend_s.size(); ++i){/* 其实每次l都等于prefix[i - 1],只是在跳的过程中一直prefix[prefix[i - 1] - 1] *//* 如果一直不相等,一直往前跳,直到为0 */while(l > 0 && extend_s[i] != extend_s[l])l = prefix[l - 1];if(extend_s[i] == extend_s[l])++l;prefix[i] = l;}
}bool kmp(std::string& source, std::string target)
{int *prefix = new int[target.size()];treat_prefix(target, prefix);int l = 0;for(int i = 0; i < source.size(); ++i){/** 跟source[i]比较,如果你不和我l位置的字符相等* 那我跳到前面,看看咱俩相不相等* 因为kmp算法的前缀数组保证,跳到前面后,target[0, l - 1]这部分仍然是和source[i - l - 1, i - 1]相等的,不需要重复比较*/while(l > 0 && source[i] != target[l])l = prefix[l - 1];if(source[i] == target[l])++l;if(l == target.size())return true;;}return false;
}

其实上面的从0开始的最长回文子串只用到了prefix数组,方法是先将源字符串s扩展,扩展方式如下

std::string reverse_s(s.rbegin(), s.rend()); //求s的翻转字符串
s = s + "#" + reverse_s;

假设s为catacb,那么扩展后的s为

catacb#bcatac

对此调用生成前缀数组的函数

0   1   2   3   4   5   6   7   8   9   10  11  12  下标索引
c   a   t   a   c   b   #   b   c   a   t   a   c   扩展后的s
0   0   0   0   1   0   0   0   1   2   3   4   5   prefix数组假设源字符串(未扩展)从0开始的最长回文子串为从s[i]到s[j],
那么子串s[i]到s[j]和翻转后从s[j]到s[i]肯定相等,因为回文字符串的特性,翻转后仍然相等
那么假设扩展后这段回文子串是s[i]到s[j](#前面)和s[p]到s[q](#后面),那么这两个子串一定相同
所以通过前缀函数的构造后面的字符串的prefix中的位置一定分别对应s[i]到s[j],那么最后一个字符的prefix元素就是最长回文子串的长度

根据以上推论,代码就比较容易了

class Solution {
public:string shortestPalindrome(string s) {std::string reverse_s(s.rbegin(), s.rend());std::string extend_s = s + "#" + reverse_s;int *prefix = new int[extend_s.size()];treat_prefix(extend_s, prefix);/* 获取最长回文子串的长度,然后把后面多余的部分也添加到前面 */int max_palindrome_len = prefix[extend_s.size() - 1];std::string ans = s;while(max_palindrome_len < s.size()){ans = s[max_palindrome_len++] + ans;}delete []prefix;return ans;}private:void treat_prefix(std::string& extend_s, int *prefix){int l = 0;prefix[0] = 0;for(int i = 1; i < extend_s.size(); ++i){while(l > 0 && extend_s[i] != extend_s[l])l = prefix[l - 1];if(extend_s[i] == extend_s[l])++l;prefix[i] = l;}}
};

这道题主要就是利用KMP算法,算是个积累知识,处理回文字符串时多一种考虑的可能

每天一道LeetCode-----最长回文子串/序列,从头开始的最长回文子串长度相关推荐

  1. 最长公共子序列求序列模板提_最长公共子序列

    最长公共子序列求序列模板提 Description: 描述: This question has been featured in interview rounds of Amazon, MakeMy ...

  2. 怎么判断一个字符串的最长回文子串是否在头尾_最长回文字串/子序列问题(leetcode5,9,519)

    leetcode 5 最长回文子串 给定一个字符串 s,找到 s 中最长的回文子串.你可以假设 s 的最大长度为 1000. 示例 1: 输入: "babad" 输出: " ...

  3. 【python】一道LeetCode搞懂递归算法!#131分割回文串 #以及刷LeetCode的一点点小心得 [数据结构与算法基础]

    题目:给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串.返回 s 所有可能的分割方案. # 示例 输入: "aab" 输出: [["aa",&q ...

  4. java最长回文子序列_算法--字符串:最长回文子序列

    转自:labuladong公众号 子序列问题是常见的算法问题,而且并不好解决. 首先,子序列问题本身就相对子串.子数组更困难一些,因为前者是不连续的序列,而后两者是连续的,就算穷举都不容易,更别说求解 ...

  5. 【LeetCode】【HOT】3. 无重复字符的最长子串(哈希表)

    [LeetCode][HOT]3. 无重复字符的最长子串 文章目录 [LeetCode][HOT]3. 无重复字符的最长子串 package hot;import java.util.HashMap; ...

  6. python找出字符串中的最长回文串子序列

    回文串,即: nums = 'aba' print(nums == nums[::-1]) # True 反转该序列后和之前元素相等 这里我们需要找出给定字符串里的最长回文串,即: nums = 'a ...

  7. 【LeetCode】0395.至少有K个重复字符的最长子串

    题目要求 本题共有两个要求 符合要求的字符串中每一个字符出现的次数都要大于等于给与的数字K 最终返回结果的是符合要求的字符串中最长的字符串长度 算法思想 hash_map + 递归 + 分治 使用数组 ...

  8. leetcode17. 电话号码的字母组合--每天刷一道leetcode算法系列!

    作者:reed,一个热爱技术的斜杠青年,程序员面试联合创始人 前文回顾: leetcode1. 两数之和--每天刷一道leetcode系列! leetcode2. 两数相加--每天刷一道leetcod ...

  9. atoi函数_每日一道 LeetCode (50):字符串转换整数 (atoi)

    ❝ 每天 3 分钟,走上算法的逆袭之路. ❞ 前文合集 每日一道 LeetCode 前文合集 代码仓库 GitHub:https://github.com/meteor1993/LeetCode Gi ...

最新文章

  1. mysql性能监控qps,tps,iops
  2. 日记 [2008年03月23日]LINUX网关后面的pptp ***客户机连接***
  3. Flask实战2问答平台-首页布局,功能完成
  4. 清华硕士面试阿里惨遭淘汰,网友:并非所有都是强者,也要看人
  5. Python:__slots__()方法和@property方法
  6. mui hello html5 安装,HBuilder开发App Step1——环境搭建,HelloMUI 以及真机调试(示例代码)...
  7. 洛谷P2480 [SDOI2010]古代猪文(卢卡斯定理+中国剩余定理)
  8. c语言选择结构程序设计笔记,C语言选择结构程序设计.ppt
  9. 【网络】几种常见的协议
  10. redhat和ubuntu系统下挂载ntfs文件系统的方法(转载)
  11. catalina.out 日志切割及定时清理
  12. android 内嵌web,Android《内嵌浏览器-WebView》
  13. 美丽的花蝴蝶 动人的海豚音 天后[Mariah Carey玛丽亚·凯莉]全集
  14. 理解频域、时域、FFT和加窗 加深对信号的认识
  15. java 读取gzip_Java读取GZIP
  16. 如何对关键词密度设置
  17. Unity UGUI中两点之间连线的通用实现
  18. 老照片怎么修复清晰?轻松几步让图片焕发新生
  19. Linux不允许进程被杀,linux – 我的进程被杀了但我无法理解内核通知
  20. 给定一个大小为 *n* 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 *⌊ n/2 ⌋* 的元素。

热门文章

  1. vue 动态添加class_前端开发:Vue项目实战-Music
  2. Software Engineering 265
  3. vue 实现的评分小星星组件,包括半星
  4. 华为快应用引擎架构及开发实践
  5. 如何通过putty软件远程登录并且控制linux平台
  6. TP5与TP3.X对比
  7. 无法删除DLL文件解决方法(转)
  8. SQL Server 数据库优化文章
  9. Easyui入门视频教程 第11集---Window的使用
  10. iOS之“支付宝支付”开发流程