【算法学习计划】动态规划 -- 回文串问题
目录
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];
}
};
今天这篇博客到这里就结束啦~( ̄▽ ̄)~*
如果觉得对你有帮助的话,希望可以关注一下喔