【动态规划】回文子串专题

news2024/11/15 16:45:05

文章目录

  • 1. 【LeetCode】647. 回文子串
    • 1.1 思路讲解
      • 1.1.1 方法选择
      • 1.1.2 dp表的创建
      • 1.1.3 状态转移方程
      • 1.1.4 填表顺序
    • 1.2 整体代码
  • 2. 【LeetCode】5. 最长回文串
    • 2.1 思路讲解
    • 2.2 代码实现
  • 3.【LeetCode】094. 分割回文串II
    • 3.1 解题思路
      • 3.1.1 创建dp表
      • 3.1.2 状态转移方程
      • 3.1.3 提前求出所有子串是否是回文串
    • 3.2 整体代码
  • 4.【LeetCode】516. 最长回文子序列
    • 4.1 思路讲解
      • 4.1.1 创建dp表
      • 4.1.2 状态转移方程
      • 4.1.3 不需考虑边界问题
    • 4.2 整体代码
  • 5. 【LeetCode】1312. 让字符串成为回文串的最少插入次数
    • 5.1 思路讲解
      • 5.1.1 创建dp表
      • 5.1.2 状态转移方程
      • 5.1.3 不需考虑边界问题
    • 5.2 整体代码

1. 【LeetCode】647. 回文子串

题目链接

1.1 思路讲解

1.1.1 方法选择

这道题我们采用动态规划的解法,倒不是动态规划的解法对于这道题有多好,它并不是最优解。但是,这道题的动态规划思想是非常有用的,我们使用这道题的动态规划思想,可以让一些hard题变为easy题。

也就是说,这道题的动态规划思想其实就是起到了一个抛砖引玉的作用。

1.1.2 dp表的创建

如何表示出所有的子串的情况?可以用 i 表示某个子串的起始位置,用 j 来表示某个子串的末尾位置,暴力枚举,可以在N^2的时间复杂度内求出所有子串是否为回文子串。

所以,我们用二维dp[i][j]表来表示,以 i 位置为起始位置且以 j 位置为结尾的子串是否为回文子串。如果为回文子串那么dp[i][j]为true,否则为false。(我们人为规定 i <= j)

1.1.3 状态转移方程

我们要知道dp[i][j]为是否为回文子串,首先要判断 s[i] 是否等于 s[j]。

如果 s[i] != s[j],那么不管 i 和 j 中间的元素序列是怎样的,以 i 位置为起始位置,以 j 位置为终止位置的子串一定不为回文子串

如果 s[i] == s[j],那么需要对 i 和 j 的位置进行判断。

  1. 如果 i == j,那么说明当前初识位置和末尾位置在同一个位置,也就是说,子串只有一个元素,此时根据题意它为回文子串
  2. 如果 i + 1 == j,那么 i 和 j 的位置是相邻的,此时它们中间没有元素,它们位置上的元素又相同,那么一定是回文子串
  3. 如果 i + 1 < j,说明 i 位置 和 j 位置中间还有其他元素,此时只需判断dp[i+1][j-1]为true还是false即可

1.1.4 填表顺序

由于我们求dp[i][j]的时候,需要用到 dp[i+1][j-1],且 i 的循环为外层的循环,所以让 i 从大到小循环即可。

1.2 整体代码

在这里插入图片描述

class Solution {
public:
    int countSubstrings(string s) {
        int n = s.size();
        // 创建二维dp表,dp表中每个位置的初始值为false
        vector<vector<bool>> dp(n, vector<bool>(n));
        
        int ret = 0; // 用于保存有多少位true的dp位置,即有多少个回文子串
        // 在循环时 i 从大到小进行循环
        for (int i = n - 1; i >= 0; --i)
        {
            // j的循环顺序其实无所谓,只要循环的区间在[i, n)即可
            for (int j = i; j < n; ++j)
            {
                // 根据状态转移方程求dp[i][j]
                if (s[i] == s[j])
                    dp[i][j] = i + 1 < j ? dp[i+1][j-1] : true;
                // 如果dp[i][j]为true,增加ret
                if (dp[i][j]) ++ret;
            }
        }
        return ret;
    }
};

