Manacher算法

导读

Manacher算法是解决有关回文字符一类的问题。之前寒假看过,琢磨了好久,现在来看,没有那么陌生了,重新理了一遍后,虽然也写了好几遍代码,但感觉已经有是在背代码的感觉了!还是理解万岁。

问题描述

给你一串字符串,返回这个字符串里最长的回文子字符串,或者返回这个最长的回文子字符串的长度。
比如: abccbaaa : 最长回文子字符串为abccba,长度为6
a d s b f p r r p f b l d e : 最长回文子字符串为:bfprrpfb,长度为8
关于什么是回文:
   回文就是正着读和反着读的是一样的,或者说他在中间有一个对称轴,关于这个轴两边是镜像的。
   顺便贴张图片,前几天在LeetCode上看到的,震惊到我了。

思路

普通解法:
对于字符串中的每个字符,都以他为中心,向两边扩来检查,遇到不相等的就停下,在这个过程中,记录这个最大长度和相对应的字符。
这样对于 奇数个的字符串 还可以用,但对于 偶数个的字符串 就不行了。举个例子:aba,可以找出答案, abba,依次为中心往外扩,得到的答案不对。
所以,这里我们就要对原来的字符串做一些预处理:在每个字符之间插入相同的字符,可以是#,可以是%,甚至可以是原字符串里的字符都可以,这个无所谓,这是起一个辅助作用,帮助我们占位置,是字符都变成 奇数个。

Manacher:
在介绍该算法之前,先介绍需要用到的一些变量:
pArr : 一个整型数组,长度和经过处理后的字符串的长度相同,每个位置上代表的意思是 在字符串里,以该位置为中心的话,他的最长回文的半径是多长,注意是半径,不是直径。
pR : 一个整型变量, 意思是目前为值,可以扩到的最长回文半径的右边界,因为这样的话,在pR以左的部分我们都可以利用已知信息来加速这个扩充过程。
center: 一个整型变量, 那个最长回文右边界pR对应着 回文中心,也就是 以center为中心,可以扩到pR长度。

好了,下面开始实施Manacher算法了:
假设我们已经遍历到了i位置,i位置有2中可能
第1中可能: i 在pR的右侧,这样Manacher派不上用场,直接暴力扩就好。
第2中可能: i 在pR的左侧。 这种可能又分为3种小情况。这个过程就是Manacher的核心思想了。
第①: i 关于 center 的对称点 i一撇, i一撇的回文半径我们之前已经算出来了,如果他的回文半径在pR的范围里,那么 i 的回文半径 和 i 一撇的回文半径相同。看着示意图应该就明白了。
(这个示意图我懒得分就贴到一起啦…)

第②; i 关于 center 的对称点 i一撇, i一撇的回文半径我们之前已经算出来了,如果他的回文半径压着pR的范围左边界, 那么i 的回文半径是从pR往右扩的,i-pR的范围可以确定是回文了。 也是看示意图。

第③:i 关于 center 的对称点 i一撇, i一撇的回文半径我们之前已经算出来了,如果他的回文半径已经超过了pR的范围,那个 i 的回文半径长度最长就是到pR了,因为如果比这个长的话,那么原来的pR就不会是现在这个值了,与我们之前定义好的,进而求出来的pR相矛盾。 也是看看示意图。

代码实现

虽然情况是分很多种的,但是写成代码后还是比较简洁的,还是像最前面写的那样,理解就好。

    // 对原字符串 进行 预处理
	public static char[] manacherString(char[] chs) {
   
		char[] chsA = new char[chs.length*2+1];
		int index = 0;
		for(int i = 0; i < chsA.length; i++) {
   
			chsA[i] = i % 2 == 0 ? '#' : chs[index++];
		}
		return chsA; 
	}
	
	// Manacher算法
	public static int manacher(String s) {
   
		if(s == null || s.length() == 0) {
   
			return 0;
		}
		
		char[] charArr = manacherString(s.toCharArray());
		int pR = -1;
		int center = -1;
		int[] pArr = new int[charArr.length];
		int len = Integer.MIN_VALUE;
		
		for(int i = 0; i < charArr.length; i++) {
   
			pArr[i] = i < pR ? Math.min(pR-i, pArr[center*2-i]) : 1;
			while(i+pArr[i] < charArr.length && i-pArr[i] >= 0) {
   
				if(charArr[ i+pArr[i] ] == charArr[ i-pArr[i] ] ) {
   
					pArr[i]++;
				}else {
   
					break;
				} 
			}
			
			if(i + pArr[i] > pR) {
   
				pR = i + pArr[i];
				center = i;
			}
			len = Math.max(len, pArr[i]);
  		}
		
		return len-1; 
	}

小应用

给你一个字符串,要求只能在他的末尾添加字符,使他整体构成一个回文字符串。
思路:

改写Manacher算法,该问题就是求必须包含最后一位字符的最长回文字符串是什么(也就是pR到最后一个位置时就停止了),在吧剩下的字符逆序过来 再 添加到末尾即可。

