2021/3/7。
今天的每日一题是Leetcode131. 分割回文串,题意如下:
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
首先要仔细读题,题目要求是将s分割成子串,注意理解题意。官方给的题解是回溯+动态规划,但是我看下来其实也就是用了个dfs,或者说是dfs本身就带有回溯的思想吧,反正对我来说把它当成dfs来好理解一些。dfs和回溯的相似之处就是都分为三步:做选择 -> 递归 -> 撤销选择。
我们可以对每个可以划分的地方进行暴力搜索(下称为爆搜),
引用宫水三叶大佬的一句话:
凡是求所有方案的题基本上都没有什么优化方案,就是「爆搜」。
dfs的思路是:我们只需要以首个字符为起点,枚举以其开头所有的回文串方案,加入集合,然后对剩下的字符串部分继续爆搜。就能做到以任意字符作为回文串起点进行分割的效果了。
至于为什么只需要以首字符为起点就够了,是因为每个字符它自己本身就是一个回文串,所以把它单独划出去也是一种情况,单独划出去后就不用考虑它了。
首先借鉴了负雪明烛大佬的思路,ans二维数组用来存所有可能划分的结果,path数组存当前划分情况,这种做法dfs传参传的是剩余字符串,处理起来较为方便但是判断回文串使用了暴力方法,时间复杂度过高。并且因为传递的是字符串不好使用动态规划预处理,需要优化,代码如下:
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> ans;
dfs(s,ans,{});
return ans;
}
void dfs(string s, vector<vector<string>>& ans, vector<string> path)
{
if(s.size()==0)
{
ans.push_back(path);
return ;
}
for(int i=1;i<=s.size();i++)
{
string temp=s.substr(0,i);
if(isPalindrome(temp))
{
path.push_back(temp);
dfs(s.substr(i),ans,path);
path.pop_back();
}
}
}
bool isPalindrome(string s)//判断回文,暴力方法
{
if(s.size()==0)
return true;
int start=0,end=s.size()-1;
while(start<=end) {
if (s[start] != s[end])
return false;
start ++;
end --;
}
return true;
}
};
然后学习了一手动态规划预处理判断回文串,思路是在[i,j]字符区间内:
1.当 [i, j] 只有一个字符时,必然是回文串。
2.当 [i, j] 长度为 2 时,只要这两个字符相等必然是回文串。
3.当 [i, j] 长度大于 2 时,只要左右区间各小一的子串为回文串并且左右端点字符相等时即为回文串。
注意要使用这种优化dfs传递的参数必须是下标,参考代码如下:
class Solution {
private:
bool f[20][20];
vector<vector<string>> ret;
vector<string> ans;
int n;
public:
void dfs(const string& s, int i) {
if (i == n) {
ret.push_back(ans);
return;
}
for (int j = i; j < n; ++j) {
if (f[i][j]) {
ans.push_back(s.substr(i, j - i + 1));
dfs(s, j + 1);
ans.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
n = s.size();
for (int j = 0; j < n; j++)
{
for (int i = j; i >= 0; i--)
{
if (i == j)
{
f[i][j] = true;
}
else
{
if (j - i + 1 == 2)
{
f[i][j]=s[i]==s[j];
}
else
{
f[i][j] = (s[i]==s[j] && f[i + 1][j - 1]);
}
}
}
}
dfs(s, 0);
return ret;
}
};
参考资料
·力扣官方题解
·负雪明烛《回溯法:思路与模板》
·宫水三叶《为什么只需要对第一个字符进行「爆搜」》