2. 【LeetCode】5. 最长回文串

题目链接

2.1 思路讲解

与求回文子串思路差别不大

它也就是求出每一个回文子串后,不是统计有多少个回文子串,而是挑出最长的那个回文子串并在循环结束之后返回即可。

代码也只不过改动了一点点而已。

2.2 代码实现

在这里插入图片描述

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        vector<vector<bool>> dp(n, vector<bool>(n));
        int start = 0; // 最长的回文子串的起始位置
        int len = 0; // 最长的回文子串的长度
        for (int i = n - 1; i >= 0; --i)
        {
            for (int j = i; j < n; ++j)
            {
                if (s[i] == s[j])
                    dp[i][j] = i + 1 < j ? dp[i+1][j-1] : true;
                // 如果该位置为回文子串,那么判断长度是否大于之前最长的长度
                // 如果大于,则对起始位置和最长长度进行更新
                if (dp[i][j] == true && j - i + 1 > len)
                {
                    len = j - i + 1;
                    start = i;
                }
            }
        }
        // 根据起始位置和长度返回最长回文子串
        return s.substr(start, len);
    }
};

3.【LeetCode】094. 分割回文串II

3.1 解题思路

3.1.1 创建dp表

这道题我们使用动态规划的方法来解,首先创建一个大小为字符串长度的dp表。dp[i] 表示 s[0, i] 的字符串最小划分多少次可以全划分为回文串。

3.1.2 状态转移方程

求状态转移方程,我们要考虑两种情况。s[0, i] 的字符串是回文串和不是回文串的情况。

注意,这里假设我们已经知道了哪段字符串是不是回文串,至于是如何知道的后面会说。

  • 如果s[0, i]是回文串,那么问题很简单,不用切割就行,即dp[i] = 0;
  • 如果s[0, i]不是回文串,我们要新增一个变量 j ,j 的范围为 (0, i],这里说明一些j的边界情况,j 要大于0的原因是 j 为0的情况即不用分割s[0, i]的情况(即s[0, i]为回文串的情况),j 为 i 的情况即 s[0, i-1] 中找不到从0开始且为回文串的情况。用这个 j 变量,我们遍历 j 的情况,j 是小于等于 i 的,那么 dp[j-1] 的值我们是知道的。如果从 j 到 i 的字符串是回文串,那么我们就令 dp[i] = min(dp[i], dp[j - 1] + 1); 遍历所有 j 的情况,就能求出 dp[i] 的最小值了。

在这里插入图片描述

3.1.3 提前求出所有子串是否是回文串

这个通过上面的题解也就能知道了。

3.2 整体代码

在这里插入图片描述

class Solution {
public:
    int minCut(string s) {
        int n = s.size();
        // 求出所有子串是否为回文串
        vector<vector<bool>> isPal(n, vector<bool>(n));
        for (int i = n - 1; i >= 0; --i)
            for (int j = i; j < n; ++j)
                if (s[i] == s[j]) 
                    isPal[i][j] = i + 1 < j ? isPal[i+1][j-1] : true;
        
        // 创建dp表,由于是求最小值,可以先将所有位置初始化为最大
        vector<int> dp(n, INT_MAX);  
        for (int i = 0; i < n; ++i)
        {
            if (isPal[0][i]) dp[i] = 0;
            else
            {
                for (int j = 1; j <= i; ++j)
                    if (isPal[j][i]) dp[i] = min(dp[i], dp[j-1] + 1);
            }
        }
        return dp[n-1];
    }
};

4.【LeetCode】516. 最长回文子序列

4.1 思路讲解

4.1.1 创建dp表

此题采用动态规划的方法,创建一个二维dp表,dp[i][j]表示s[i, j]中最大回文子序列的长度。且我们人为规定 i 是一定小于等于 j 的。

4.1.2 状态转移方程

在求dp[i][j]时,首先要判断s[i]和s[j]是否相同。

如果 s[i] == s[j]

  1. 如果 i == j,说明 i 与 j 的位置相同,此时dp[i][j] 就为 1
  2. 如果 i + 1 == j,说明 i 与 j 相邻,此时dp[i][j] 就为2
  3. 其他情况下,说明 i 和 j 中间有其他元素,那么此时dp[i][j] = dp[i+1][j-1] + 2;

