动态规划算法----回文串问题

引言

在算法的世界里,回文串问题一直是一个经典且富有挑战性的题目。而动态规划作为一种强大的算法思想,为解决这类问题提供了高效且优雅的解决方案。本文将深入探讨如何运用动态规划算法来解决回文串相关问题,从问题描述、动态规划思路,到代码实现与复杂度分析,全面剖析这一过程。

回文串问题描述

回文串是指一个字符串从左到右读和从右到左读是完全一样的,例如 “level”、“madam” 等。常见的回文串问题有:给定一个字符串,找出其中最长的回文子串。例如,对于字符串 “babad”,最长回文子串是 “bab” 或 “aba”;对于字符串 “cbbd”,最长回文子串是 “bb”。

动态规划思路

  1. 定义状态
    我们使用一个二维数组 dp[i][j] 来表示字符串 s 从索引 i 到 j 的子串是否为回文串。如果 dp[i][j] 为 true,则表示 s[i...j] 是回文串;否则为 false
  2. 状态转移方程
    • 当 i == j 时,即单个字符,显然是回文串,所以 dp[i][i] = true
    • 当 j - i == 1 时,即两个字符的子串,如果这两个字符相等,那么该子串是回文串,即 dp[i][j] = (s[i] == s[j])
    • 对于长度大于 2 的子串,当 s[i] == s[j] 且 dp[i + 1][j - 1] 为 true 时,说明去掉两端字符后的子串是回文串,那么 dp[i][j] = true。用公式表示为:
      if (s[i] == s[j]) {
          if (j - i <= 2) {
              dp[i][j] = true;
          } else {
              dp[i][j] = dp[i + 1][j - 1];
          }
      } else {
          dp[i][j] = false;
      }

3.遍历顺序
由于计算 dp[i][j] 需要依赖 dp[i + 1][j - 1],所以我们需要从较短的子串开始计算,逐步计算到较长的子串。具体来说,我们先遍历子串的长度 len,从 1 到字符串的长度 n,然后遍历子串的起始位置 i,根据长度 len 计算出结束位置 j = i + len - 1

案例一  回文子串

题目链接

回文子串https://leetcode.cn/problems/palindromic-substrings/description/https://leetcode.cn/problems/palindromic-substrings/description/

问题描述

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

 算法思路

1.状态表示

dp[i][j]:表示字符串在区间(i,j)内的子串,是否是回文串。(以i位置表示开始,j位置表示结束)

注意!!!  这里有一个隐藏条件: i  <=  j   即只用到矩阵上三角部分 

2.状态转移方程

对于dp[i][j]位置的状态转移方程分类讨论如下:

(1)s[i] != s[j]   则dp[i][j] = false;

  (2)   s[i] == s[j],根据子串长度分类讨论如下

  • i==j  则dp[i][j] = true;    (子串长度为一)
  • i+1 == j,  则dp[i][j] = true   (子串长度为2)
  • i+1 < j    则dp[i][j] = dp[i+1][j-1]   (子串长度大于2)

3.初始化 

在状态转移方程分类讨论时,已经对方程的边界情况做了说明,故此题初始化可以省略。

4.填表顺序 

由状态转移方程可以知道,在填dp[i][j]时,用到dp[i+1][j-1],故填表顺序应该从下往上填写每一行,从左往右填写每一行。

5.返回值

返回dp表中true的个数

代码编写

 int countSubstrings(string s) 
    {
        int n =  s.size();
        vector> dp(n,vector (n));
        int ret = 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])
                    ret++;
            }
        }
        return ret;
    }

案例二  最长回文子串 

题目链接

最长回文子串https://leetcode.cn/problems/longest-palindromic-substring/description/https://leetcode.cn/problems/longest-palindromic-substring/description/

问题描述

给你一个字符串 s,找到 s 中最长的 回文子串。

示例 1:输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。

示例 2:输入:s = "cbbd" 输出:"bb"

算法思路

判断子串是否是回文---用dp表统计所有子串是否是回文的信息----根据dp表的起始位置得到结果

dp[i][j]:表示字符串在区间(i,j)内的子串,是否是回文串。(以i位置表示开始,j位置表示结束)

注意!!!  这里有一个隐藏条件: i  <=  j   即只用到矩阵上三角部分 

2.状态转移方程

对于dp[i][j]位置的状态转移方程分类讨论如下:

(1)s[i] != s[j]   则dp[i][j] = false;

  (2)   s[i] == s[j],根据子串长度分类讨论如下

  • i==j  则dp[i][j] = true;    (子串长度为一)
  • i+1 == j,  则dp[i][j] = true   (子串长度为2)
  • i+1 < j    则dp[i][j] = dp[i+1][j-1]   (子串长度大于2)

3.初始化 

在状态转移方程分类讨论时,已经对方程的边界情况做了说明,故此题初始化可以省略。

4.填表顺序 

由状态转移方程可以知道,在填dp[i][j]时,用到dp[i+1][j-1],故填表顺序应该从下往上填写每一行,从左往右填写每一行。

5.返回值

dp表中值为true的情况下,长度最大的回文字串的起始位置以及长度

代码编写

string longestPalindrome(string s) {
        int n = s.size();
        vector> dp(n,vector(n));
        int len = 1,begin = 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,begin = i;
                } 
            }
        }
        return s.substr(begin,len);
        
    }

