从零学算法5

5.给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”

  • 暴力解法。无非就是双重循环,截取出所有子串,判断是否为回文串,然后取最大值。稍微优化一下,如果此时子串长度小于当前最长的回文子串 max 就直接跳过,当前子串长度为 j - i +1
  •   String s;
      public String longestPalindrome(String s) {
          this.s = s;
          int n = s.length();
          // start:最长回文子串起始下标
          int max = 0,start = 0;
          for(int i=0;i<n;i++){
              for(int j=i;j<n;j++){
                  if(j-i+1 < max)continue;
                  if(isPalindrome(i,j)){
                      max = Math.max(max, j-i+1);
                      start = i;
                  }
              }
          }
          return s.substring(start, start+max);
      }
      public boolean isPalindrome(int i,int j){
      // 根据回文串对称的性质从左右两端往中间找,有不一样的就不是
          while(i<j){
              if(s.charAt(i++)!=s.charAt(j--))return false;
          }
          return true;
      }
    
  • 中心扩散法。遍历 s 的每个下标,以其为中心点向两边扩散,看是否为相同字符,就能得到以该点为中心的回文串。但是回文串不一定为奇数长度,即中心点处长度不一定为 1,比如 abccba,中心点处其实为 cc,所以就在扩散时定义初始左右起点为这两个字符的下标即可。
  •   public String longestPalindrome(String s) {
          int n = s.length();
          int max = 0,start = 0;
          for(int mid=0;mid<n;mid++){
              if(n-mid<max/2)break;
              int i=mid,j=mid;
              //有相同的点就移动初始右端点
              // 为什么不需要移动左端点?看下面 mid = j 部分
              while(j<n-1 && s.charAt(j)==s.charAt(j+1))j++;
              // i~j 这一段的点作为中心处已经在这一轮考虑过,所以之后直接跳过即可
              // 同时这也保证了下一轮 i 初始化为 mid 时,i 的左边不会和 i 处字符重复
              // 相当于在上一轮就移动了左端点,遍历顺序为从左到右,所以首轮左处无端点无需处理
              mid = j;
              // 向两端延伸求最大回文串长度
              while(i>0 && j<n-1 && s.charAt(i-1)==s.charAt(j+1)){
                  i--;
                  j++;
              }
      		// 更新最大值和最大回文子串起始下标
              if(j-i+1>max){
                  max = j-i+1;
                  start=i;
              }
          }
          return s.substring(start, start+max);
      }
    
  • 动态规划法。根据上面中心扩散法判断回文串的方式,可以大致看到动态规划的雏形。假定 boolean dp[left][right] 表示子串 s[left:right] 是否为回文串。那么从 dp[left][right] 为 true 开始,如果 s.charAt(left-1)==s.charAt(right+1),就可以得到 dp[left-1][right+1] 为 true,即 dp[l][r] = dp[l+1][r-1] && s.charAt(l-1)==s.charAt(r+1)。得到递推公式以后还需要确定一下边界条件。以下 s.charAt(left) 我就简写成 s[left] 了,如果 s[left]!=s[right],说明当前子串左右两端字符不相同,那么你怎么都不会是回文串;如果相同,那么当子串长度小于等于 3 时,你一定是回文串,比如 aba,bb,a,否则就根据 dp[left + 1][right - 1] 递推。
  • 考虑到dp[left][right] 依赖 dp[left + 1][right - 1] 的特性,比如 dp[0][3],要先知道 dp[1][2] ,那 left 在外层遍历是肯定不可能先得到 dp[1][x] 再得到 dp[0][x] 的,所以遍历顺序需要调整为外层为 right,内层为 left
  •   public String longestPalindrome(String s) {
          int n = s.length();
          boolean[][] dp = new boolean[n][n];
          int max = 0,start = 0;
          for(int j=0;j<n;j++){
              for(int i=0;i<=j;i++){
                  if(s.charAt(i)!=s.charAt(j))continue;
                  if(j-i<3)dp[i][j]=true;
                  else dp[i][j]=dp[i+1][j-1];
                  if(dp[i][j] && j-i+1>max){
                      max = j-i+1;
                      start=i;
                  }
              }
          }
          return s.substring(start, start+max);
      }
    
  • 长度为 1 的子串肯定是回文串,所以还可以根据这点稍微优化一下
  •   int max = 1,start = 0;
      for(int j=1;j<n;j++){
          for(int i=0;i<j;i++){
    
  • 马拉车算法(Manacher):回文串有奇数 aba 和偶数 abba 两种形式,使用马拉车算法,会在字符串的每两个字符间以及字符首尾加上一个特殊字符,这样最终字符串长度都会为奇数,同时,原本的每个回文串的长度也会都变成奇数,以上面两个字符串为例就可以证明
  • aba->*a*b*a*
  • abba->*a*b*b*a*
  • 其实就是奇数加偶数必定为奇数
  • 再引用一个变量回文半径,表示回文串左边或右边到中心点的长度,比如 *a*b*a* 回文半径为 *a*b 或者 b*a* 的长度也就是 4, *a*b*b*a* 回文半径为 5
  • 铺垫完毕,进入正题,中心扩散法每次换一个中心点,都要重新计算此时以 i 点为中心点的回文串长度,而马拉车在中心扩散法的基础上改进,可以利用之前的结果。
  • 因为遍历顺序是从左到右,所以我们取之前的回文串结果中右边界最大的一个,利用它推出当前以 i 为中心的回文串长度。我们就假设之前的那个结果中心点为 maxCenter,左右端点为 left 和 maxRight。我们根据 i 的位置划分出不同情况下怎么推出当前结果
    1. i < maxRight,那么以 maxCenter 为对称中心对称过去的点 j 肯定在 left 到 maxCenter,如果以 j 为中心点得到的回文串也在 left 到 maxCenter 之内,那根据对称的性质,i 对应的结果和 j 对应的一样,下图顺便说明了得到 j 点的计算公式,把 left 带入 0 为例子比较容易推导, p[] 为回文串半径数组
      请添加图片描述

    2. i < maxRight,但是以 j 为中心的回文串长度超过了 left,还是根据对称,我起码可以肯定我的回文串半径最小也等于 j - left ,也就是 maxRight - i,我们以此为基础用中心扩散法继续扩散即可
      请添加图片描述

    3. i > maxRight,那没办法了,老老实实从头开始中心扩散法吧请添加图片描述

  • i 为什么没有小于 maxCenter 的情况,因为 maxCenter 就是我们之前不断遍历得到的某个 i,新的 i 当然大于之前的 i 了
  •   int charLen = s.length();//源字符串的长度
      int length = charLen * 2 + 1;//添加特殊字符之后的长度
      char[] chars = s.toCharArray();//源字符串的字符数组
      char[] res = new char[length];//添加特殊字符的字符数组
      int index = 0;
      //添加特殊字符
      for (int i = 0; i < res.length; i++) {
          res[i] = (i % 2) == 0 ? '#' : chars[index++];
      }
    
      //新建p数组 ,p[i]表示以res[i]为中心的回文串半径
      int[] p = new int[length];
      //maxRight(某个回文串延伸到的最右边下标)
      //maxCenter(maxRight所属回文串中心下标),
      //resCenter(记录遍历过的最大回文串中心下标)
      //resLen(记录遍历过的最大回文半径)
      int maxRight = 0, maxCenter = 0, resCenter = 0, resLen = 0;
      //遍历字符数组res
      for (int i = 0; i < length; i++) {
          if (i < maxRight) {
              //情况一,i没有超出范围[left,maxRight]
              //2 * maxCenter - i其实就是j的位置,实际上是判断p[j]<maxRight - i
              // maxRight - i 其实就是 j-left,也就是说 j 的回文半径在 left ~ j
              if (p[2 * maxCenter - i] < maxRight - i) {
                  //j的回文半径没有超出范围[left,maxRight],直接让p[i]=p[j]即可
                  p[i] = p[2 * maxCenter - i];
              } else {
                  //情况二,j的回文半径已经超出了范围[left,maxRight],我们可以确定p[i]的最小值
                  //是maxRight - i,至于到底有多大,后面还需要在计算
                  // i - p[i] 表示以 i 为中心, p[i] 为半径的回文串的左端点,同理 i + p[i] 为右端点
                  // 半径不断增加就等于中心点不断向外扩散
                  p[i] = maxRight - i;
                  while (i - p[i] >= 0 && i + p[i] < length && res[i - p[i]] == res[i + p[i]])
                      p[i]++;
              }
          } else {
              //情况三,i超出了范围[left,maxRight],就没法利用之前的已知数据,而是要一个个判断了
              p[i] = 1;
              while (i - p[i] >= 0 && i + p[i] < length && res[i - p[i]] == res[i + p[i]])
                  p[i]++;
          }
          // 匹配完之后,如果右边界i + p[i]超过maxRight,那么就更新maxRight和maxCenter
          // 因为主要看 i 相对右边界来推导结果,所以能更新右边界就更新
          if (i + p[i] > maxRight) {
              maxRight = i + p[i];
              maxCenter = i;
          }
          //记录最长回文串的半径和中心位置
          if (p[i] > resLen) {
              resLen = p[i];
              resCenter = i;
          }
      }
      //计算最长回文串的长度和开始的位置
      resLen = resLen - 1;
      int start = (resCenter - resLen) >> 1;
      //截取最长回文子串
      return s.substring(start, start + resLen);
    
  • 如下图所示,有特殊字符的回文半径 - 1 其实就是原始回文串的长度,这也就是 resLen = resLen - 1; 的由来
    请添加图片描述
  • 上面三种情况还能合并,把他们都看成确定半径以及扩散这两步即可,情况 1 确定完半径以后不满足扩散条件所以不会扩散,情况 2 和 3 也就是初始半径不同,都会扩散
  •  int charLen = s.length();//源字符串的长度
     int length = charLen * 2 + 1;//添加特殊字符之后的长度
     char[] chars = s.toCharArray();//源字符串的字符数组
     char[] res = new char[length];//添加特殊字符的字符数组
     int index = 0;
     //添加特殊字符
     for (int i = 0; i < res.length; i++) {
         res[i] = (i % 2) == 0 ? '#' : chars[index++];
     }
    
     //新建p数组 ,p[i]表示以res[i]为中心的回文串半径
     int[] p = new int[length];
     //maxRight(某个回文串延伸到的最右边下标)
     //maxCenter(maxRight所属回文串中心下标),
     //resCenter(记录遍历过的最大回文串中心下标)
     //resLen(记录遍历过的最大回文半径)
     int maxRight = 0, maxCenter = 0, resCenter = 0, resLen = 0;
     //遍历字符数组res
     for (int i = 0; i < length; i++) {
         //合并后的代码
         p[i] = maxRight > i ? Math.min(maxRight - i, p[2 * maxCenter - i]) : 1;
         //上面的语句只能确定i~maxRight的回文情况,至于maxRight之后的部分是否对称,
         //就看是否需要一个个去匹配了,匹配的时候首先数组不能越界
         while (i - p[i] >= 0 && i + p[i] < length && res[i - p[i]] == res[i + p[i]])
             p[i]++;
         //匹配完之后,如果右边界i + p[i]超过maxRight,那么就更新maxRight和maxCenter
         if (i + p[i] > maxRight) {
             maxRight = i + p[i];
             maxCenter = i;
         }
         //记录最长回文串的半径和中心位置
         if (p[i] > resLen) {
             resLen = p[i];
             resCenter = i;
         }
     }
     //计算最长回文串的长度和开始的位置
     resLen = resLen - 1;
     int start = (resCenter - resLen) >> 1;
     //截取最长回文子串
     return s.substring(start, start + resLen);
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/263674.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue中为什么data属性是一个函数而不是一个对象?(看完就会了)

文章目录 一、实例和组件定义data的区别二、组件data定义函数与对象的区别三、原理分析四、结论 一、实例和组件定义data的区别 vue实例的时候定义data属性既可以是一个对象&#xff0c;也可以是一个函数 const app new Vue({el:"#app",// 对象格式data:{foo:&quo…

springboot学习笔记(五)

MybatisPlus进阶 1.MybatisPlus一对多查询 2.分页查询 1.MybatisPlus一对多查询 场景&#xff1a;我有一个表&#xff0c;里面填写的是用户的个人信息&#xff08;姓名&#xff0c;生日&#xff0c;密码&#xff0c;用户ID&#xff09;。我还有一个表填写的订单信息&#x…

Linux系统管理、服务器设置、安全、云数据中心

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 我们来快速了解liunx命令 文章目录 前言解析命令提示符linux的文件和目录文件和目录管理文件操作 进程管理命令系统管理网络管理 书籍推荐 本文以服务器最常用的CentOS为例 解析命令提示…

图片怎么转文字?这几个图片提取文字方法教会你!

在数字时代&#xff0c;我们每天都与大量的图片、文本信息打交道。当我们需要从图片中提取文字时&#xff0c;传统的方式可能是手动输入或者借助某些付费工具&#xff0c;今天介绍这三个工具不仅易于使用&#xff0c;而且效果卓越&#xff0c;我只需上传图片&#xff0c;工具便…

uniapp地图开发(APP,H5)

uniapp地图开发&#xff08;APP&#xff0c;H5&#xff09; 背景实现页面实现功能实现注意事项 尾巴 背景 最近项目中需要使用地图相关功能&#xff0c;需要用到聚合&#xff0c;marker拖拽&#xff0c;自定义marker显示内容&#xff0c;根据角色不同maker显示不同图标等功能。…

Nacos教程

常见的微服务架构&#xff1a; 1. dubbo: zookeeper dubbo SpringMVC/SpringBoot 配套 通信方式&#xff1a;rpc 注册中心&#xff1a;zookeeper / redis 2.SpringCloud &#xff1a; 全家桶 轻松嵌入第三方组件 (Netflix) 配套 通信方式&#xff1a;http restful 注册中心…

【MATLAB】史上最全的13种数据拟合算法全家桶

有意向获取代码&#xff0c;请转文末观看代码获取方式~ 1 【MATLAB】傅里叶级数拟合算法 傅里叶级数拟合算法是一种强大而灵活的数学方法&#xff0c;可以将复杂的函数拆解成多个简单的正弦和余弦函数的和。通过求解函数中的系数&#xff0c;我们可以用有限项傅里叶级数来拟合…

类和对象(下篇)

再谈构造函数 构造函数体赋值 在之前的学习中我们知道&#xff0c;在创建一个对象时&#xff0c;我们的编译器就会自动调用构造函数将对象初始化&#xff0c;给对象中各个成员变量一个合适的初始值。 例如&#xff1a; class Date { public:Date(int year, int month, int d…

Java文件流大家族(通俗易懂,学习推荐版,很详细)——操作文件本身和文件中的数据

1.File&#xff08;操作文件本身&#xff09; 1.定义 目录 2.常用方法 3.路径引用符 可以用/或者\\分隔路径 还可以用File.separator分隔路径&#xff0c;会根据不同系统使用啥分隔符。 4.绝对路径、相对路径及桌面路径表示 桌面路径为&#xff1a; 我电脑的用户名为X 5.示例…

服务器数据恢复-误操作导致xfs分区数据丢失的数据恢复案例

服务器数据恢复环境&#xff1a; 某品牌OceanStorT系列某型号存储MD1200磁盘柜&#xff0c;组建的raid5磁盘阵列。上层分配了1个lun&#xff0c;安装的linux操作系统&#xff0c;划分两个分区&#xff0c;分区一通过lvm进行扩容&#xff0c;分区二格式化为xfs文件系统。 服务器…

初级数据结构(七)——二叉树

文中代码源文件已上传&#xff1a;数据结构源码 <-上一篇 初级数据结构&#xff08;六&#xff09;——堆 | NULL 下一篇-> 1、写在前面 二叉树的基本概念在《初级数据结构&#xff08;五&#xff09;——树和二叉树的概念》中已经介绍得足够详细了。上一…

海康威视对讲广播系统 RCE漏洞复现(CVE-2023-6895)

0x01 产品简介 Hikvision Intercom Broadcasting System是中国海康威视(Hikvision)公司的一个对讲广播系统。 0x02 漏洞概述 Hikvision Intercom Broadcasting System 3.0.3_20201113_RELEASE(HIK)版本存在操作系统命令注入漏洞,该漏洞源于文件/php/ping.php的参数jsonda…

虾皮跨境电商物流:打造高效便捷的全球供应链解决方案

随着全球化的推进和电子商务的蓬勃发展&#xff0c;跨境电商物流成为了越来越多商家和消费者关注的焦点。虾皮&#xff08;Shopee&#xff09;作为一家领先的电商平台&#xff0c;不仅提供了丰富多样的商品选择&#xff0c;还致力于为卖家和消费者提供高效便捷的跨境电商物流服…

conda环境下执行conda命令提示无法识别解决方案

1 问题描述 win10环境命令行执行conda命令&#xff0c;报命令无法识别&#xff0c;错误信息如下&#xff1a; PS D:\code\cv> conda activate pt conda : 无法将“conda”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果包括路径&a…

SpringIOC之LocaleContext

博主介绍:✌全网粉丝5W+,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验✌ 博主作品:《Java项目案例》主要基于SpringBoot+MyBatis/MyBatis-plus+…

使用Mosquitto/python3进行MQTT连接

一、简介 MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上&#xff0c;是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议&#xff0c;为此&#xff0c;它需要一个消息中间件。 …

用BEVformer来卷自动驾驶-1

之所以是-1,是因为大概率1篇文章写不完,但是又不知道应该用几篇来说事,先写着看 按照惯例,上论文地址:2203.17270v1.pdf (arxiv.org) 什么是BEV, Birds -Eye-View的意思,就是鸟瞰 比如稍微传统一些的自动驾驶,大部分的实现。如果靠纯CV的方案的话,那么基本…

P73 bert奇闻

同一个字&#xff0c;前后接的不同&#xff0c;词汇的意思不同&#xff0c;通过bert 之后输出的向量也不一样。 bert 输出后的向量包含上下文的信息。 比如 吃苹果 和苹果电脑中的 果&#xff0c;向量不一样。 DNA 分类 把DNA 的 A T C G 用 we you he she 表示&#xff0c;然…

构建现代企业培训系统的技术实践

在当今竞争激烈的商业环境中&#xff0c;企业培训系统成为提高员工技能、促进组织发展的关键组成部分。本文将深入探讨构建现代企业培训系统的关键技术实践&#xff0c;旨在帮助企业更好地满足学员需求、提高培训效果。 1. 系统架构设计 现代企业培训系统的成功建设始于一个…

Java版企业电子招投标系统源代码,支持二次开发,采用Spring cloud微服务架构

招投标管理系统是一个集门户管理、立项管理、采购项目管理、采购公告管理、考核管理、报表管理、评审管理、企业管理、采购管理和系统管理于一体的综合性应用平台。它适用于招标代理、政府采购、企业采购和工程交易等业务的企业&#xff0c;旨在提高项目管理的效率和质量。该系…