如果s[i] != s[j]

那么此时,说明不能同时以 i 为开头和以 j 为结尾,我们去掉这种情况寻找一个最大子序列即可。方法就是在 dp[i+1, j] 和 dp[i, j-1] 中选一个最大的即可。即dp[i][j] = max(dp[i+1[j], dp[i][j-1]);

4.1.3 不需考虑边界问题

在求dp[i][j]的时候,我们可能会用到 i + 1 和 j - 1,在它们有可能越界的时候,一定是 i 等于 j 的时候。我们创建的dp表是二维的,我们可以想到,在可能越界的时候,就是左上角的位置或者右下角的位置,但其实这两个位置满足 i == j,那么dp[i][j] 就会被直接赋值为1,此时就不会用到 i + 1 和 j - 1 了,所以其实我们不用考虑越界的情况。

4.2 整体代码

在这里插入图片描述

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        // 创建二维dp表,dp[i][j]表示s[i, j]最大子序列的长度
        vector<vector<int>> dp(n, vector<int>(n));
        // dp[i][j]需要用到dp[i+1][j-1]
        // 所以i从大到小循环,j从小到大循环,且i是小于等于j的
        for (int j = 0; j < n; ++j)
        {
            for (int i = j; i >= 0; --i)
            {
                if (s[i] == s[j])
                {
                    if (i == j) dp[i][j] = 1;
                    else if (i + 1 == j) dp[i][j] = 2;
                    else dp[i][j] = dp[i+1][j-1] + 2;
                }
                else dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
            }
        }
        return dp[0][n-1];
    }
};

5. 【LeetCode】1312. 让字符串成为回文串的最少插入次数

题目链接

5.1 思路讲解

5.1.1 创建dp表

采用动态规划的解法,可以借鉴以上题的思路。创建二维dp表,dp[i][j]表示 i 到 j 位置变成回文串的最小操作次数。(我们人为规定 i 是小于等于 j 的)

5.1.2 状态转移方程

同样我们要分 s[i] == s[j] 和 s[i] != s[j] 的情况讨论。

如果 s[i] == s[j]

  1. 如果 i == j,那么此时 i 与 j 的位置相同,一个字符本身就是回文串,dp[i][j] = 0
  2. 如果 i + 1 == j,那么此时 i 与 j 位置相同,此时这两个字符同样也是回文串,dp[i][j] = 0
  3. 如果 i + 1 < j ,那么此时 i 与 j 中间有其他字符,dp[i+1][j-1]为 i 和 j 中间字符串要变为回文串的最小操作次数,因为 s[i] == s[j],所以此时加上 i 和 j 位置的字符之后, dp[i][j] = dp[i+1][j-1]。

如果 s[i] != s[j]

i 和 j 位置的字符不相同,那么此时只需找到 dp[i+1][j] 和 dp[i][j-1] 的最小值,然后将最小的那个加1即可,即dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1; 就相当于再在另一端补上一个与 s[i] 或者 s[j] 相同的字符即可。

在这里插入图片描述

5.1.3 不需考虑边界问题

这道题和第四题一样,都是需要考虑越界的,具体原因和第四题相同。

在求dp[i][j]的时候,我们可能会用到 i + 1 和 j - 1,在它们有可能越界的时候,一定是 i 等于 j 的时候。我们创建的dp表是二维的,我们可以想到,在可能越界的时候,就是左上角的位置或者右下角的位置,但其实这两个位置满足 i == j,那么dp[i][j] 就会被直接赋值为0,此时就不会用到 i + 1 和 j - 1 了,所以其实我们不用考虑越界的情况。

5.2 整体代码

在这里插入图片描述

