【动态规划篇】:当回文串遇上动态规划--如何用二维DP“折叠”字符串?

news2025/4/3 23:42:51

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:动态规划篇–CSDN博客

在这里插入图片描述

文章目录

  • 一.回文串类DP
    • 核心思想(判断所有子串是否是回文子串)
  • 二.例题
    • 1.回文子串
    • 2.最长回文子串
    • 3.分割回文串4
    • 4.分割回文串2
    • 5.最长回文子序列
    • 6.让字符串成为回文串的最少插入次数

一.回文串类DP

核心思想(判断所有子串是否是回文子串)

1.状态表示:

定义二维数组dp[i][j],表示字符串s区间[i,j]的子串是否是回文串,其中i位置的字符是子串的起始位置,j位置的字符是子串的结束位置,存储每一个子串的信息,是dp[i][j]true,不是为false

2.状态转移方程:

  • 如果两端的字符s[i]==s[j],分为三种情况:
    • 如果子串长度为1,则下标i==j,说明字串是一个单独的字符,则一定是回文子串,dp[i][j]=true
    • 如果子串长度为2,则下标i+1=j,说明子串是两个相邻的字符,并且前提条件两个字符相同,所以也一定是回文子串,dp[i][j]=true
    • 如果子串长度大于2,则下标j-i>2,需要检查内部区间[i+1,j-1]部分的子串是否是回文子串,如果是并且两端的字符也相等,所以区间[i,j]部分的字串也是回文子串,dp[i][j]=true;如果不是,那区间[i,j]部分的字串也就一定不是回文子串,dp[i][j]=false
  • 如果两端的字符s[i]!=s[j],则区间[i,j]的子串一定不是回文子串,dp[i][j]=false

3.初始化:

单个字符一定是回文子串,所以初始值可以设置为true。

4.填表顺序:

根据状态转移方程来决定,当前状态dp[i][j]需要用到前一个状态dp[i+1][j-1],在二维数组中,位于[i,j]位置的左下角,所以填表时,需要从最后一行到第一行,其中每一行按照从左往右的顺序。

因为下标i<=j,所以二维数组中,只需填表斜对角线的上半部分即可。

5.返回值:

需要根据题意来决定。

注意:上面讲解的是关于如何判断所有子串是否是回文子串,并不是每一道的解决步骤都是这样,具体解决方式需要根据题意来决定,但所有的回文串类DP都是在此基础上变形,所以该思想对于解决回文串类DP非常重要。

二.例题

1.回文子串

题目

在这里插入图片描述

算法原理

题意要求统计所有的回文子串个数,所以可以用二维数组状态表dp[i][j]存储所有子串是否是回文子串的信息,是为true,不是为false。所以最后填完状态表后只需遍历一遍,统计true的个数即可。

代码实现

int countSubstrings(string s){
    int n = s.size();

    //状态表示 dp[i][j]表示以i位置字符为开头,j位置字符为结尾的子串是否是回文子串
    //状态表中会存储每个子串是否是回文子串的信息
    vector<vector<bool>> dp(n, vector<bool>(n));

    int ret = 0;
    //填表顺序 从最后一行到第一行 因为当前状态值需要用到左下角的状态值
    for (int i = n - 1; i >= 0; i--){
        for (int j = i; j < n; j++){
            if(s[i]==s[j]){
                if (i == j || i + 1 == j || dp[i + 1][j - 1] == true){
                    dp[i][j] = true;
                }
            }

            //如果当前子串是回文子串,个数加一
            if (dp[i][j] == true){
                ret += 1;
            }
        }
    }

    //返回值
    return ret;
}

2.最长回文子串

题目

在这里插入图片描述

算法原理

题意要求找到最长回文子串并返回,所以在次之前依然需要找到所有的回文子串才能找到最长长度的那个,还是用二维数组状态表dp[i][j]存储所有子串的信息,在填表的时候,如果当前区间的子串是回文子串,就判断是否更新最长长度,注意还要保留最长回文子串的起始位置。

代码实现

