当前位置:   article > 正文

代码随想录算法训练营DAY54|C++动态规划Part15|⭐️647.回文子串、516.最长回文子序列

代码随想录算法训练营DAY54|C++动态规划Part15|⭐️647.回文子串、516.最长回文子序列

⭐️647.回文子串

力扣题目链接

文章链接:647.回文子串

视频链接:动态规划,字符串性质决定了DP数组的定义 | LeetCode:647.回文子串
状态:第一反应必须得是双指针,然后再是到动态规划,为了能够顺利解答最长回文子串,本题一定也要掌握动态规划。
其实回文子串类题目的动规思路,也是照着双指针来的

其实子串问题和子序列问题非常类似,也是存在最优子结构,那就意味着原问题的最优解可以由子问题的最优解推导出来。

其实回文的问题很容易联想到双指针。不过为了后续能够解决516.最长回文子序列问题,我们还是先使用动规解决一下该问题,为后面打基础。

双指针

首先我们必须明确,回文子串就是以一个字符为中心(基数长度的回文),也可以是以两个相邻字符之间的位置为中心(偶数长度的回文)。

所以我们可以考虑以字符串中的每一个字符为中心,然后尝试扩展这个中心来查找可能的回文子串

  • 定义一个辅助函数enpandAroundCenter,该函数接受字符串和两个指针位置,然后向两边扩展,直到不再形成回文,没形成一个回文,计数器就增加
std::function<void(int, int)> expandAroundCenter = [&](int left, int right) {
	while(left >= 0 && right < n && s[left] == s[right]) {
		ans++;
		left--;
		right++;
	}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 然后我们可以分别检查字符串的偶数位置为中心和以奇数位置为中心。
for (int i = 0; i < n; i++){
    expandAroundCenter(i, i);
    if (i + 1 < n) expandAroundCenter(i, i + 1);
}
  • 1
  • 2
  • 3
  • 4
  • 总体代码:
class Solution {
public:
    int countSubstrings(string s) {
        int n = s.length();
        int ans = 0;
        
        // 使用lambda表达式来进行函数定义
        // 可以用aotu 代替 std::function
        std::function<void(int, int)> expandAroundCenter = [&](int left, int right) {
            while (left >= 0 && right < n && s[left] == s[right]) {
                ans++; // Count this palindrome
                left--; // Expand to the left
                right++; // Expand to the right
            }
        };   
        for (int i = 0; i < n; i++) {
            // 检查偶数位置
            expandAroundCenter(i, i);
            // 检查奇数位置
            if (i + 1 < n) {
                expandAroundCenter(i, i + 1);
            }
        }
        return ans;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)

动规思路

  • 确定dp数组以及下标含义

我们回文子串的问题并不能像解决子序列问题那样去设置dp数组。

如果我们定义,dp[i] 为 下标i结尾的字符串有 dp[i]个回文串的话,我们会发现很难找到递归关系,因为根本就无法跟相邻数组元素有什么关系。联想到使用双指针解决回文子串,我们的dp数组设置是不是也能参考设计呢

最好的解决方法设置:布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

  • 确定递推公式

两种情况:s[i]与s[j]相等,s[i]与s[j]不相等,其中相等时又分为三种情况

如果不相等,那dp[i][j]=false

如果相等:

  1. 下标i 与 j相同,同一个字符例如a,当然是回文子串
  2. 下标i 与 j相差为1,例如aa,也是回文子串
  3. 下标:ij相差大于1的时候,例如cabac此时s[i]s[j]已经相同了,我们看ij区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true

综上所述:

if (s[i] == s[j]) {
    if (j - i <= 1) { // 情况一 和 情况二
        result++;
        dp[i][j] = true;
    } else if (dp[i + 1][j - 1]) { // 情况三
        result++;
        dp[i][j] = true;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 如何初始化

全是设置为false

  • 确定遍历顺序

二维的递推顺序最好就是根据递推公式画一个格子:

我们需要首先知道左下角的格子,才能推导出我们的dp,

所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的

  • 推导dp数组

输入:“aaa”,dp[i][j]状态如下:

图片中有6个true,所以就有六个回文子串

CPP代码

这里的两个代码技巧都是要掌握的,我第一次写的时候,甚至判断了s[i] != s[j],成功写出一坨。

//完整代码
class Solution {
public:
    int countSubstrings(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int result = 0;
        for (int i = s.size() - 1; i >= 0; i--) {  // 注意遍历顺序
            for (int j = i; j < s.size(); j++) {
                if (s[i] == s[j]) {
                    if (j - i <= 1) { // 情况一 和 情况二
                        result++;
                        dp[i][j] = true;
                    } else if (dp[i + 1][j - 1]) { // 情况三
                        result++;
                        dp[i][j] = true;
                    }
                }
            }
        }
        return result;
    }
};

//简洁版代码
class Solution {
public:
    int countSubstrings(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int result = 0;
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i; j < s.size(); j++) {
                if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
                    result++;
                    dp[i][j] = true;
                }
            }
        }
        return result;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)

516最长回文子序列

力扣题目链接

文章链接:516最长回文子序列

视频链接:动态规划再显神通,LeetCode:516.最长回文子序列
状态:有点思维惯性了,延续上文的true or false给我整懵了,其实子序列的题目设置dp数组基本都是一个套路,基本的本题遇上一题的相同点在于,基本的判断思路是延续的。

对于这个问题,使用双指针并不是最有效的方法。因为最长回文子序列不要求是连续的字符,而是可以跳过某些字符形成回文。本题中,回文子序列与回文子串是有区别的。

思路

  • 确定dp数组以及下标的含义

dp[i][j]:字符串s[i, j]范围内最长的回文子序列的长度为dp[i][j]

  • 确定递推公式

在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同。

如果s[i]s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;

具体如图所示:

如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列

加入s[j]的回文子序列长度为dp[i + 1][j]

加入s[i]的回文子序列长度为dp[i][j - 1]

那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);

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]);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • dp数组如何初始化

从递推公式可以看出,递推公式计算不到i 和j相同时候的情况。

当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。

其他都应该初始化成0,这样题对公式dp[i][j]=max(dp[i + 1][j], dp[i][j - 1]);才不会被初始值覆盖

vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
  • 1
  • 2
  • 确定遍历顺序

从递归公式中,可以看出,dp[i][j] 依赖于 dp[i + 1][j - 1]dp[i + 1][j]dp[i][j - 1],如图:

所以当我们遍历i的时候是从上到下,然后j是从左到右

for (int i = s.size() - 1; i >= 0; i--) {
    for (int j = i + 1; j < s.size(); j++) {
    ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 举例推导dp数组

输入s:“cbbd” 为例,dp数组状态如图:

20210127151521432

红色框即:dp[0][s.size() - 1]; 为最终结果。

CPP代码

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i + 1; j < s.size(); 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][s.size() - 1];
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

动态规划总结篇

二刷回来整总结!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/660414
推荐阅读