题目来源:https://leetcode-cn.com/problems/next-greater-element-ii/
题目描述
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
输入: “aab”
输出:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]
题目大意
- 看到题目的第一眼可能可以想到跟dp会有点关系,动态规划是处理子问题常见的方法之一,把问题分为一个个若干的子问题再进行求解
- 可以思考一下,假设当某一子串已经是回文串,此时若将子串进行扩张,扩张后的子串是回文串的充要条件是子串是回文串且子串的两个端点字符相等(此处无需用双指针重新比较回文串,是因为原子串已满足)
- 状态方程如下,当i≥j的时候为True,此处其实可以不看i>j的情况,从最小的子问题,单个字符(s[0]或s[s.length()-1]都可以)组成的字符串为回文串开始处理,再逐渐扩展到F(0,s.length())
F ( i , j ) = { T r u e i ≥ j F ( i + 1 , j − 1 ) & & ( s [ i ] = = s [ j ] ) i < j F(i,j)= \left \{ \begin{array} {c}True{\quad}i≥j \\F(i + 1, j - 1)\&\&(s[i]==s[j]){\quad}i<j \end{array} \right. F(i,j)={ Truei≥jF(i+1,j−1)&&(s[i]==s[j])i<j
回溯法介绍
-
搬一下其他的题解
-
回溯法是一种算法思想,而递归是一种编程方法,回溯法可以用递归来实现。
-
回溯法的整体思路是:搜索每一条路,每次回溯是对具体的一条路径而言的。对当前搜索路径下的的未探索区域进行搜索,则可能有两种情况:
-
当前未搜索区域满足结束条件,则保存当前路径并退出当前搜索;
当前未搜索区域需要继续搜索,则遍历当前所有可能的选择:如果该选择符合要求,则把当前选择加入当前的搜索路径中,并继续搜索新的未探索区域。
上面说的未搜索区域是指搜索某条路径时的未搜索区域,并不是全局的未搜索区域。
回溯法+动态规划
- 有了上述的状态方程,则可以用动态规划对字符串进行一个预处理,然后再通过dfs遍历的方式对字符串进行遍历(表面遍历F(i,j),实质是对字符串划分所有子串再判断当前字符串是否满足回文串的条件(此处有了动态规划的预处理,判断条件时间复杂度O(1)))
- 对字符串划分所有子串的递归法(dfs)可见这篇文章,原理上是一样的https://blog.csdn.net/lr_shadow/article/details/113934454
class Solution {
public:
vector<vector<string>> ans;
vector<vector<int>> f;
vector<string> ret;
int n;
void dfs(const string& s, int i){
if(i == n){
ans.push_back(ret);
return;
}
for(int j = i ; j < n; ++j){
if(f[i][j]){
ret.push_back(s.substr(i, j - i + 1));
dfs(s, j + 1);
ret.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
n = s.length();
f.assign(n, vector<int>(n, true));
for(int i = n - 1 ; i >= 0 ; --i){
for(int j = i + 1 ; j < n ; ++j){
f[i][j] = f[i + 1][j - 1] && (s[i] == s[j]);
}
}
dfs(s, 0);
return ans;
}
};
复杂度分析
- 时间复杂度:O(n*2^n)。n为字符串的长度,最坏情况下,s包含n个完全相同的字符,总共划分有2^n-1情况,每个情况需要O(n)的时间构建
- 空间复杂度:O(n*2^n)。最坏情况下划分为2^n-1个字符串,每个都需要O(n)空间
回溯法+记忆化搜索
- 注意记忆化搜索不会遍历所有划分的子集,所以不需要进行True的初始化,维护一个二维数组记录走过的记录即可,若某次遍历当前没走过,就刷新记录,如果当前有记录,直接返回记录对应的函数值,记忆化搜索可以减少递归中时间复杂度过大的问题,减少不必要的遍历,例如经典的斐波那契问题
class Solution {
public:
vector<vector<string>> ans;
vector<vector<int>> f;
vector<string> ret;
int n;
void dfs(const string& s, int i){
if(i == n){
ans.push_back(ret);
return;
}
for(int j = i ; j < n; ++j){
if(IsPalindrome(s, i, j) == 1){
ret.push_back(s.substr(i, j - i + 1));
dfs(s, j + 1);
ret.pop_back();
}
}
}
// s.substr(i, j) string: s index: i, j
// 0 是未搜索 1 是回文串 -1 是非回文串
int IsPalindrome(const string& s, int i, int j){
// 当前已被搜索过了返回1或-1
if(f[i][j])
return f[i][j];
if(i >= j)
return f[i][j] = 1;
return f[i][j] = ((s[i] == s[j]) ? IsPalindrome(s, i + 1, j - 1) : -1);
}
vector<vector<string>> partition(string s) {
n = s.length();
// 此处不要写成vector<int>(n, true)
// 因为记忆化搜索不会遍历所有子串
f.assign(n, vector<int>(n));
dfs(s, 0);
return ans;
}
};
复杂度分析
- 时间复杂度:最坏情况下与方法一相同
- 空间复杂度:O(n*2^n)。最坏情况下与方法一相同
- 虽然最坏时空复杂度与方法一无大区别,但是大部分情况下平均时空复杂度优于方法一