string longestPalindrome(string s){
    int n = s.size();

    //状态表示 dp[i][j]表示以i位置字符为开头,j位置字符为结尾的子串是否是回文子串
    //状态表中会存储每个子串是否是回文子串的信息
    vector<vector<int>> dp(n, vector<int>(n));

    int maxlen = 0;
    int begin = 0;
    //填表
    for (int i = n - 1; i >= 0; i--){
        for (int j = i; j < n; j++){
            if(s[i]==s[j]){
                if (i == j || i + 1 == j || dp[i + 1][j - 1] == true){
                    dp[i][j] = true;
                }
            }

            if (dp[i][j] == true){
                //更新最长的长度和开头下标
                maxlen = max(maxlen, j - i + 1);
                if (maxlen == j - i + 1){
                    begin = i;
                }
            }
        }
    }

    //返回值
    return s.substr(begin, maxlen);
}

3.分割回文串4

题目

在这里插入图片描述

算法原理

根据题意要求,需要将原字符串分割成三个回文字符串,所以依然需要知道所有的回文子串,还是用二维数组状态表dp[i][j]存储所有子串的信息。

假设原字符串分成三个区间的字符串,[0,i-1][i,j][j+1,n-1]区间,并且i>=1,j<=n-2(因为[0,i-1]区间和[j+1,n-1]区间的字符串至少长度为1),如果存在[i,j]区间的字符串是回文串,并且[0,i-1]区间和[j+1,n-1]的字符串也是回文串,就能分成三个,反之则不存在。

代码实现

bool checkPartitioning(string s){
    int n = s.size();

    //状态表示
    vector<vector<bool>> dp(n, vector<bool>(n));

    //填表
    for (int i = n - 1; i >= 0; i--){
        for (int j = i; j < n; j++){
            if (s[i] == s[j]){
                if (i == j || i + 1 == j || dp[i + 1][j - 1] == true){
                    dp[i][j] = true;
                }
            }
        }
    }

    //返回值 分割成三个回文子串 [0,i-1],[i,j],[j+1,n-1]
    for (int i = 1; i < n - 1; i++){
        for (int j = i; j < n - 1; j++){
            if (dp[0][i - 1] == true && dp[i][j] == true && dp[j + 1][n - 1] == true){
                return true;
            }
        }
    }
    return false;
}

4.分割回文串2

题目

在这里插入图片描述

算法原理

根据题意还是需要先知道所有子串是否是回文子串,所以先预处理状态表dp[i][j],找到所有区间的回文子串。

定义一个一维数组min_cut[i]状态表,

状态表示:[0,i]区间内的字符串,分割成回文子串最小的分割次数。

状态转移方程:分为两种情况,

如果[0,i]区间内的字符串已经是回文子串,最小分割次数就为0,min_cut[i]=0

如果[0,i]区间内的字符串不是回文子串,用下标j遍历区间[0,i],如果区间[j,i]字符串是回文子串,判断区间[0,j-1]的字符串分割成回文子串的最小分割次数,也就是找到min_cut[j-1]的最小值然后加一。

最后返回值就是区间[0,n-1]的字符串分割成回文子串的最小分割数,也就是min_cut[n-1]

代码实现

int minCut(string s){
    int n = s.size();

    //先获取每个子串是否是回文子串的信息,存放到二维状态表中
    vector<vector<bool>> dp(n, vector<bool>(n));
    for (int i = n - 1; i >= 0; i--){
        for (int j = i; j < n; j++){
            if (s[i] == s[j]){
                if (i == j || i + 1 == j || dp[i + 1][j - 1] == true){
                    dp[i][j] = true;
                }
            }
        }
    }

    //状态表示 min_cut[i]表示[0,i]区间内的子串,分割成回文子串最小的分割次数
    //初始化 因为要去前状态中的最小值,所以状态表中全部先初始化为最大值
    vector<int> min_cut(n, INT_MAX);

    //填表
    for (int i = 0; i < n; i++){
        if (dp[0][i] == true){
            min_cut[i] = 0;
        }
        else{
            for (int j = i; j > 0; j--){
                if (dp[j][i] == true){
                    //状态转移方程
                    min_cut[i] = min(min_cut[j - 1] + 1, min_cut[i]);
                }
            }
        }
    }

    //返回值
    return min_cut[n - 1];
}