class Solution {
public:
    int minInsertions(string s) {
        int n = s.size();
        // dp[i][j]表示s[i, j]变成回文串的最小操作次数
        vector<vector<int>> dp(n, vector<int>(n, INT_MAX));
        // dp[i][j] 需要用到 dp[i+1][j-1]
        // 所以i应该从大到小遍历,j应该从小到大遍历
        // 且i是要小于等于j的,所以i的初始值为j
        // 因为i的初始值为j,所以j在循环外层,i在内层
        for (int j = 0; j < n; ++j)
        {
            for (int i = j; i >= 0; --i)
            {
                if (s[i] == s[j])
                    dp[i][j] = i + 1 < j ? dp[i+1][j-1] : 0;
                else
                    dp[i][j] = min(dp[i][j-1], dp[i+1][j]) + 1;
            }
        }
        return dp[0][n-1];
    }
};

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[每日习题]第一个只出现一次的字符 小易的升级之路——牛客习题

hello,大家好&#xff0c;这里是bang___bang_&#xff0c;本篇记录2道牛客习题&#xff0c;第一个只出现一次的字符&#xff08;简单&#xff09;&#xff0c;小易的升级之路&#xff08;简单&#xff09;&#xff0c;如有需要&#xff0c;希望能有所帮助&#xff01; 目录 1️…

继承(Inheritance)

Odoo的一个强大方面是它的模块化。模块专用于业务需求&#xff0c;但模块也可以相互交互。这对于扩展现有模块的功能非常有用。例如&#xff0c;在我们的房地产场景中&#xff0c;我们希望在常规用户视图中直接显示销售人员的财产列表。 在介绍特定的Odoo模块继承之前&#xf…

vue diff 双端比较算法

文章目录 双端指针比较策略命中策略四命中策略二命中策略三命中策略一未命中四种策略&#xff0c;遍历旧节点列表新增情况一新增情况二 删除节点双端比较的优势 双端指针 使用四个变量 oldStartIdx、oldEndIdx、newStartIdx 以及 newEndIdx 分别存储旧 children 和新 children …

26 MFC序列化函数

文章目录 Serialize对于存储文件的序列化 Serialize Serialize 是一个在 MFC (Microsoft Foundation Classes) 中常用的函数或概念。它用于将对象的数据进行序列化和反序列化&#xff0c;便于在不同的场景中保存、传输和恢复对象的状态。 在 MFC 中&#xff0c;Serialize 函数…

如何查询显卡算力

感谢阅读 找到demo_suite此目录下打开控制台 找到demo_suite 一般在 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.6\extras\demo_suite 这个目录 注意&#xff1a; 1.V后面的数字表示版本&#xff0c;请根据自己的版本进行更改 2.此目录为我的安装目录&#xff0…

python批量检查folder中的文件是否符合要求

目录 问题描述&#xff1a; 问题解决&#xff1a; 问题描述&#xff1a; 最近在手动整理一些文档&#xff0c;要求是每一个folder以ID命名&#xff0c;每一个folder中存放三个内容&#xff08;如下图&#xff09;。如何实现批量检查每一个folder三个内容是否存在&#xff1f;…

【2023年电赛国一必备】E题报告模板--可直接使用

创作不易&#xff0c;麻烦关注CSDN【技术交流、免费报告资料】 通过百度网盘分享的文件&#xff1a;https://pan.baidu.com/s/1aXzYwLMLx_b59abvplUiYw?pwddn71 提取码:dn71 复制这段内容打开「百度网盘APP 即可获取」 任务 图1 任务内容 要求 图2 基本要求内容 图3 发挥部…

K8s实战入门(三)

文章目录 3. 实战入门3.1 Namespace3.1.1 测试两个不同的名称空间之间的 Pod 是否连通性 3.2 Pod3.3 Label3.4 Deployment3.5 Service 3. 实战入门 本章节将介绍如何在kubernetes集群中部署一个nginx服务&#xff0c;并且能够对其进行访问。 3.1 Namespace Namespace是kuber…

【电源专题】充电IC与DC-DC有什么区别

充电IC和DC-DC一样使用很广泛,如手机、平板等需要电池供电的系统中,一般都会见到充电IC的身影。那么大家有没有考虑过一个问题。充电IC与DC-DC有什么区别? 首先如下所示为充电IC的两个阶段,一个阶段是恒流充电阶段,我们一般称之为CC阶段,另一个是恒压充电阶段,我们称之为…

Java课题笔记~ IoC 控制反转

