目录
分割回文串
题干
题目:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
说明:s 仅由小写英文字母组成。
思路和代码
回文串是指一个字符串,其从前往后读和从后往前读是完全相同的。也就是说字符串顺序和逆序都是相同的。那么首先要有一个函数判断字符串是否为回文串。
递归法
递归法的思路是先确定组合的第一个回文串,之后再递归从剩余的字符串中分割回文串。
Q:分隔符是什么?分隔符如何每次都重新遍历?
分隔符其实就是 startIndex,让分隔符从前往后遍历,每次往后移动一个字符,再判断是否是回文串。
-
递归参数和返回值:参数是字符串 s,需要分割的起始位置 startIndex;无返回值。
-
递归结束条件:当遍历到整个字符串的末尾时,说明找到了一个组合,此时递归结束返回。
-
递归顺序:先确定组合的第一个回文串,之后再递归从剩余的字符串中分割回文串,将分割好的回文串插入组合中,若组合已经填满(即字符串遍历完),则回溯到上一层重新分割字符串。
class Solution {
public:
// 判断是否为回文串,两个指针分别从头和从末尾遍历
bool isPalindrome(string &s, int start, int end){
while (start <= end){
if (s[start] == s[end]){
start++;
end--;
} else{
return false;
}
}
return true;
}
vector<string> composition;
vector<vector<string>> result;
// 分割字符串,也是回溯算法的实现
void splitString(string &s, int startIndex){
if (startIndex >= s.size()){
// 分割到最后一个字符,说明字符串已经遍历完毕
result.push_back(composition);
return;
}
// 用 i 遍历子串的末尾
// startIndex固定子串的起始位置
for (int i = startIndex; i < s.size(); ++i) {
// 如果当前子串是回文串,加入组合中
if (isPalindrome(s,startIndex,i)){
string sub = s.substr(startIndex,i-startIndex+1);
composition.push_back(sub);
} else{
// 如果不是回文串,让末尾指针继续往后遍历
continue;
}
// 找出一个当前回文串后,要在后续的字符串中继续分割子串
splitString(s,i+1); // 后续字符串的起点为当前子串末尾的下一个元素
composition.pop_back();
}
}
vector<vector<string>> partition(string s) {
composition.clear();
result.clear();
splitString(s,0);
return result;
}
};
复原 IP 地址
题干
题目:有效 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 中的任何数字。你可以按 任何 顺序返回答案。
思路和代码
这道题和分割回文串有共同点,分割回文串需要找满足回文串的子串,而复原 IP 地址需要在字符串中满足IP地址格式的子串。不同点在于这道题不仅要做分割,还需要在分割的子串之间插入小数点 "."。
我们可以用一个整型数组 “ vector<int> ip ” 收集满足 IP 地址的四个整数,等需要插入结果集时,再把整数转化为字符串,并在每个整数之间插入小数点 “.”。
-
递归参数和返回值:参数是传入的字符串和分割的起始位置;无返回值。
-
递归结束的条件:
-
已经收集好四个整数但是字符串却还没遍历完,说明字符串过长,直接返回。
-
当 startIndex 已经遍历到字符串末尾,且四个整数已收集完毕,则插入结果集,并返回。
-
-
递归顺序:先获取当前分割好的子串并转化为整数,判断其是否满足 IP 地址要求,如果不是则直接返回;如果满足,则递归分割剩余的字符串。
Q:为什么在这道题中不满足IP地址要求就直接 return,而不是 continue?
因为题目要求 IP地址 必须是字符串中连续的子串,子串不能是间断的元素,所以直接 return。
class Solution {
public:
vector<int> ip; // 记录ip地址的四个整数
string composition;
vector<string> result;
void splitString(string &s, int startIndex){
if (ip.size() == 4 && startIndex != s.size()){
// 已经收集好四个整数但是字符串却还没遍历完,不满足条件,直接返回
return;
}
if (startIndex >= s.size()){ // 已经遍历完整个字符串,需要将4个整数插入字符串组合中。
composition.clear(); // 每次插入Ip地址前清空组合
if (ip.size() == 4){ // 4个整数都收集完才插入结果,否则直接返回
for (int i = 0; i < ip.size(); ++i) {
composition += to_string(ip[i]);
composition.push_back('.');
}
composition.pop_back(); // 去掉最后一个小数点
result.push_back(composition);
}
return;
}
for (int i = startIndex; i < s.size(); ++i) {
string sub = s.substr(startIndex,i-startIndex+1); // 获取当前分割的子串
int num = stoi(sub); // 将子串转为整数
// ... 判断是否满足 IP 地址需求
if (num >= 0 && num <= 255){ // 整数必须在 0~255范围内
ip.push_back(num); // 满足要求插入 ip 数组中
} else{
return; // 不满足 IP 地址条件的直接返回
}
// ...继续分割后续的字符串
splitString(s,i+1);
ip.pop_back(); // 回溯,弹出一个整数
if (num == 0) return; // 如果数字为 0,0不可以作为前导,所以不需要再往后循环遍历,直接返回
}
}
vector<string> restoreIpAddresses(string s) {
if (s.length() < 4 || s.length() > 12){
// 剪枝,IP地址的长度最小是 4 个整数,最大是 12 个整数
return {};
}
splitString(s,0);
return result;
}
};
子集(非重复)
题干
题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。注意,子集包括了空集。
思路和代码
方法一:修改子集大小
设集合大小为 k,这道题就是在集合中找出 以 0 ~ k 为子集大小 的组合。
递归方法定义为找大小为 size 的组合,在找子集方法中循环递归不同 size 大小。
class Solution {
public:
vector<int> composition;
vector<vector<int>> result;
void findCompositions(vector<int> &nums, int startIndex, int size){
if (composition.size() == size){ // 当组合满足子集大小时,插入结果
result.push_back(composition);
return;
}
for (int i = startIndex; i < nums.size(); ++i) {
composition.push_back(nums[i]);
findCompositions(nums,i+1,size); // size 没有取地址,所以不需要回溯
composition.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
result.push_back({}); // 插入空集
for (int i = 1; i <= nums.size(); ++i) { // 循环设置不同的子集大小
findCompositions(nums,0,i);
}
return result;
}
};
方法二:收集树的每个节点
在之前找组合的过程中,我们都是收集树的叶子节点,因为题目很明确地告知组合的要求。而在找子集的过程中,组合的大小没有限制,如果将寻找子集的过程抽象为一棵树,则寻找子集就是寻找树中的每一个结点。
class Solution {
public:
vector<int> composition;
vector<vector<int>> result;
void backTrack(vector<int> &nums, int startIndex){
// 初始时,composition为空集,所以第一次进来的时候就已经插入了空子集
result.push_back(composition); // 每层递归的组合都要保存
if (startIndex >= nums.size()){
return; // 结束条件其实不加也可以
}
for (int i = startIndex; i < nums.size(); ++i) {
composition.push_back(nums[i]);
backTrack(nums,i+1);
composition.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backTrack(nums,0); // 此时不用特意插入空集
return result;
}
};
子集Ⅱ(含重复)
题干
题目:给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
思路和代码
方法一:先去重,记录每个元素重复的次数
用 unordered_map 记录数组中的每个元素以及元素的重复使用次数,用新数组 newNums 存储去重后的数组。
class Solution {
public:
unordered_map<int,int> numsCount; // 记录不同数字的重复次数
vector<int> composition;
vector<vector<int>> result;
void backTrack(vector<int> &nums, int startIndex){
if (startIndex >= nums.size()){
return;
}
for (int i = startIndex; i < nums.size(); ++i) {
composition.push_back(nums[i]);
result.push_back(composition);
numsCount[nums[i]]--; // 使用次数减少
if (numsCount[nums[i]] > 0){
backTrack(nums,i); // 如果使用次数还有,递归传入自身 i
} else{
backTrack(nums,i+1); // 如果使用次数已经为 0,要传入下一个元素 i+1
}
composition.pop_back();
numsCount[nums[i]]++;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<int> newNums; // 记录去重后的数组
for (int i : nums) {
numsCount[i]++; // 记录不同元素重复的次数
if (numsCount[i] == 1){
newNums.push_back(i);
}
}
backTrack(newNums,0);
result.push_back({}); // 插入空集
return result;
}
};
方法二:先排序,再树层去重
方法二的思路都是对树层去重(即在同一层 for 循环中去重),实现方法有两种,分别使用集合 set 或使用数组 isUsed 来辨别重复的元素是在树层中还是在树枝中,如果是在树层中,则去重。要注意的是,使用方法前都需要对数组进行排序,让相同的元素排在一起,才方便后续的去重。
Q:为什么递归不用去重?而要对 同一层 for 循环去重?
因为递归实际上改变的是元素在子集中填充的位置,元素值虽相同但位置不同,代表的也是不同的子集。
而在同一层 for 循环中遍历的都是子集中的同一个位置,元素值当然要不同!
使用 unordered_set 对树层去重
我们需要对同一层 for 中出现过的重复元素进行去重,那么用一个集合记录在 for 循环中遍历过的元素,如果后续循环又出现了集合记录过的元素,则说明重复,跳过。
class Solution {
public:
vector<int> composition;
vector<vector<int>> result;
void backTrack(vector<int> &nums, int startIndex){
result.push_back(composition);
if (startIndex >= nums.size()){
return; // 递归条件可以不写
}
unordered_set<int> uSet; // 记录同一层 for 循环中遍历过的元素
for (int i = startIndex; i < nums.size(); ++i) {
if (uSet.find(nums[i]) != uSet.end()) continue; // 之后可能还有别的元素,所以是 continue
composition.push_back(nums[i]);
uSet.insert(nums[i]);
backTrack(nums,i+1);
composition.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end()); // 先对数组进行排序,让重复元素相邻排在一起
backTrack(nums,0);
return result;
}
};
使用 bool 型数组对树层去重
用 bool 型数组 isUsed[ ] 记录每个数组中的元素是否有被使用过。如果当前元素和前一个元素相同,且前一个元素没有被使用,说明当前元素和前一个元素不在递归中,而是在同一层 for 循环中,即在树层而非树枝,需要去重,跳过。
class Solution {
public:
bool isUsed[11]; // 记录数组每个元素的使用状态,默认为 false, 1 <= nums.length <= 10
vector<int> composition;
vector<vector<int>> result;
void backTrack(vector<int> &nums, int startIndex){
result.push_back(composition);
if (startIndex >= nums.size()) return;
for (int i = startIndex; i < nums.size(); ++i) {
if (i > 0 && nums[i] == nums[i-1] && !isUsed[i-1]){
// 如果当前元素和前一个元素相同,且前一个元素没有被使用
// 说明当前元素和前一个元素不在递归中,而是在 for 循环中,即在树层而非树枝,需要去重,跳过
continue;
}
composition.push_back(nums[i]);
isUsed[i] = true; // 修改当前元素的使用状态
backTrack(nums,i+1);
composition.pop_back();
isUsed[i] = false;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end()); // 先对数组进行排序,让相同元素排在一起
backTrack(nums,0);
return result;
}
};
回溯算法难点
-
要知道 for 循环遍历的是什么?for 循环确定了组合中的什么?
-
递归改变了什么?
-
去重是对树枝去重还是对树层去重?去重前是否需要排序?