5.最长回文子序列

题目

在这里插入图片描述

算法原理

本道题有点不同,需要找到的是回文子序列,关键点:回文子序列是允许字符不连续的,因此需要灵活利用状态转移。

状态表示:二维数组dp[i][j],表示s字符串[i,j]区间内,最长回文子序列的长度。

状态转移方程:分为两种情况,

如果两端的字符s[i]==s[j]

如果下标i==j,说明单独的一个字符表示回文子序列,不存在区间[i+1,j-1],长度直接为1。

如果下标i+1==j,说明两端的字符表示回文子序列,不存在区间[i+1,j-1],长度直接为2。

如果下标j-i>2,可以将这两个字符加入回文子序列的两端,因此找到区间[i+1,j-1]内的最长回文子序列长度然后加2,dp[i][j]=dp[i+1][j-1]+2

如果两度的字符s[i]!=s[j]

无法同时包含这两个字符,需要舍弃左端或者右端的字符,然后找剩余区间中的最长回文子序列长度,dp[i][j]=max(dp[i+1][j],dp[i][j-1])

填表顺序:计算当前状态dp[i][j]的值,需要先知道前三个状态的值,dp[i+1][j-1],dp[i+1][j],dp[i][j-1],在二维数组中分别位于当前位置的左下角,左侧和下侧。因此填表时需要从最后一行到第一行,其中每一行从左往右。

代码实现

int longestPalindromeSubseq(string s){
    int n = s.size();

    //状态表示 dp[i][j]表示[i,j]区间内,最长回文子序列的长度
    vector<vector<int>> dp(n, vector<int>(n));

    //填表 从上往下,其中每一行从左往右
    for (int i = n - 1; i >= 0; i--){
        for (int j = i; j < n; j++){
            //如果当前两个位置的字符相等,找区间内的最长回文子序列的长度
            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;
                }
            }
            //如果当前两个位置的字符不相等,找[i+1,j]和[i,j-1]两个区间内的最长回文子序列的长度
            else{
                dp[i][j] = max(dp[i][j - 1], dp[i + 1][j]);
            }
        }
    }

    //返回值
    return dp[0][n - 1];
}

6.让字符串成为回文串的最少插入次数

题目

在这里插入图片描述

算法原理

状态表示:二维数组dp[i][j],表示s字符串[i,j]区间内字符串成为回文串的最少插入次数

状态转移方程:分为两种情况,

如果两端的字符s[i]==s[j]

如果下标i==j,说明单独的一个字符表示长度为1的回文串,不用再插入字符,次数直接为0。

如果下标i+1==j,说明两端的字符表示长度为2的回文串,不用再插入字符,长次数还是为0。

如果下标j-i>2,可以将这两个字符直接加入回文串的两端,因此找到区间[i+1,j-1]内的回文子串最少插入次数,dp[i][j]=dp[i+1][j-1]

如果两度的字符s[i]!=s[j]

无法同时包含这两个字符,需要舍弃左端的字符然后在左侧插入一个右端的字符或者舍弃右端的字符然后在右侧插入一个左端的字符,然后找剩余区间中的回文子串最少插入次数,dp[i][j]=max(dp[i+1][j]+1,dp[i][j-1]+1)

填表顺序:计算当前状态dp[i][j]的值,需要先知道前三个状态的值,dp[i+1][j-1],dp[i+1][j],dp[i][j-1],在二维数组中分别位于当前位置的左下角,左侧和下侧。因此填表时需要从最后一行到第一行,其中每一行从左往右。

代码实现

int minInsertions(string s){
    int n = s.size();

    //状态表示 dp[i][j]表示[i,j]区间内字符串成为回文串的最少插入次数
    vector<vector<int>> dp(n, vector<int>(n));

    //填表 从最后一行到第一行,其中每一行从左往右
    for (int i = n - 1; i >= 0; i--){
        for (int j = i; j < n; j++){
            if (s[i] == s[j]){
                if (i == j || i + 1 == j){
                    dp[i][j] = 0;
                }
                else{
                    dp[i][j] = dp[i + 1][j - 1];
                }
            }
            else{
                dp[i][j] = min(dp[i][j - 1] + 1, dp[i + 1][j] + 1);
            }
        }
    }

    //返回值
    return dp[0][n - 1];
}