二、IoC 控制反转 控制反转&#xff08;IoC&#xff0c;Inversion of Control&#xff09;&#xff0c;是一个概念&#xff0c;是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器&#xff0c;通过容器来实现对象的 装配和管理。控制反转就是对对象控制权的转移&a…

ShaderToy着色器移植到Three.js全过程记录

推荐&#xff1a;用 NSDT设计器 快速搭建可编程3D场景。 作为 Publicis Pixelpark Innovationlab 研究的一部分&#xff0c;我们研究了如何将较低底层的语言用于网络技术。 显而易见的选择似乎是 asm.js 或 WebAssembly。 但你也可以使用 WebGL 着色器来解决面向机器的问题。 …

springboot+vue网红酒店客房预定系统的设计与实现_ui9bt

随着计算机技术发展&#xff0c;计算机系统的应用已延伸到社会的各个领域&#xff0c;大量基于网络的广泛应用给生活带来了十分的便利。所以把网红酒店预定管理与现在网络相结合&#xff0c;利用计算机搭建网红酒店预定系统&#xff0c;实现网红酒店预定的信息化。则对于进一步…

【验证测试】未初始化的全局变量和局部变量的初值

验证目标&#xff1a; 未初始化的全局变量的初值为 0未初始化的局部变量的初值为随机值 测试用例&#xff1a; #include <stdio.h>char gval1; int gval2; static long gval3;int main() {unsigned char uchTmp1;unsigned int uTmp2;printf("%d\n", gval1)…

Word中如何断开表格中线段

Word中如何断开表格中线段_word表格断线怎么弄_仰望星空_LiDAR的博客-CSDN博客有时候为了美观&#xff0c;需要实现如下的效果&#xff0c;即第2条线段被断开成3段步骤如下&#xff1a;选中需要断开的格网&#xff0c;如下&#xff0c;再选择段落、针对下框标即可。_word表格断…

Verilog学习记录-自用

always语句块一定条件写完整&#xff0c;否则电平触发&#xff0c;综合生成锁存器 task不可综合&#xff0c;主要用于仿真/验证 大部分都是并行执行的&#xff0c;只有begin end块中阻塞语句是串行 if-else和case的区别 if-else面积小&#xff0c;但时延&#xff08;执…

Service not registered 异常导致手机重启分析

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 一、Service not registered 异常导致手机重启二、Service not registered 解决方案 一、Service not registered 异常导致手机重启 1.重启 的部分Log如…

C++封装思想之二:友元机制和运算符重载(1W字详解)

目录 友元机制和运算符重载 友元机制 友元函数 友元的作用 友元类 前置声明 友元类的注意事项 友元成员函数&#xff08;类的某个成员函数 作为另一个类的友元&#xff09; 运算符重载 运算符重载的作用 运算符重载的注意事项 运算符重载的实现 成员函数重载 友…

2021-03-03 Multisim 14.0 电池充电防止反接保护

R2R3当作充电线电阻看,也可设置这2个电阻导线电阻,电阻取值依据充电电流范围确定,由于电池存在电压因此可以用光耦检测,发光二极管当作继电器看,可采用继电器自锁,当下次再次反接的话另一个继电器同样,2个继电器相互控制.本电路可验证极性变化时2路检测的变化,图中S1为模拟电池…

计算机视觉:替换万物Inpaint Anything

目录 1 Inpaint Anything介绍 1.1 为什么我们需要Inpaint Anything 1.2 Inpaint Anything工作原理 1.3 Inpaint Anything的功能是什么 1.4 Segment Anything模型&#xff08;SAM&#xff09; 1.5 Inpaint Anything 1.5.1 移除任何物体 1.5.2 填充任意内容 1.5.3 替换任…

Finalshell连接Linux超时之Connection timed out: connect

目录 &#x1f349;前言 &#x1f33c;报错 &#x1f33c;摸索 &#x1f4aa;解决措施 &#x1f349;前言 &#xff08;1&#xff09;福利&#xff1a;花了2小时才解决的BUG&#xff0c;希望本篇文章能帮你10分钟解决&#xff01; &#xff08;2&#xff09;tips&#xff1…