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

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉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例题的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
在这里插入图片描述

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

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

相关文章

DeepSeek正重构具身大模型和人形机器人赛道!

中国人工智能公司DeepSeek&#xff08;深度求索&#xff09;以“低成本、高效率、强开放”的研发范式横空出世&#xff0c;火遍并震撼全球科技圈&#xff1b;DeepSeek展现出来的核心竞争力&#xff0c;除了低成本及推理能力&#xff0c;更重要的是开源模型能力追赶上了最新的闭…

网络工程师 (39)常见广域网技术

一、HDLC 前言 HDLC&#xff08;High-level Data Link Control&#xff0c;高级数据链路控制&#xff09;是一种面向比特的链路层协议。 &#xff08;一&#xff09;定义与历史背景 HDLC是由国际电信联盟&#xff08;ITU&#xff09;标准化的&#xff0c;它基于IBM公司早期的同…

制作Ubuntu根文件

系列文章目录 Linux内核学习 Linux 知识&#xff08;1&#xff09; Linux 知识&#xff08;2&#xff09; WSL Ubuntu QEMU 虚拟机 Linux 调试视频 PCIe 与 USB 的补充知识 vscode 使用说明 树莓派 4B 指南 设备驱动畅想 Linux内核子系统 Linux 文件系统挂载 QEMU 通过网络实现…

SpringMVC详解

文章目录 1 什么是MVC 1.1 MVC设计思想1.2 Spring MVC 2 SpringMVC快速入门3 SpringMVC处理请求 3.1 请求分类及处理方式 3.1.1 静态请求3.1.2 动态请求 3.2 处理静态请求 3.2.1 处理html文件请求3.2.2 处理图片等请求 3.3 处理动态请求 3.3.1 注解说明3.3.2 示例 3.4 常见问题…

一个让Stable Diffusion更稳定、更易用的Github开源项目

2023除了ChatGPT大火&#xff0c;Stable Diffusion同样也是非常火热&#xff0c;Stable Diffusion是一个Github开源项目&#xff0c;很多爱好者都会本地安装&#xff0c;但面对一些初学者来说&#xff0c;在安装、配置和使用过程中还是会经常出现很多问题&#xff0c;特别不了解…

Deepseek R1模型本地化部署+API接口调用详细教程:释放AI生产力

文章目录 前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装ollama2部署DeepSeek R1模型删除已存在模型&#xff0c;以7b模型为例 三、DeepSeek API接口调用Cline配置 前言 随着最近人工智能 DeepSeek 的爆火&#xff0c;越来越多的技术大佬们开始关注如…

[java] 常见的七大查找算法

目录 基本查找-重要 二分查找-重要 插值查找-重要 斐波那契查找 分块查找-重要 哈希查找 树表查找 基本查找-重要 也叫做顺序查找&#xff08;顺序查找适合于存储结构为数组或者链表&#xff09; 基本思想&#xff1a;顺序查找也称为线形查找&#xff0c;属于无序查找算…

开源、免费项目管理工具比较:2025最新整理30款

好用的开源、免费版项目管理系统有&#xff1a;1.Redmine&#xff1b;2. Taiga&#xff1b;3. OpenProject&#xff1b; 4.ProjectLibre&#xff1b; 5.GanttProject&#xff1b; 6.Tuleap&#xff1b; 7.Trac&#xff1b;8. Phabricator&#xff1b; 9.Notion&#xff1b; 10.…

Linux-C/C++《七、字符串处理》(字符串输入/输出、C 库中提供的字符串处理函数、正则表达式等)

字符串处理在几乎所有的编程语言中都是一个绕不开的话题&#xff0c;在一些高级语言当中&#xff0c;对字符串的处理支 持度更是完善&#xff0c;譬如 C、 C# 、 Python 等。若在 C 语言中想要对字符串进行相关的处理&#xff0c;譬如将两个字符串进行拼接、字符串查找、两个…

鸿蒙中,UIAbility组件启动模式(3种分别是Singleton(单实例模式)Multiton(多实例模式)Specified(指定实例模式))

UIAbility的启动模式是指UIAbility实例在启动时的不同呈现状态。针对不同的业务场景&#xff0c;系统提供了三种启动模式&#xff1a; Singleton&#xff08;单实例模式&#xff09; Multiton&#xff08;多实例模式&#xff09; Specified&#xff08;指定实例模式&#xf…