以上就是关于回文串类DP例题的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
在这里插入图片描述

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

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

相关文章

Windows 安装 GDAL 并配置 Rust-GDAL 开发环境-1

Rust-GDAL 是 Rust 语言的 GDAL&#xff08;Geospatial Data Abstraction Library&#xff09; 绑定库&#xff0c;用于处理地理数据。由于 GDAL 依赖较多&#xff0c;在 Windows 上的安装相对复杂&#xff0c;本文档将介绍如何安装 GDAL 并配置 Rust-GDAL 的开发环境。 1. 检…

第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式

第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式 解决按键扫描&#xff0c;松手检测时阻塞的问题实现LED闪烁的非阻塞总结补充&#xff08;为什么不会阻塞&#xff09; 参考江协科技 KEY1和KEY2两者独立控制互不影响 阻塞&#xff1a;如果按下按键不松手&#xff0c;程序就…

开源语音克隆项目 OpenVoice V2 本地部署

#本机环境 WIN11 I5 GPU 4060ti 16G 内存 32G #开始 git clone https://github.com/myshell-ai/OpenVoice.git conda create -n opvenv python3.9 -y conda activate opvenv pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/…

DeepSeek大模型一键部署解决方案:全平台多机分布式推理与国产硬件优化异构计算私有部署

DeepSeek R1 走红后&#xff0c;私有部署需求也随之增长&#xff0c;各种私有部署教程层出不穷。大部分教程只是简单地使用 Ollama、LM Studio 单机运行量化蒸馏模型&#xff0c;无法满足复杂场景需求。一些操作配置也过于繁琐&#xff0c;有的需要手动下载并合并分片模型文件&…

如何利用PLM软件有效地推进制造企业标准化工作?

在智能制造浪潮的推动下&#xff0c;中国制造业正面临从“规模扩张”向“质量提升”的关键转型。工信部数据显示&#xff0c;85%的制造企业在产品研发、生产过程中因标准化程度不足导致效率损失超20%&#xff0c;而标准化水平每提升10%&#xff0c;企业综合成本可降低5%-8%。如…

环境影响评价(EIA)中,土地利用、植被类型及生态系统图件的制作

在环境影响评价&#xff08;EIA&#xff09;中&#xff0c;土地利用、植被类型及生态系统图件的制作需依据科学、法规和技术规范&#xff0c;以确保数据的准确性和图件的规范性。以下是主要的制作依据&#xff1a; 1. 法律法规与政策依据 《中华人民共和国环境影响评价法》 明确…

更高效实用 vscode 的常用设置

VSCode 可以说是文本编辑神器, 不止程序员使用, 普通人用其作为文本编辑工具, 更是效率翻倍. 这里分享博主对于 VSCode 的好用设置, 让 VSCode 如虎添翼 进入设置 首先进入设置界面, 后续都在这里进行配置修改 具体设置 每项配置通过搜索关键字, 来快速定位配置项 自动保存…

【异或数列——博弈论】

题目 思路 异或和为0&#xff08;即每一位都有偶数个1&#xff09;&#xff1a;平局最高有效位只有唯一的1&#xff1a;先手必胜最高有效位有奇数个1&#xff0c;偶数个0&#xff1a;先手必胜 若先选1产生优势&#xff0c;则剩下偶数个1&#xff0c;偶数个0&#xff1a;对手选…

草图绘制技巧

1、点击菜单栏文件–》新建–》左下角高级新手切换–》零件&#xff1b; 2、槽口&#xff1a;直槽口&#xff0c;中心点槽口&#xff0c;三点源槽口&#xff0c;中心点圆弧槽口&#xff1b; 3、草图的约束&#xff1a;需要按住ctrl键&#xff0c;选中两个草图&#xff0c;然后…

Spring Boot中如何自定义Starter

文章目录 Spring Boot中如何自定义Starter概念和作用1. 概念介绍2. 作用和优势2.1 简化依赖管理2.2 提供开箱即用的自动配置2.3 标准化和模块化开发2.4 提高开发效率2.5 提供灵活的配置覆盖3. 应用场景创建核心依赖1. 确定核心依赖的作用2. 创建 starter-core 模块2.1 依赖管理…

