当前位置: 首页 > news >正文

【算法学习计划】动态规划 -- 回文串问题

目录

leetcode 647.回文子串

leetcode 5.最长回文子串

leetcode1745.分割回文串IV

leetcode 132.分割回文串Ⅱ

leetcode 516.最长回文子序列

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


今天,我们将通过 6 道题目,来带各位了解并掌握动态规划中的回文串问题

(下文中的标题都是leedcode对应题目的链接)

leetcode 647.回文子串

首先根据我们之前的经验,我们的状态表示可以是:以 i 位置为结尾的所有回文子串的数目

但是我们很快会发现一个问题,那就是,我们无法通过一个 i 位置来确定这个子串是否是回文子串

所以我们需要两个位置,一个 i,一个 j,通过这两个位置,我们就能够确定这个 i~j 范围内的子串是否是回文

所以我们可以这样子,建立一个dp表表示这个范围内的子串是否是回文,而我们的返回值要一个数目,那我们就将dp表中所有为true的子串全部加起来,最后的结果就是返回值

具体这样判断:首先需要知道 i 位置的值和 j 位置的值是否相等,如果不相等的话,那么这就一定不是回文子串,就可以直接设置为false

如果相等,那么 i 和 j 会有三种情况,如果 i == j,那就意味着 i 和 j 指向同一个位置,那么只有他一个元素自然是回文,第二种情况就是: i+1 == j,这种情况意味着 i 和 j 是相邻的,而两个元素又有一个相等的前提,那么自然也是回文

随后一种就是,i 位置和 j 位置之间有其他元素,那么既然 i 位置的元素和 j 位置的元素相同,那么我们只需要判断 i+1 位置到 j-1 位置之间的子串是否回文即可,而这种情况我们可以在dp表中看到,也就是dp[i+1][j-1],既然有用到 i+1,那么我们的填表顺序就是从右往左填

代码如下:

class Solution {
public:
    int countSubstrings(string s) 
    {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n));

        int res = 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][j] = true;
                    else dp[i][j] = dp[i+1][j-1];
                    if(dp[i][j]) res++;
                }
        }    
        return res;
    }
};

leetcode 5.最长回文子串

题意跟简单,就是给你一串字符串,找到里面长度最大的那个子字符串

首先我们可以沿用上一道题的方法,先创建一个dp表,然后我们表里面的信息就是 i、j 区域内的子串是否为回文

然后我们要的最长可以在填表的时候同步更新,也就是,我们每一次判断完是否是回文串之后,如果是的话,那么我们就可以通过 j - i + 1 来获得这个这一串回文串的长度,接着我们在外面设置三个变量res

接着,如果这个串比我们记录的res(之前的最长子串)还要长的话,那就证明我们之前记录的并不是最长,那就更新res、left、right,否则就不更新(left、right代表最长子串的左右下标,因为我们最后要返回一个字符串,我们就需要用到substr,所以需要同步更新下标)

而我们的返回值就是 s.substr(left, right-left+1);(下面的代码为了方便就用了a、b)

代码如下:

class Solution {
public:
    string longestPalindrome(string s) 
    {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n));

        int comp = INT_MIN, a = 0, b = 0;
        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] == 0 ? 0 : dp[i+1][j-1] + 2;
                }
                if(comp < dp[i][j])
                    comp = dp[i][j], a = i, b = j;

            }
        }   
        return s.substr(a, b-a+1);
    }
};

leetcode1745.分割回文串IV

这题是困难题,但是当我们有了之前的经验之后,这道题其实和一道简单题差不多

首先来看题目,就是给你一个字符串,返回他能否被切割成三个字符串

那我们就先创建一个dp表,然后表里面放的都是 i、j 区域内这个字符串是否是回文串

当我们填完了这个表之后,而我们又需要看能否被切割成三个,那么先来举个例子:

0 ~ i-1      i ~ j       j+1 ~ n-1

首先如果他能形成三个子串的话,那么下标必然满足这个规律,那么我们是否可以直接遍历原表,因为我们将前两个确定了之后,最后一个区域是可以直接算出来的,所以我们只需要两层for循环,在一个O(N^2)时间复杂度之内将同时满足这三个条件的情况找出来,如果找得到,那么就返回true,否则就是false

经过了前面的铺垫之后,这一道题就是这么简单

