字符串练习

labuladong的算法小抄 」学习笔记

内容

    最长公共子串
    最长公共子序列
    最长回文子串
    最长回文子序列
    最长递增子序列

最长公共子串

原题链接:力扣
先看最长公共子序列再看这道题。

时间复杂度O(MN),空间复杂度O(MN)。

类似最长公共子序列的分析,这里,我们使用c[i,j] 表示 以 Xi 和 Yj 结尾的最长公共子串的长度,因为要求子串连续,所以对于 Xi 与 Yj 来讲,它们要么与之前的公共子串构成新的公共子串;要么就是不构成公共子串。故状态转移方程:
在这里插入图片描述

class Solution {
public:
    int longestCommonSubstring(string str1, string str2) {
        int len1 = str1.length();
        int len2 = str2.length();
        vector<vector<int> > dp(len1 + 1, vector<int>(len2 + 1, 0));
        int result = 0;
        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (str1[i - 1] == str2[j - 1]) {
                    dp[i][j] = dp[i - 1][j -1] + 1;
                    result = max(dp[i][j], result);
                } else {
                    dp[i][j] = 0;
                }
            }
        }
        return result;
    }
};

最长公共子序列(Longest Common Subsequence,简称 LCS)

摘自:labuladong,原题链接:力扣1143

子序列类型的问题,穷举出所有可能的结果都不容易,而动态规划算法做的就是穷举 + 剪枝,它俩天生一对儿。所以可以说只要涉及子序列问题,十有八九都需要动态规划来解决,往这方面考虑就对了。

这道题是定义一个二维数组dp。

  • 第一步,一定要明确dp数组的含义。对于s1[1…i]和s2[1…j],它们的 LCS 长度是dp[i][j]。
  • 第二步,定义 base case。专门让索引为 0 的行和列表示空串,dp[0][…]和dp[…][0]都应该初始化为 0,这就是 base case。
  • 第三步,找状态转移方程。那么对于 s1 和 s2 中的每个字符,有什么选择?很简单,两种选择,要么在 lcs 中,要么不在。如果 s1[i] == s2[j] ,那么这个字符一定在 lcs 中;否则的话,s1[i] 和 s2[j] 这两个字符至少有一个不在 lcs 中,需要丢弃一个。
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size();
        int n = text2.size();
        int** dp = new int*[m+1];
        for(int i=0;i<=m;i++){
            dp[i] = new int[n+1];
            dp[i][0] = 0;
        }
        fill_n(dp[0],n+1,0);
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++){
                if(text1[i-1]==text2[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
};

最长回文子串

摘自:labuladong,原题链接:力扣5

回文串:正着读和反着读都一样的字符串
寻找回文串的问题核心思想:从中间开始向两边扩散来判断回文串。——使用双指针。

for 0 <= i < len(s):
    找到以 s[i] 为中心的回文串
    更新答案

因为回文串长度可能是技术或者偶数,所以做如下更改:

for 0 <= i < len(s):
    找到以 s[i] 为中心的回文串
    找到以 s[i] 和 s[i+1] 为中心的回文串
    更新答案

寻找最长字串的代码:

string palindrome(string& s, int l, int r) {
	// 防止索引越界
	while (1 >= 0 && r < s.size()&& s[l] == s[r]) {
		// 向两边展开
		l--; r++;
	}
	// 返回以 s[I] 和 s[r] 为中心的最长回文串
	return s.substr(l + 1,r - l - 1);
}

主函数完整代码如下:

string longestPalindrome(string s) {
	string res;
	for (int i = 0;i < s.size();i++) {
	// 以 s[i] 为中心的最长回文子串
		string s1 = palindrome(s,i,i);
		// 以 s[i] 和 s[i+1] 为中心的最长回文子串
		string s2 = palindrome(s,i,i + 1);
		// res = longest(res,s1,s2)
		res = res.size() > s1.size() ? res : s1;
		res = res.size() > s2.size() ? res : s2;
	}
	return res;
}

时间复杂度 O(N^2),空间复杂度 O(1)。

最长回文子序列

摘自:labuladong,原题链接:力扣516

一旦涉及到子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n^2)。既然要用动态规划,那就要定义 dp 数组,找状态转移关系。

其实有了前面的最长公共子序列做例子,这道题就好做了。

具体来说,如果我们想求 dp[i][j] ,假设知道了子问题 dp[i+1][j-1] 的结果(s[i+1…j-1]中最长回文子序列的长度),是否能想办法算出dp[i][j]的值(s[i…j]中,最长回文子序列的长度)呢?

可以!这取决于s[i]和s[j]的字符:

  • 如果它俩相等,那么它俩加上s[i+1…j-1]中的最长回文子序列就是s[i…j]的最长回文子序列;
  • 如果它俩不相等,说明它俩不可能同时出现在s[i…j]的最长回文子序列中,那么把它俩分别加入s[i+1…j-1]中,看看哪个子串产生的回文子序列更长即可。

这道题定义二维 dp 是因为前后字母都要判断。

base case 以及状态转移:
在这里插入图片描述
为了保证每次计算 dp[i][j] ,左、下、左下三个方向的位置已经被计算出来,只能斜着遍历或者反着遍历:
在这里插入图片描述

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

最长递增子序列

摘自:labuladong,原题链接:力扣300

设计动态规划的通用技巧:数学归纳思想。比如我们想证明一个数学结论,那么我们先假设这个结论在 k<n 时成立,然后想办法证明 k=n 的时候此结论也成立。如果能够证明出来,那么就说明这个结论对于 k 等于任何数都成立。

  • 类似的,设计动态规划算法,不是需要一个 dp 数组吗?我们可以假设 dp[0…i−1] 都已经被算出来了,然后问自己:怎么通过这些结果算出dp[i] ?

  • 直接拿最长递增子序列这个问题举例,首先要定义清楚 dp 数组的含义,即 dp[i] 的值到底代表着什么?

  • 定义是这样的:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。

  • 根据这个定义,最终结果(子序列的最大长度)应该是 dp 数组中的最大值。

其实到这里我就会了,下面思考的就是状态转移了。这道题就是每到一个节点遍历一遍前面的就好,所以时间复杂度就是O(n^2)了。再有就是细节问题,base case,需要把dp数组初始化填充为1。
在这里插入图片描述
总结一下动态规划的设计流程:

首先明确 dp 数组所存数据的含义。这步很重要,如果不得当或者不够清晰,会阻碍之后的步骤。

然后根据 dp 数组的定义,运用数学归纳法的思想,假设 dp[0…i−1] 都已知,想办法求出 dp[i],一旦这一步完成,整个题目基本就解决了。

但如果无法完成这一步,很可能就是 dp 数组的定义不够恰当,需要重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。

这道题,东哥还有一种二分查找的解法,叫做耐心排序。(东哥代码放下面了)
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-月光光-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值