🌠作者:@阿亮joy.
🎆专栏:《数据结构与算法要啸着学》
🎇座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
- 👉最少分割回文👈
- 👉编辑距离👈
- 👉不同的子序列👈
- 👉总结👈
👉最少分割回文👈
给定一个字符串 s,请将 s 分割成一些子串,使每个子串都是回文串。返回符合要求的最少分割次数 。
思路:
class Solution
{
private:
bool isPalindrome(const string& s, int begin, int end)
{
while(begin < end)
{
if(s[begin] != s[end])
{
return false;
}
++begin;
--end;
}
return true;
}
public:
int minCut(string s)
{
int len = s.size();
vector<int> minCut(len + 1);
// 初始状态:F(i) = i - 1
for(int i = 1; i <= len; ++i)
{
minCut[i] = i - 1;
}
for(int i = 2; i <= len; ++i)
{
// [1, i]整体是回文串
if(isPalindrome(s, 0, i - 1))
{
minCut[i] = 0;
continue;
}
// j < i && [j + 1, i]是否为回文串
for(int j = 1; j < i; ++j)
{
if(isPalindrome(s, j, i - 1))
{
minCut[i] = min(minCut[i], minCut[j] + 1);
}
}
}
return minCut[len];
}
};
class Solution
{
private:
bool isPalindrome(const string& s, int begin, int end)
{
while(begin < end)
{
if(s[begin] != s[end])
{
return false;
}
++begin;
--end;
}
return true;
}
public:
int minCut(string s)
{
int len = s.size();
vector<int> minCut(len + 1);
// 初始状态:F(i) = i - 1
for(int i = 0; i <= len; ++i)
{
minCut[i] = i - 1;
}
// F(0) = -1,若整个字符串为回文串,也能得出最小分割次数为0
for(int i = 2; i <= len; ++i)
{
// j < i && [j + 1, i]是否为回文串
for(int j = 0; j < i; ++j)
{
if(isPalindrome(s, j, i - 1))
{
minCut[i] = min(minCut[i], minCut[j] + 1);
}
}
}
return minCut[len];
}
};
上面的解法是 O(N ^ 3) 的算法,两层 for 循环再加一个判断回文串 O(N) 的算法,所以这个解法的时间复杂度为 O(N ^ 3)。我们可以先将回文串的结果保存下来,要用时可以直接用,这样就可以将时间复杂度将到 O(N ^ 2) 了。
判断回文串也是需要用到动态规划的。
class Solution
{
private:
vector<vector<bool>> getMat(const string& s)
{
int n = s.size();
vector<vector<bool>> Mat(n, vector<bool>(n, false));
// 从矩阵的右下角开始更新
for(int i = n - 1; i >= 0; --i)
{
for(int j = i; j < n; ++j)
{
if(j == i) // 初始状态
Mat[i][j] = true;
else if(j == i + 1)
Mat[i][j] = s[i] == s[j];
else
Mat[i][j] = ((s[i] == s[j]) && (Mat[i + 1][j - 1]));
}
}
return Mat;
}
public:
int minCut(string s)
{
int len = s.size();
vector<vector<bool>> Mat = getMat(s);
vector<int> minCut(len + 1);
// 初始状态:F(i) = i - 1
for(int i = 0; i <= len; ++i)
{
minCut[i] = i - 1;
}
// F(0) = -1,若整个字符串为回文串,也能得出最小分割次数为0
for(int i = 2; i <= len; ++i)
{
// j < i && [j + 1, i]是否为回文串
for(int j = 0; j < i; ++j)
{
if(Mat[j][i - 1])
{
minCut[i] = min(minCut[i], minCut[j] + 1);
}
}
}
return minCut[len];
}
};
该解法的时间复杂度为 O(N ^ 2),空间复杂度为 O(N ^ 2),典型的以空间换时间的做法。
👉编辑距离👈
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
思路:
class Solution
{
public:
int minDistance(string word1, string word2)
{
int row = word1.size();
int col = word2.size();
vector<vector<int>> minEdit(row + 1, vector<int>(col + 1));
// 初始状态:F(i, 0) = 0 和 F(0, j) = j
for(int i = 0; i <= row; ++i) minEdit[i][0] = i;
for(int j = 1; j <= col; ++j) minEdit[0][j] = j;
for(int i = 1; i <= row; ++i)
{
for(int j = 1; j <= col; ++j)
{
// 插入和删除
minEdit[i][j] = min(minEdit[i][j - 1], minEdit[i - 1][j]) + 1;
// 替换 word1[i - 1]是否等于word2[j - 1]
if(word1[i - 1] == word2[j - 1])
{
minEdit[i][j] = min(minEdit[i][j], minEdit[i - 1][j - 1]);
}
else
{
minEdit[i][j] = min(minEdit[i][j], minEdit[i - 1][j - 1] + 1);
}
}
}
return minEdit[row][col];
}
};
👉不同的子序列👈
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个子序列是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
思路:
class Solution
{
public:
int numDistinct(string s, string t)
{
int row = s.size();
int col = t.size();
vector<vector<unsigned int>> dp(row + 1, vector<unsigned int>(col + 1, 0));
for(int i = 0; i <= row; ++i) dp[i][0] = 1; // 初始状态
for(int i = 1; i <= row; ++i)
{
for(int j = 1; j <= col; ++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[row][col];
}
};
空间复杂度优化
由于当前的dp[i][j]
仅取决于dp[i - 1][j - 1]
和dp[i - 1][j]
,所以我们不需要用二维数组将所有状态的解都保留下来,可以用一维数组将上一次的解保留下来就行了。但需要注意的是,需要从后向前更新,不然无法拿到上一次的解。
class Solution
{
public:
int numDistinct(string s, string t)
{
int row = s.size();
int col = t.size();
// 初始状态
vector<unsigned int> dp(col + 1, 0);
dp[0] = 1;
for(int i = 1; i <= row; ++i)
{
for(int j = col; j >= 1; --j)
{
if(s[i - 1] == t[j - 1])
dp[j] = dp[j - 1] + dp[j]; // dp += dp[i - 1]
// s[i - 1] != t[j - 1]时,不需要更新dp[j]
}
}
return dp[col];
}
};
👉总结👈
动态规划的难点:
- 状态如何定义:从问题中抽象出状态,每一个状态都对应着一个子问题。状态的定义可能不止一种方式,那如何定义状态的合理性呢?某一个状态的阶或者多个状态的解能否推出最终问题的解,也就是状态之间能够形成递推关系(状态转移方程)。
- 一维状态 VS 二维状态:首先尝试一维状态,如果一维状态的合理性不满足时,再去尝试二维状态。
- 常见问题的状态:字符串:状态一般对应子串,状态一般每次增加一个新的字符。二维状态有时候能够优化成一维状态。
那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️