Linux第107步_Linux之PCF8563实验

使用PCF8563代替内核的RTC&#xff0c;可以降低功耗&#xff0c;提高时间的精度。同时有助于进一步熟悉I2C驱动的编写。 1、了解rtc_time64_to_tm()和rtc_tm_to_time64() 打开“drivers/rtc/lib.c” /* * rtc_time64_to_tm - Converts time64_t to rtc_time. * Convert seco…

上海正控ZK880 变频器基本操作

1&#xff0c;变频器参数设置&#xff1a; F0-02设置成2&#xff1b;&#xff08;通过modbus指令进行开机和关机的动作&#xff1b;&#xff09; Fd-00设置成5005&#xff1b; Fd-01设置成0&#xff1b; Fd-02设置成1&#xff1b; Fd-03设置成2ms&#xff1b; 2.硬件连接&a…

测试环境管理的最佳实践:从搭建到维护的实战指南

引言 在电商公司的一次“双十一”大促前,测试团队发现预发布环境的订单支付接口频繁超时。经过排查,发现测试环境的Redis版本与生产环境不一致,导致缓存策略失效。这一事件直接导致上线延迟48小时,损失数百万营收。测试环境的稳定性直接决定了软件交付的质量与效率。本文将…

利用亚马逊云科技RDS for SQL Server配置向量数据存储

生成式人工智能&#xff08;AI&#xff09;正迎来又一个快速发展期&#xff0c;引起了开发者们的广泛关注。将生成式能力集成到商业服务和解决方案中变得非常重要。当前的生成式AI解决方案是机器学习和深度学习模型逐步进化迭代的结果。从深度学习到生成式AI的质变飞跃主要是由…

YOLOV8的学习记录(一) 环境配置和安装

YOLO8的官网地址&#xff1a;YOLOv8 - Ultralytics YOLO Docs • YOLOV8的环境要求&#xff1a; YOLO集成在ultralytics库中&#xff0c;ultralytics库的环境要求&#xff1a; Python>3.7 PyTorch>1.10.0 在按照所需python版本新建好的conda环境中安装好torch&#x…

如何在 IntelliJ IDEA 中使用 Bito AI 插件

如何在 IntelliJ IDEA 中使用 Bito AI 插件 Bito: On-Demand AI Code Reviews Bito AI 插件是一个智能开发工具&#xff0c;能够帮助开发者提升编码效率&#xff0c;自动化生成代码、注释、单元测试等。本文将详细介绍 Bito AI 插件在 IntelliJ IDEA 中的使用方法&#xff0c…

如何升级Python版本。以下是详细的步骤和注意事项:检查当前Python版本:在命令行或终端中输入以下命令来查看当前安装的Python版本: bash复制代

升级Python版本。以下是详细的步骤和注意事项&#xff1a; 检查当前Python版本&#xff1a;在命令行或终端中输入以下命令来查看当前安装的Python版本&#xff1a; bash复制代码 python --version 这将显示你当前使用的Python版本。 下载最新版本的Python&#xff1a;访问Py…

SpringBoot(7)-Swagger

目录 一、是什么 二、SpringBoot集成Swagger 三、配置Swagger 3.1 配置文档信息 3.2 配置扫描接口 3.3 配置Swagger开关 3.4 配置API分组 3.5 实体配置 四、常用注解 五、总结 一、是什么 是一款API框架&#xff0c;API文档和API定义同步更新&#xff0c;可以在线测…

C++效率掌握之STL库:string底层剖析

文章目录 1.学习string底层的必要性2.string类对象基本函数实现3.string类对象的遍历4.string类对象的扩容追加5.string类对象的插入、删除6.string类对象的查找、提取、大小调整7.string类对象的流输出、流提取希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力…

SCI学术论文图片怎么免费绘制:drawio,gitmind

SCI学术论文图片怎么免费绘制 目录 SCI学术论文图片怎么免费绘制overleaf怎么图片不清晰怎么办SCI学术论文图片怎么导出pdfdrawiogitmind**1. 使用在线工具****Lucidchart****2. Draw.io****3. ProcessOn****4. 使用桌面工具****Dia****5. 使用Markdown工具(如Typora)**如果你…