内容中台构建高效数字化内容管理新范式

内容概要 在数字化转型浪潮中&#xff0c;高效的内容管理能力已成为企业构建核心竞争力的关键要素。通过动态发布引擎、元数据智能分类与跨平台协作机制&#xff0c;企业能够实现内容的实时触达与精准分发&#xff0c;同时确保知识资产在多终端环境下的无缝适配与安全共享。这…

PyQt组态软件 拖拽设计界面测试

PyQt组态软件测试 最近在研究PyQt,尝试写个拖拽设计界面的组态软件&#xff0c;目前实现的功能如下&#xff1a; 支持拖入控件&#xff0c;鼠标拖动控件位置 拖动控件边缘修改控件大小支持属性编辑器&#xff0c;修改当前选中控件的属性 拖动框选控件&#xff0c;点选控件 控…

深度学习R4周:LSTM-火灾温度预测

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 任务&#xff1a; 数据集中提供了火灾温度&#xff08;Tem1&#xff09;、一氧化碳浓度&#xff08;CO 1&#xff09;烟雾浓度&#xff08;Soot 1&#xff09;…

Datawhale 数学建模导论二 笔记1

第6章 数据处理与拟合模型 本章主要涉及到的知识点有&#xff1a; 数据与大数据Python数据预处理常见的统计分析模型随机过程与随机模拟数据可视化 本章内容涉及到基础的概率论与数理统计理论&#xff0c;如果对这部分内容不熟悉&#xff0c;可以参考相关概率论与数理统计的…

UIView 与 CALayer 的联系和区别

今天说一下UIView 与 CALayer 一、UIView 和 CALayer 的关系 在 iOS 开发中&#xff0c;UIView 是用户界面的基础&#xff0c;它负责处理用户交互和绘制内容&#xff0c;而 CALayer 是 UIView 内部用于显示内容的核心图层&#xff08;Layer&#xff09;。每个 UIView 内部都有…

一键安装教程

Maven 安装 右键 以管理员身份运行点击 下一步安装完成后会同步配置环境变量打开 cmd, 输入 mvn 查看mvn版本修改 maven 本地仓库地址 见图三, 本地新建文件夹&#xff0c;修改为你本地文件夹地址 Redis 安装 右键 以管理员身份运行点击 下一步会安装到选择的文件夹下 JAVA\R…

李宏毅机器学习笔记:【6.Optimization、Adaptive Learning Rate】

Optimization 1.Adaptive Learning Rate2.不同的参数需要不同的学习率3.Root Mean Square4.RMSProp5.Adam6.learning rate scheduling7.warm up总结 critical point不一定是你在训练一个network时候遇到的最大的障碍。 1.Adaptive Learning Rate 也就是我们要给每个参数不同的…

vscode使用常见问题处理合集

目录 一、使用vite创建的vue3项目&#xff0c;script和style首行代码不会缩进,且格式化属性字段等会换行问题 首行缩进情况如下&#xff1a; 属性、参数格式化换行情况如下&#xff1a; 解决方式&#xff1a; 一、使用vite创建的vue3项目&#xff0c;script和style首行代码不…

【技术解析】MultiPatchFormer:多尺度时间序列预测的全新突破

今天给我大家带来一篇最新的时间序列预测论文——MultiPatchFormer。这篇论文提出了一种基于Transformer的创新模型&#xff0c;旨在解决时间序列预测中的关键挑战&#xff0c;特别是在处理多尺度时间依赖性和复杂通道间相关性时的难题。MultiPatchFormer通过引入一维卷积技术&…

【网络安全 | 漏洞挖掘】价值3133美元的Google IDOR

未经许可,不得转载。 文章目录 正文正文 目标URL:REDACTED.google.com。 为了深入了解其功能,我查阅了 developer.google.com 上的相关文档,并开始进行测试。 在测试过程中,我发现了一个 XSS 漏洞,但它触发的域名是经过正确沙盒化的 *.googleusercontent.com,这符合 …