切割问题
一、问题特点
这类问题通常会给出一个数组(或字符串),要求列举出所有满足特定条件的划分。其特点如下:
- 通常不允许改变原集合中元素的相对顺序
- 所求答案通常是若干组满足特定条件的划分
- 划分中的所有元素必须在原集合中连续取值
二、相关例题
(一)分割回文串
131.分割回文串(LeetCode)
题目要求:
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串 。返回 s 所有可能的分割方案。
回文串是正着读和反着读都一样的字符串。
示例:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
提示:
1 <= s.length <= 16
s 仅由小写英文字母组成
解题思路:
切割问题同样是回溯算法的典型应用,其解题思路和组合问题大致相同,同样需要用for循环加递归的方法遍历搜索树查找满足条件的解,又因为切割问题中不允许重复取值,所以仍需要startIndex标明切割的起始位置。和组合问题不同的是,切割问题每次添加到路径中的往往不是单个元素s[i],而是从[startIndex, i]索引范围内的子串。
本题中需要找出所有元素均为回文子串的划分。所以在进行添加操作前,需要先判断该段子串是否为回文串,该步骤需要重复许多次,可以通过设置一个函数bool isPalindrome(string s){}利用双指针法实现判断,但由于问题中每一次取子串都需要检查,故调用次数过多,影响程序运行效率。为了进行优化,可以设一个二维数组vector<vector> isP先将字符串s中所有可能的子串是否为回文串的情况记录下来,当需要判断时查找二维数组即可。后者只需要遍历判断一次,效率更高。
代码示例:
// 优化后的代码
class Solution {
public:
void isPalindrome(string s) {
// 判断字符串s中的所有子串是否为回文串并将结果存入isP[][]中
// 根据s的大小刷新数组大小
isP.resize(s.size(), vector<bool>(s.size(), false));
// 子串[i, j]是回文串的充要条件是s[i] = s[j] && [i+1, j-1]是回文串
for(int i = s.size() - 1; i >= 0; i--) {
// 反向遍历,确保在判断[i, j]时,[i+1, j-1]已被判断
for(int j = i; j < s.size(); j++) {
if(i == j) isP[i][j] = true;
else if(j - i == 1) isP[i][j] = (s[i] == s[j]);
else {
isP[i][j] = (isP[i+1][j-1] && s[i] == s[j]);
}
}
}
return;
}
void backtracking(const string s, int startIndex) {
// startIndex是分割线
// [0, startIndex - 1]是已被分割的部分
if(startIndex >= s.size()) {
// 本路径分割完毕,存入结果数组中
ans.push_back(path);
return;
}
for(int i = startIndex; i < s.size(); ++i) {
if(!isP[startIndex][i]) continue;
// 确认[startIndex, i]是回文子串,准备取出
string temp = s.substr(startIndex, i - startIndex + 1);
path.push_back(temp);
backtracking(s, i + 1); // 纵向遍历
path.pop_back();
}
return;
}
vector<vector<string>> partition(string s) {
isPalindrome(s);
backtracking(s, 0);
return ans;
}
private:
vector<string> path;
vector<vector<string>> ans;
vector<vector<bool>> isP;
};
// 未优化的代码
class Solution {
public:
bool isPalindrome(string s, int m, int n) {
// 判断字符串s中第i个字符到第j个字符之间的子串是否是回文串
// 时间复杂度:O(n)
if(m > n || n >= s.size()) return false;
for(int i = m; i < n; ++i , --n) {
if(s[i] != s[n]) return false;
}
return true;
}
void backtracking(const string s, int startIndex) {
// startIndex是分割线
// [0, startIndex - 1]是已被分割的部分
if(startIndex >= s.size()) {
// 本路径分割完毕,存入结果数组中
ans.push_back(path);
return;
}
for(int i = startIndex; i < s.size(); ++i) {
if(!isPalindrome(s, startIndex, i)) continue;
// 确认[startIndex, i]是回文子串,准备取出
string temp = s.substr(startIndex, i - startIndex + 1);
path.push_back(temp);
backtracking(s, i + 1); // 纵向遍历
path.pop_back();
}
return;
}
vector<vector<string>> partition(string s) {
backtracking(s, 0);
return ans;
}
private:
vector<string> path;
vector<vector<string>> ans;
};
(二)复原IP地址
93.复原IP地址(LeetCode)
题目要求:
有效 IP 地址正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你不能重新排序或删除 s 中的任何数字。你可以按任何顺序返回答案。
示例:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
解题思路:
- 因为不能重复取值,所以需要startIndex计数
- 因为一定是分成四份,所以需要一个int型参数count用于纪录当前深度,当深度不等于4时结果无效,据此可以直接剪去深度大于4的分支
- 因为每一次分割出来的子串长度为[1, 3]所以可以剪去长度大于3的分支
- 要求分割后的子串不能有前导0,范围在[0, 255],可以设置一个函数判断特定子串是否满足条件。
- 注意除了第一次向path中添加子串,每一次添加前都需要先向path中添加一个"."
- 注意回溯时需要删除添加的子串和"."
代码示例:
class Solution {
public:
bool isValid(string num) {
// 判断数字是否满足有效条件
if(num.size() > 1 && num[0] == '0') return false;
int n = stoi(num);
if(n > 255 || n < 0) return false;
return true;
}
void backtracking(string s, int startIndex, int count) {
if(startIndex >= s.size()) {
// 分割完毕,将结果保存到数组ans
if(count == 4) ans.push_back(path);
return;
}else if(count == 4) return;
// 递归查找的深度一定是4
for(int i = startIndex; i < s.size(); ++i) {
// 分割[startIndex, i]
// 每一个整数的长度一定小于4
if(i - startIndex >= 3) break;
string temp = s.substr(startIndex, i - startIndex + 1);
if(!isValid(temp)) break;
// 将当前有效字段加入字符串
if(startIndex != 0) path.append(".");
path.append(temp);
backtracking(s, i + 1, count + 1);
// 回溯
if(startIndex == 0) path.erase(path.size() - temp.size(), temp.size());
else path.erase(path.size() - temp.size() - 1, temp.size() + 1);
}
return;
}
vector<string> restoreIpAddresses(string s) {
backtracking(s, 0, 0);
return ans;
}
private:
vector<string> ans;
string path = "";
};