代码:
	// 对原字符串 进行 预处理
	public static char[] manacherString(char[] chs) {
   
		char[] chsA = new char[chs.length*2+1];
		int index = 0;
		for(int i = 0; i < chsA.length; i++) {
   
			chsA[i] = i % 2 == 0 ? '#' : chs[index++];
		}
		return chsA; 
	}
	
	// 返回的是添加的那部分字符
	public static String manacherAd(String s) {
   
		if(s == null || s.length() == 0) {
   
			return null;
		}
		
		char[] charArr = manacherString(s.toCharArray());
		int center = -1;
		int pR = -1;
		int[] pArr = new int[charArr.length];
		int maxContainsEnd = 0;		// 包含最后一位字符对应的回文是有多长
		
		for(int i = 0; i < pArr.length; i++) {
   
			pArr[i] = i < pR ? Math.min(pR-i, pArr[2*center-i]) : i;
			while(i+pArr[i] < charArr.length && i-pArr[i] >= 0) {
   	// 保证扩的时候不越界
				if(charArr[ i+pArr[i] ] == charArr[ i-pArr[i] ] ) {
   
					pArr[i]++;
				}else {
   
					break;
				}
			}
			
			if(i+pArr[i] > pR) {
   
				pR = i + pArr[i];
				center = i;
			}
			if(pR == charArr.length) {
   
				maxContainsEnd = pArr[i];
				break;
			}
		}
		
		char[] res = new char[ s.length()-maxContainsEnd+1 ];
		for(int i = 0; i < res.length; i++) {
   
			res[res.length-1-i] = charArr[i*2+1];
		}
		
		return String.valueOf(res);
		
	}
全部评论

相关推荐

2025-12-24 13:37
已编辑
浙江农林大学 C++
Eryi_是不是名字...:金牌哥,你这要是考研C9进复试线乱杀啊。可以试试字节腾讯华子,我感觉投华子实习概率很大啊
点赞 评论 收藏
分享
2025-12-24 15:25
已编辑
门头沟学院 前端工程师
是腾讯的csig腾讯云,前天晚上九点突然打电话约面,激动的通宵学了一晚上,第二天状态很差改了今天(以后再也不通宵学习了)感觉自己浪费了面试官一个半小时单纯手写+场景,无八股无项目无算法,打击真的很大,全是在面试官提醒的情况下完成的,自己技术方面真的还是有待提高,实力匹配不上大厂和已经面试的两个公司完全不一样,很注重编码能力和解决问题的能力,然而我这两个方面都很薄弱,面试官人很好很耐心的等我写完题目,遇到瓶颈也会提醒我,写不出题也会很耐心的跟我讲解好感动,到最后面试结束还安慰我打算把下周最后一场面试面完之后就不面啦,如果能去实习还是很开心,但是最重要的还是好好努力提高技术以下是面经第一题//&nbsp;实现一个解析&nbsp;url&nbsp;参数的函数function&nbsp;parseUrl(urlStr)&nbsp;{//&nbsp;TODO}parseUrl('*********************************************');//&nbsp;返回&nbsp;{a:&nbsp;1,&nbsp;b:&nbsp;2,&nbsp;c:&nbsp;3}追问:在链接里见过什么部分?用&nbsp;hash&nbsp;路由的话放在哪第二题//&nbsp;考虑有一个异步任务要执行,返回&nbsp;Promise,这个任务可能会失败,请实现&nbsp;retry&nbsp;方法,返回新方法,可以在失败后自动重试指定的次数。/***&nbsp;异步任务重试*&nbsp;@param&nbsp;task&nbsp;要执行的异步任务*&nbsp;@param&nbsp;times&nbsp;需要重试的次数,默认为&nbsp;3&nbsp;次*/function&nbsp;retry(task,&nbsp;times&nbsp;=&nbsp;3)&nbsp;{//&nbsp;TODO:&nbsp;请实现}//&nbsp;---------------测试示例&nbsp;----------------//&nbsp;原方法const&nbsp;request&nbsp;=&nbsp;async&nbsp;(data)&nbsp;=&gt;&nbsp;{//&nbsp;模拟失败if&nbsp;(Math.random()&nbsp;&lt;&nbsp;0.7)&nbsp;{throw&nbsp;new&nbsp;Error('request&nbsp;failed');}const&nbsp;res&nbsp;=&nbsp;await&nbsp;fetch(&#39;https://jsonplaceholder.typicode.com/posts&#39;,&nbsp;{method:&nbsp;'POST',body:&nbsp;JSON.stringify(data),});return&nbsp;res.json();}//&nbsp;新的方法const&nbsp;requestWithRetry&nbsp;=&nbsp;retry(request);//&nbsp;使用async&nbsp;function&nbsp;run()&nbsp;{const&nbsp;res&nbsp;=&nbsp;await&nbsp;requestWithRetry({&nbsp;body:&nbsp;'content'&nbsp;});console.log(res);}run();第三题就是给&nbsp;retry&nbsp;函数添加类型注释,用到泛型第四题:在组件库中将&nbsp;Alert&nbsp;用&nbsp;api&nbsp;的形式实现(应该就是&nbsp;message&nbsp;这个组件)怎么渲染到一个浮层里而不是原地渲染出来
不知道怎么取名字_:技术这个东西,太杂了,而且要下功夫的
查看5道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务