案例三   回文串分割Ⅳ 

题目链接

 回文串分割Ⅳhttps://leetcode.cn/problems/palindrome-partitioning-iv/description/https://leetcode.cn/problems/palindrome-partitioning-iv/description/

题目描述

 给你一个字符串 s ,如果可以将它分割成三个 非空 回文子字符串,那么返回 true ,否则返回 false 。当一个字符串正着读和反着读是一模一样的,就称其为 回文字符串 。

示例 1:

输入:s = "abcbdd"
输出:true
解释:"abcbdd" = "a" + "bcb" + "dd",三个子字符串都是回文的。

示例 2:

输入:s = "bcbddxy"
输出:false
解释:s 没办法被分割成 3 个回文子字符串。

算法思路 

首先用dp表保存所有字串是否是回文串的信息,对字符串通过i,j 位置可以分割为(0,i-1)、(i,j)、(j+1,n-1)三部分,枚举所有中间字符串的起始位置和结束位置。

代码编写

bool checkPartitioning(string s) 
    {
        //1,用动态规划思想把所有子串是否是回文串预处理一下
        int n = s.size();
        vector> dp(n,vector (n));
        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;
        //2.枚举所有的第二个字符串的起始位置以及结束位置
        for(int i = 1; i < n; i++)
            for(int j = i; j < n-1; j++)
            {
                if(dp[0][i-1] && dp[i][j] && dp[j+1][n-1])
                    return true;
            }
        return false;
    }

案例四  回文串分割Ⅱ 

题目链接

132. 分割回文串 II - 力扣(LeetCode)

题目描述 

 给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回符合要求的 最少分割次数 。

示例 1:

输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。

示例 2:

输入:s = "a"
输出:0

算法思路 

 1.状态表示

dp[i]: 表示字符串s在区间(0,i)上的最长子串,最少分割次数

2.状态转移方程

dp[i]位置的值可以分类讨论:

  • 区间(0,i)是回文子串,则有dp[i] = 0;
  • 区间(0,i)不是回文子串,则枚举(0,i)区间的子区间(j,i)的所有子串,进行如下分类讨论 :

                如果是回文,则由dp[j-1]+1,在所有分割中取次数最少分割,dp[i] = min(dp[j-1]+1,dp[i])

                如果不是回文则,继续枚举下一个区间

优化措施:

用二维dp表,将所有的子串是否是回文的信息保存在dp表中

3.初始化

首先,由状态转移方程可以知道只有当j = 0时才可能发生越界访问情况,但是j 的取值范围是 

0 < j <= i,

其次,为了防止第一次求min时,dp[i]的值干扰结果,所以将dp表中所有的值初始化为无穷大

4.填表顺序

从左向右依次填写

5.返回值

dp[n-1]

代码编写

int minCut(string s) 
    {
        //1.预处理所有子串是否是回文子串、
        int n = s.size();
        vector> dp(n,vector(n));
        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;
        vector dp2(n,INT_MAX);
        for(int i = 0; i < n; i++)
        {
            if(dp[0][i]) dp2[i] = 0;
            else 
            {
                for(int j =1; j <= i; j++)
                {
                    if(dp[j][i])
                        dp2[i] = min(dp2[i], dp2[j-1]+1);
                }
            }
        }
        return dp2[n-1];
    }

案例五   最长回文子序列

 题目链接

516. 最长回文子序列 - 力扣(LeetCode)

题目描述

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

算法思路

1.状态表示

dp[i][j]:表示字符串s在区间(i,j)内所有子序列中,最长的回文序列长度

2.状态转移方程

对于dp[i][j]的值作如下分类讨论:

(1)s[i] == s[j],根据子串长度做如下分类讨论:

  • i==j   则dp[i][j] = 1
  • i+1 == j  则 dp[i][j] = 2
  • i+1 < j   则dp[i][j] = dp[i+1][j-1]+2

(2)   s[i] != s[j] ,  则dp[i][j] = max(dp[i+1][j],dp[i][j-1])

3.初始化

由状态转移方程可以知道,对于dp[i][j]的值可能需要用到 dp[i+1][j-1]、dp[i+1][j]、dp[i][j-1]三个位置的值,只有dp[0][0]和dp[n-1][n-1]可能发生越界访问,但是在分类讨论时已经处理过,所以不需要进行初始化

4.填表顺序

从下往上填,每一行从左往右填

5.返回值

dp[0][n-1]

代码编写

int longestPalindromeSubseq(string s) 
    {
        int n = s.size();
        vector> dp(n,vector(n));
        for(int i = n-1; i >= 0; i--)
        {
            dp[i][i] = 1;
            for(int j = i+1; j < n; j++)
            {
                if(s[i] == s[j]) 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];
    }

总结

动态规划算法为解决回文串问题提供了一种系统性的方法。通过合理定义状态和状态转移方程,我们能够有效地找出最长回文子串。理解这一过程不仅有助于解决具体的回文串问题,还能提升对动态规划算法的理解和运用能力,在面对其他类似的字符串处理或最优子结构问题时,能够更得心应手地运用动态规划思想找到解决方案。希望本文的讲解能帮助你在算法学习的道路上更上一层楼,对动态规划与回文串问题有更深入的认识。

你可能感兴趣的:(算法,动态规划,c++)