代码如下:

class Solution {
public:
    bool checkPartitioning(string s) 
    {
        int n = s.size();
        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] = true;
                    else dp[i][j] = dp[i+1][j-1];
                }
        }

        for(int a = 1; a < n-1; a++)
        {
            for(int b = a; b < n-1; b++)
                if(dp[0][a-1] && dp[a][b] && dp[b+1][n-1])
                    return true;
        }

        return false;
    }
};

    

leetcode 132.分割回文串Ⅱ

题意很简单,就是给你一个字符串,问你把这个字符串分割成一个一个的子字符串最少需要多少次

首先,先确定状态表示:以 i 位置为结尾的子串被全部分割成回文子串的最少分割次数

那么既然是以 i 位置为结尾的,那么我们就会分为以下两种情况,一个是,如果 0 ~ i 位置这一整个字符串本身就是一个回文序列,那么我们是不是就不用切割了啊!那么此时的次数就是 0,这就是最少的次数

接着就是,如果 0~i 这个区间的字符串本身不是一个回文串,那么我们就需要引入一个 j 变量来在 i 变量之前搜索,如果 j ~ i 位置内的字符串是一个回文子串,那么我们 i 位置就应该填dp[j-1] + 1,代表 j - 1 位置之前的子串最少分割次数,最后因为我们无法确定 j 处于哪个位置的时候,切割的次数最少,所以我们需要依次遍历,最后取一个最小值

(注意,最坏的情况也就是 i 位置和 j 位置是同一个位置,这时候就相当于这个字符单独一个作为回文子串)

最后就是,我们在上面的判断之中会重复判断某一串字符串是否是回文串,所以我们可以先创建一张dp表,将每一个位置是否是回文串的信息先提前填好,这样我们就可以做到 O(1) 级别的时间复杂度来找到

这个步骤上面的每一道题都有讲到,这里就不讲了

代码如下:

class Solution {
public:
    int minCut(string s) 
    {
        int n = s.size(), i, j;
        vector<vector<int>> judge(n, vector<int>(n));

        for(i = n-1; i >= 0; i--)
            for(j = i; j < n; j++)
                if(s[i] == s[j])
                {
                    if(i == j || i+1 == j) judge[i][j] = true;
                    else judge[i][j] = judge[i+1][j-1];
                }
        vector<int> dp(n, INT_MAX);
        dp[0] = 0;

        for(i = 1; i < n; i++)
        {
            if(judge[0][i])
            {
                dp[i] = 0;
                continue;
            }
            for(j = 1; j <= i; j++)
                if(judge[j][i])dp[i] = min(dp[j-1] + 1, dp[i]);
        }

        return dp[n-1];
    }
};

leetcode 516.最长回文子序列

首先还是先写状态表示:由于我们是要找出最长的回文子序列,那么依据前面的经验,我们的状态表示就应该是:

在 i ~ j 区间内的所有子序列中,最长的那个子序列的长度

然后就是情况分析,分类讨论了

  • 当 s[i] == s[j] 时,我们就又会面临三种情况,第一个是当 i 位置和 j 位置是同一个位置的时候,那么这时候的长度就是 1,当 i 位置的下一个位置就是 j 位置的时候,而 s[i] == s[j],所以这时候的长度就应该等于 2,最后就是 i 位置和 j 位置之间间隔了很多其他元素,那么在我们的dp表中,dp[i+1][j-1] 位置不久刚好存着 i ~ j 位置之内不包括 i 和 j 的其他元素的最长回文子序列的长度吗,这时我们 dp[i][j] 就应该等于dp[i+1][j-1] + 2,而我们的 i 在本次循环的时候不动,j 在向后遍历,那么我们无法确定哪个 i、j 对于的子序列长度最长,所以我们在这里需要做一个max的判定操作
  • 当 s[i] != s[j] 时,这时候就意味着,以 i 为起点 j 为终点的子序列一定不能同时包含 i 和 j,因为他们两个不相等,而这不符合回文,所以我们就需要在 i+1 ~ j  或者  i ~ j-1  中去找,这个可以在dp表中找,而这两种情况只是因为可以将不同时包含 i、j 的所有回文子序列找清楚,但是我们不能确定这两个哪个区间的长度会更大,所以我们在这里需要进行一个max操作,取大的那一个

