Given a string s, partition s such that every substring of the partition is a palindrome.
Return all possible palindrome partitioning of s.
For example, given s = "aab"
,
Return
[ ["aa","b"], ["a","a","b"] ]
本题同Word Break II有异曲同工之妙,对单词进行划分。题目涉及到递归、分治、回溯、DFS、DP、二维迭代这些知识,相当经典,下面详细分析以总结最近的算法练习。
如何下手:先考虑普通的分割思路,以【ababaaaa】为例来说明。
第一步找出所有以a开头的回文做分割,得到1.【a babaaaa】、2.【aba baaaa】、3.【ababa aaa】;
接着处理1.【a babaaaa】中的子串【babaaaa】,对其做开头的回文分割...递归过程一直处理下去
接着处理2.【aba baaaa】中的子串【baaaa】,对其做开头的回文分割...递归过程一直处理下去
接着处理3.【ababa aaa】中的子串【aaa】,对其做开头的回文分割...递归过程一直处理下去
不难看出这是一个递归的过程,而且是一个深度遍历的过程。每做一次头部回文分割,得到的是一颗子树,需要对这颗子树进行遍历。由此可以轻松写出DPS算法。 设ret保存最后结果,item代表其中某一种回文分割, index表示子串的其实位置:调用DFS(s, 0, item)
void DFS(s, index, item) {
if(index == s.size()) {
ret += item;
}
for(i = index; i < s.size(); i++) {
if(s[index~i]为回文) {
item += s[index~i];
DFS(s, i+1, item);
item -= s[index~i];
}
}
}
DFS解释:DFS首先为递归过程,先考虑for循环,它代表了对树形结构所有子树的访问,即
1.【a babaaaa】、2.【aba baaaa】、3.【ababa aaa】这三种以a开头做回文分割的情况。边界条件为,当访问的字符到达s末尾位置时,表示分割完成。
这里有一点需要解释,for循环中的 i 采用递加的方式去遍历以s[index]开头的回文,而人的思考方式看起来并不是这样。人的思考方式是“跳跃”的,好像在 【ababaaaa】一下子就找到了a、aba、ababa三个开头回文,并且能判断ababaaaa不是回文。而实际上这种工作计算机去执行的时候,必须顺序扫描访问。PS:人有可以先有感觉,然后做出理性判断;机器的只能机械的一步一步做判断。不然我们可以写成for(X in s[index~n]) ,其中X为开头回文。
每一次去做s[index~i]为回文的判断很耗时,同时其中也有重复的计算问题:S[i~j]是否为回文可以通过判断S[i]和S[j]以及S[i+1, j-1]是否为回文来判断。设pal[i][j]记录S[i~j]是否为回文,现在考虑提前计算出任意 i 位置到 j 位置是否为回文。分析可知这里可以用分治法考虑,即S[i~j]与S[i+1, j-1]关系,同时其中的冗余问题让我们想到了用DP可以节省计算时间。
分析S[i~j]与S[i+1, j-1]:
分治问题可以用递归和迭代两种,如果是递归考虑问题时从大问题到小问题考虑,如果是迭代应该从小问题到大问题逐步增大规模。现在考虑用迭代方法从边界在是出发。若计算S的某一种分词情况,最后会逐步考虑到最后一个字母,迭代从最后一个字母开始 for(i = len -1; i >=0; i++) {s[i]...},其中子问题为S[i~n]中所有的pal[i][j]其中 j 范围为[i, n-1]。
我们考虑i = 10时,需要计算出所有10到n的回文判断,即pal[10][10] 、pal[10][11] 、pal[10][12] 、... 、pal[10][n-1]。
我们考虑到i = 9时,需要计算出所有9到n的回文判断,即pal[9][9] 、pal[9][10] 、pal[9][11] 、... 、pal[9][n-1]。
由于i = n-1的情况是很容易知晓的,那么得出i = n - 2的情况也很容易。也就是说计算i = 9时,提前知道了i = 10的情况,i= 9回文判断可以借助i = 10计算出的结果。 j 的范围可以从 i 取到 n -1, 对于每一个j 都有:pal[i][j]的判断需要依靠pal[i+1][j-1]
由此二维的迭代很容易写出来。边界细节可以仔细考虑。
for(i = len - 1; i >=0; i++) {
for(j = i; j < len; j++) {
if(s[i] = s[j] && (j - i < 2 || s[i+1][j-1])) {
pal[i][j]
}
}
}
二维迭代似乎难理解,其实也可以看成是一维迭代。要计算出S[i~n]的所有情况,先得计算出S[i+1 ~ n ]的情况。而其中前者的S[i~j]又依赖于后者的S[i+1, j-1]。
至此,上述文字记录了从无动态规划的递归考虑,到加入动态规划结果的递归考虑,表明了DP并不是空穴来风,凭空设想出来的,一定是由问题的需求考虑出来的:问题是否可以大化小,是否有冗余问题。同时解释了二维迭代的过程,现将二维迭代看成一维迭代能更容以理解问题。
class Solution {
vector<vector<string>> retVString;
bool palin[1500][1500];
public:
vector<vector<string>> partition(string s) {
// Start typing your C/C++ solution below
// DO NOT write int main() function
if(s.size() == 0)
return vector<vector<string>>();
int leng = s.size();
for(int i = 0; i < leng; i++)
for(int j = 0; j < leng; j++)
palin[i][j] = false;
for(int i = leng-1; i >= 0; i--){
for(int j = i; j < leng; j++){
if(s[i] == s[j] && (j-i<2 || palin[i+1][j-1])){
palin[i][j] = true;
}
}
}
retVString.clear();
dfs(s, 0, vector<string>());
return retVString;
}
void dfs (string& s, int start, vector<string> palinStr)
{
if(start == s.size())
{
retVString.push_back(palinStr);
}
for(int i = start; i < s.size(); i++)
{
if(palin[start][i])
{
palinStr.push_back(s.substr(start, i - start + 1));
dfs(s, i+1, palinStr);
palinStr.pop_back();
}
}
}
};
其中DP的过程也可以顺着进行:
for(int i = 0; i <= leng - 1; i++){
for(int j = i; j >= 0; j--){
if(s[i] == s[j] && (i-j <2 || palin[j+1][i-1])){
palin[j][i] = true;
}
}
}