一、最长公共子序列 / 子串
1、最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <algorithm>
using namespace std;
//最长公共子序列
//DP
//长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
void longestCommonSubsequence(string& s1, string& s2, int &res) {
vector<vector<int>> dp(s1.size() + 1, vector<int>(s2.size() + 1, 0));
for (int i = 1; i <= s1.size(); i++) {
for (int j = 1; j <= s2.size(); j++) {
if (s1[i - 1] == s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1; //增加长度
}
else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); //其中一个字符串少一个字母的最大值
}
}
}
res = dp[s1.size()][s2.size()];
//return dp[s1.size() - 1][s2.size() - 1];
}
int main(int argc, char* argv[]) {
string s1 = "";
string s2 = "";
getline(cin, s1);
getline(cin, s2);
int res = 0;
longestCommonSubsequence(s1, s2, res);
cout << res << endl;
return 0;
}
2、最长公共子串
查找两个字符串a,b中的最长公共子串。若有多个,输出在较短串中最先出现的那个。
注:子串的定义:将一个字符串删去前缀和后缀(也可以不删)形成的字符串。请和“子序列”的概念分开!
#include <bits/stdc++.h>
using namespace std;
//dp
//dp[i][j]表示 到s1第i个,到s2第j个为止 的公共子串长度 (其中s1较短)
void longestCommonSubsequence(string s1, string s2, string& res) {
vector<vector<int>> dp(s1.size() + 1, vector<int>(s2.size() + 1, 0));
int maxLength = INT_MIN, end = 0; //end表示字符串的末位位置 (最大不超过s1的长度)
for(int i = 1; i <= s1.size(); i++){
for(int j = 1; j <= s2.size(); j++){
if(s1[i - 1] == s2[j - 1]){
dp[i][j] = dp[i - 1][j - 1] + 1; //则增加长度
}
else{
dp[i][j] = 0; // //该位置为0
}
if(dp[i][j] > maxLength){ //更新最大长度
maxLength = dp[i][j];
end = i - 1; //
}
}
}
res = s1.substr(end - maxLength + 1, maxLength); //
}
int main(){
string s1 = "";
string s2 = "";
getline(cin, s1);
getline(cin, s2);
if(s1.length() > s2.length()){
swap(s1, s2); //使较小的字符串在前
}
string res = "";
longestCommonSubsequence(s1, s2, res);
cout << res << endl;
return 0;
}
二、KMP算法
求解一个字符串m在另一个字符串s中出现的首个索引位置使用kmp算法。详细注释见代码。
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
using namespace std;
//返回数组的首地址
//nextArray的第i个值是从0 -- i-1长的字符串中 的最长相等前后缀的长度
int* getNextArray(string m) {
int* nextArray = new int[m.size()];
if(m.size() == 1){
nextArray[0] = -1;//人为规定为-1
return nextArray;
}
nextArray[0] = -1;//人为规定为-1
nextArray[1] = 0;//人为规定为0
int cn = 0;//cn表示哪个字符在和i - 1 位置的字符进行比较(前缀的下一个位置);也表示当前使用的信息的长度是多少
for (int i = 2; i < m.size();) {
//根据nextArray[i-1]计算nextArray[i]
if (m[i - 1] == m[cn]) {
/*nextArray[i] = cn + 1;
i++;
cn++;*/
nextArray[i++] = ++cn;
}
else if (cn > 0) { //m[i - 1] != m[cn]
cn = nextArray[cn]; //cn往前跳
}
else { //cn不能再往前跳
nextArray[i++] = 0; //没有相等的前后缀 nextArray[i] = 0
}
}
return nextArray;
}
int kmpGetIndexOf(string &s, string &m) {
if (s.size() == NULL || m.size() == NULL || m.size() < 1 || s.size() < m.size()) {
return -1;
}
int i = 0, j = 0;
int *nextArray = getNextArray(m);//拿到next数组
while (i < s.size() && j < m.size()) {
if (s[i] == m[j]) {
i++;
j++; //只有匹配相等j才自加 如果越界则匹配成功
}
else if (nextArray[j] == -1) { //else if(j == 0) //j无法再向前跳
i++; //s的下标加1
}
else {
j = nextArray[j];//j跳回到next数组中对应的位置
}
}
return j == m.size() ? i - j : -1; //匹配成功返回两个指针的差值
}
//返回m在s中出现的第一个索引位置
int main(int argc, char* argv[]) {
string s = ""; string m = "";
char s_c, s_m;
getline(cin, s);
getline(cin, m);
int res = 0;
res = kmpGetIndexOf(s, m);
cout << res << endl;
return -1;
}
三、有关字符串句子的单词反转问题
先把句子中所有字符串取出放入字符串数组,再对数组中的字符串进行操作后重新连接即可,具体问题具体细节还需要按题目要求分析
而遍历句子取字符串的思路,就是遇到字符把它放入临时字符串,遇到空格或者标点(如果有标点),就把临时字符串输出,并且清空。
以下代码中的res即为字符串数组,求解res的过程即是模板。
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <algorithm>
using namespace std;
void reverseWords(string& s1, string& s2) {
s1 += ' '; //补一个空格,避免最后一个单词无法取出
string tmp = "";//存储单个单词的临时字符串
vector<string> res;//存储每个单词的字符串数组
for (char ch : s1) {
if (ch == ' ') { //如果是空格
if (!tmp.empty()) { //如果s1的前后有空格 要这行 否则不需要
res.push_back(tmp);
tmp.clear();
}
}
else {//如果不是空格
tmp += ch;
}
}
//到这里 res中存储的是每个单词
reverse(res.begin(), res.end()); //反转
for (auto str : res) {
s2 += str + ' '; //重新拼接为反转后的字符串句子
}
s2.pop_back(); //将最后一个空格推出去
//return s2;
}
int main(int argc, char* argv[]) {
string s1 = "";
string s2 = "";
getline(cin, s1);
reverseWords(s1, s2);
cout << s2 << endl;
return 0;
}
四、子序列判断问题
1、判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1: 输入:s = “abc”, t = “ahbgdc” 输出:true
示例 2: 输入:s = “axc”, t = “ahbgdc” 输出:false
class Solution {
public:
//动态规划
//dp[i][j]表示s中以下标i-1为结尾的字符串 和 t中以下标j-1为结尾的字符串 相等子序列的长度
bool isSubsequence(string s, string t) {
vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
for(int i = 1; i <= s.size(); i++){
for(int j = 1; j <= t.size(); j++){
if(s[i - 1] == t[j - 1]){
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else{
dp[i][j] = dp[i][j - 1];//相当于t要删除元素,t如果把当前元素t[j-1]删除,那么dp[i][j] 的数值就是 看s[i-1]与 t[j-2]的比较结果了
}
}
}
//如果dp[s.size()][t.size()] 与 字符串s的长度相同说明:s与t的最长相同子序列就是s,那么s 就是 t 的子序列
if(dp[s.size()][t.size()] == s.size()) return true;
return false;
}
};
2、不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
//dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
class Solution {
public:
int numDistinct(string s, string t) {
vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); j++) {
if (s[i - 1] == t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[s.size()][t.size()];
}
};
五、两个字符串的删除操作
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
输入: “sea”, “eat”
输出: 2 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
//除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size()+1, vector<int>(word2.size()+1, 0));
for (int i=1; i<=word1.size(); i++){
for (int j=1; j<=word2.size(); j++){
if (word1[i-1] == word2[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 word1.size()+word2.size()-dp[word1.size()][word2.size()]*2;
}
};
六、有关回文串的问题
1、验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
class Solution {
public:
bool isPalindrome(string s) {
string sgood; //存储转换好可以进行比较的字符串
for (char ch: s) {
if (isalnum(ch)) { //如果是字母或者数字(排除空格)
sgood += tolower(ch);//大写字母转换为小写字母 //方便比较
}
}
//双指针 挨个字母进行比较
int n = sgood.size();
int left = 0, right = n - 1;
while (left < right) {
if (sgood[left] != sgood[right]) {
return false;
}
++left;
--right;
}
return true;
}
};
2、回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:“abc” 输出:3 解释:三个回文子串: “a”, “b”, “c”
示例 2:
输入:“aaa” 输出:6 解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”
提示:
输入的字符串长度不会超过 1000 。
//dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));//初始化全为false
int result = 0;
//一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i; j < s.size(); j++) {//j>=i 只填充dp数组的右上半部分
if (s[i] == s[j]) {
if (j - i <= 1) { //必是回文子串
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { //[i, j]区间长度大于1
result++;
dp[i][j] = true;
}
}
}
}
return result;
}
};
3、最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1: 输入: “bbbab” 输出: 4 一个可能的最长回文子序列为 “bbbb”。
示例 2: 输入:“cbbd” 输出: 2 一个可能的最长回文子序列为 “bb”。
提示:
1 <= s.length <= 1000
s 只包含小写英文字母
//dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。
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;//一个字符的回文子序列长度就是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; //最两边相等 长度加2
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);//最两边不相等,分别加其中一个,所构成的最长回文子序列长度的最大值
}
}
}
return dp[0][s.size() - 1];
}
};
4、最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
class Solution {
public:
//dp[i][j]表示s中[i, j]区间是否是回文子串
string longestPalindrome(string s) {
if(s.size() < 2) return s;
string res = "";
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
for(int i = 0; i < s.size(); i++) dp[i][i] = true;//对角线
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 < 3) dp[i][j] = true; // //2个字符-则这两个字符必相等 1个字符-也是回文子串
else dp[i][j] = dp[i + 1][j - 1];//区间大于2个字符 由左下角的bool类型决定(收缩区间)
}
if(dp[i][j] && (j - i + 1) > res.size()){ //取最长回文子串
res = s.substr(i, j - i + 1);
}
}
}
return res;
}
};
5、最多删除一个字符得到回文
给定一个非空字符串 s,请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。
class Solution {
public:
bool check(const string& s, int low, int high){
while(low < high){
if(s[low] != s[high]){
return false;
}
low++;
high--;
}
return true;
}
bool validPalindrome(string s) {
int low = 0, high = s.size() - 1;
while(low < high){
char l = s[low], h = s[high];
if(l == h){
low++;
high--;
}
else{
return check(s, low, high - 1) || check(s, low + 1, high); //
}
}
return true;
}
};
七、有关字符串匹配的问题
1、剑指 Offer 48. 最长不含重复字符的子字符串
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
class Solution {
public:
//滑动窗口
int lengthOfLongestSubstring(string s) {
int res = 0;
unordered_set<char> st;
int right = 0;
for(int i = 0; i < s.size(); i++){ //i为左边界
if(i != 0){
st.erase(s[i - 1]);
}
while(right < s.size() && st.find(s[right]) == st.end()){
st.insert(s[right]);
right++;
}
res = max(res, right - i);
}
return res;
}
};
2、滑动窗口模板
滑动窗口可用于解决一些列的字符匹配问题,典型的问题包括:在字符串s 中找到一个最短的子串,使得其能覆盖到目标字符串 t。对于目标字符串 t,我们可以在字符串 s 上滑动窗口,当窗口包含 t 中的全部字符后,我们再根据题意考虑能否收缩窗口。模板如下:
class Solution {
public:
string minWindow(string s, string t) {
if(s.size() < t.size()) return "";
//哈希表:记录还需要匹配到的 各个元素 的数目 (如果m[ch] < 0说明当前ch过多,不再需要了)
unordered_map<char, int> m;
for(int i = 0; i < t.size(); i++){
m[t[i]]++;
}
int need = t.size(); //记录还需要匹配到的字符总数【need=0表示匹配到了】
int start = 0, end = -1; //记录目标子串s[start, end]的起始和结尾 (res) //end = -1 --> 当for循环不执行时返回空字符串
int minLength = s.size() + 1; //符合题意的最短子串长度【初始化为一个不可能的较大值】
int left = 0, right = 0; //滑动窗口的左右边界
for(; right < s.size(); right++){
char ch = s[right]; //窗口中新加入的字符 (右边界的字符)
if(m.find(ch) != m.end()){
if(m[ch] > 0){ //对当前字符ch还有需求
need -= 1; //此时新加入窗口中的ch对need有影响
}
m[ch]--; //
}
//窗口左边界持续右移
while(need == 0){ //need=0,当前窗口完全覆盖了t
if(right - left + 1 < minLength){
minLength = right - left + 1;
start = left; end = right;
}
char ch = s[left]; //窗口中要滑出的字符
if(m.find(ch) != m.end()){
if(m[ch] >= 0){ //对当前字符ch还有需求,或刚好无需求(其实此时只有=0的情况)
need += 1; //此时滑出窗口中的ch对need有影响
}
m[ch]++; //
}
left++; //窗口左边界+1
}
}
return s.substr(start, end - start + 1);
}
};