经过上面的分析,我们就将所有的情况都分析清楚了,接下来我们来讲一讲填表顺序

由于我们需要用到 i+1 位置的值,所以我们在填表的时候,我们需要从右往左填,这样才能保证当我们需要用到 i+1 位置的时候,不会为空

接着是初始化,由于最小的子序列就是单个元素本身,长度就是 1,所以我们在初始化的时候,我们可以在一刚开始就将所有元素全部初始化为 1 (当然不初始化也没关系,因为我们都特判到了)

最后是返回值,由于我们要的是最长子序列的长度,所以我们就需要返回dp表中的最大值,这个工作我们可以在填表的时候同步进行

代码如下:

class Solution {
public:
    int longestPalindromeSubseq(string s) 
    {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n, 1));

        int res = 1;
        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; 
                }
                else dp[i][j] = max(dp[i][j-1], dp[i+1][j]);

                res = max(res, dp[i][j]);
            }
        }  

        return res;  
    }
};

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

这道题目经过上面那些题目的铺垫之后,其实已经没有那么难了

先来分析一下题目,我们可以得知,单单一个位置是无法完全确定是否是回文的,所以我们需要一个区间,一个头一个尾来进行判断

所以我们的状态表示就是:在 i、j 区域内的子串要被添加成回文子串最少需要插入几次

然后就是对着 i、j 这两个位置分析

其实和上一道题是几乎一样的:一个是两个位置的值是相等的,这延伸出三种情况,一个是 i 和 j 是同一个位置,一个是 i 位置的下一个位置是 j。这两种情况的插入次数都是 0

第三种是 i 和 j 之间又很多其他元素,这种情况的插入次数可以直接在dp表里面找,也就是dp[i+1][j-1]

然后第二个大类是两个位置的值不相等,那么我们就把 i ~ j-1  和  i+1 ~ j   这两种情况分别看成两个整体,所以我们只需要在原本的次数上再加上一次 i 或者一次 j 即可,所以就是:min(dp[i+1][j] + 1, dp[i][j-1] + 1)  (min操作时为了取这两种情况的最小次数)

最后我们返回的值就是dp[0][n-1],代表整个字符串需要的最少插入次数

代码如下:
 

class Solution {
public:
    int minInsertions(string s) 
    {
        int n = s.size();
        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+1][j] + 1, dp[i][j-1] + 1);
            }
        }

        return dp[0][n-1];
    }
};

今天这篇博客到这里就结束啦~( ̄▽ ̄)~*

如果觉得对你有帮助的话,希望可以关注一下喔

相关文章:

  • React前端开发中实现断点续传
  • CSS - Pseudo-classes(伪类选择器)
  • TypeScript类型兼容性 vs JavaScript动态类型:深入对比解析
  • 共享经济再中介化进程中的技术创新与模式重构研究——以“开源AI智能名片链动2+1模式S2B2C商城小程序“为例
  • python | 输入日期,判断这一天是这一年的第几天
  • 分布式 IO 模块:氢能源安全高效储运的智能钥匙
  • 项目中使用柯里化函数
  • 优选算法系列(2.滑动窗口 _ 上)
  • 基于CPLD+MCU的3U机箱数字量输入采集板DI,主要针对标准DC110V开关量信号进行采集处理
  • 【CPU】CPU多级缓存和MESI一致性协议
  • 基于System V的共享内存函数使用指南
  • 云原生混合云管理:跨集群智能编排引擎
  • NumPy系列 - 创建矩阵
  • 青少年编程与数学 02-011 MySQL数据库应用 02课题、MySQL数据库安装
  • 微服务架构中10个常用的设计模式
  • GUI编程和TKinter介绍
  • MongoDB下载安装
  • 【MySQL】(6) 数据库约束
  • 使用unsloth进行grpo强化学习训练
  • html5制作2048游戏开发心得与技术分享
  • 河南发布高温橙警:郑州、洛阳等地最高气温将达40℃以上
  • 新时代,新方志:2025上海地方志论坛暨理论研讨会举办
  • 北邮今年本科招生将首次突破四千人,新增低空技术与工程专业
  • “朱雀玄武敕令”改名“周乔治华盛顿”?警方称未通过审核
  • 韩正会见美国景顺集团董事会主席瓦格纳
  • 为什么越来越多景区,把C位留给了书店?