一、组合总和
1.题目
Leetcode:第 39 题
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates =[2,3,6,7],
target =7
输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。
示例 2:
输入: candidates = [2,3,5],
target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2],
target = 1
输出: []
2.解题思路
使用回溯算法来解决组合求和问题。backtracking 函数是一个递归函数,它尝试将每个可能的数字添加到当前路径中,并递归地继续添加下一个数字,直到找到所有可能的组合。每次递归调用时,都会检查当前路径是否满足条件(即当前和是否等于目标和),如果满足,则将其添加到结果中。combinationSum 函数是公共接口,它初始化结果和路径,然后开始递归过程。
3.实现代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 一、组合总和
class Solution1 {
public:
vector<vector<int>> result; // 用于存储所有可能组合的结果集
vector<int> path; // 用于记录当前组合的路径
// 递归函数,用于生成所有可能的组合
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum > target) { // 如果当前和已经超过目标和,递归返回,不再继续扩展当前路径
return;
}
if (sum == target) { // 如果当前和等于目标和,表示找到了一个有效的组合
result.push_back(path); // 将当前路径添加到结果集中
return; // 递归返回,不再继续扩展当前路径
}
// 从startIndex开始遍历候选数字数组
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i]; // 将当前数字添加到当前和中
path.push_back(candidates[i]); // 将当前数字添加到当前路径中
// 递归调用backtracking函数,尝试添加下一个数字
backtracking(candidates, target, sum, i);
// 回溯,从当前和中移除最后一个数字,并从当前路径中移除最后一个数字
sum -= candidates[i];
path.pop_back();
}
}
// 主函数,用于生成给定目标和的所有可能组合
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear(); // 清空之前的组合结果集
path.clear(); // 清空当前路径
backtracking(candidates, target, 0, 0); // 调用递归函数,从索引0开始生成组合
return result; // 返回所有可能的组合结果集
}
};
// 二、组合总和(剪枝优化)
class Solution2 {
public:
vector<vector<int>> result; // 用于存储所有可能组合的结果集
vector<int> path; // 用于记录当前组合的路径
// 递归函数,用于生成所有可能的组合
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum == target) { // 如果当前和等于目标和,表示找到了一个有效的组合
result.push_back(path); // 将当前路径添加到结果集中
return; // 递归返回,不再继续扩展当前路径
}
// 遍历候选数字数组,从startIndex开始,直到不能形成有效组合为止
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {//剪枝优化
sum += candidates[i]; // 将当前数字添加到当前和中
path.push_back(candidates[i]); // 将当前数字添加到当前路径中
// 递归调用backtracking函数,尝试添加下一个数字
backtracking(candidates, target, sum, i);
// 回溯,从当前和中移除最后一个数字,并从当前路径中移除最后一个数字
sum -= candidates[i];
path.pop_back();
}
}
// 主函数,用于生成给定目标和的所有可能组合
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear(); // 清空之前的组合结果集
path.clear(); // 清空当前路径
sort(candidates.begin(), candidates.end()); // 对候选数字数组进行排序,以便使用剪枝
backtracking(candidates, target, 0, 0); // 调用递归函数,从索引0开始生成组合
return result; // 返回所有可能的组合结果集
}
};
//测试
int main()
{
Solution1 s;
vector<vector<int>> result;
vector<int>candidates = { 2, 3, 6, 7 };
int target = 7;
result = s.combinationSum(candidates, target);
cout << "所有的组合有:" << endl;
for (auto & ans :result) {
for (auto& i : ans) {
cout << i << " ";
}
cout << endl;
}
cout << endl;
return 0;
}
二、组合Ⅱ
1.题目
Leetcode:第 40 题
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates =[10,1,2,7,6,1,5]
, target =8
, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5, 输出: [ [1,2,2], [5] ]
2.解题思路
使用回溯算法来解决组合求和问题。combinationSum2
首先对候选数字进行排序,然后初始化路径和结果集,并开始回溯过程。backtracking
函数是回溯算法的核心,它尝试将每个候选数字添加到当前路径中,并递归地继续寻找满足条件的组合。为了避免重复使用相同的数字,它会跳过与前一个元素相同的候选数字。当找到一个满足条件的组合时,它会将其添加到结果集中。通过这种方式,backtracking
函数能够找到所有可能的组合。
3.实现代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 一、组合总和II
class Solution {
public:
// 定义一个二维数组result,用于存储所有满足条件的组合
vector<vector<int>> result;
// 定义一个一维数组path,用于存储当前的组合路径
vector<int> path;
// 定义backtracking函数,用于实现回溯算法
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
// 如果当前和sum等于目标值target,则将当前路径path添加到结果集result中
if (sum == target) {
result.push_back(path);
return;
}
// 遍历候选数字candidates,从startIndex开始,直到遍历完或者当前和超过目标值
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
// 如果当前元素与前一个元素相同,跳过当前循环,避免重复使用相同的数字
if (i > startIndex && candidates[i] == candidates[i - 1]) {
continue;
}
// 将当前候选数字加入到当前和sum中,并将其加入到当前路径path中
sum += candidates[i];
path.push_back(candidates[i]);
// 递归调用backtracking函数,尝试使用下一个候选数字
backtracking(candidates, target, sum, i + 1);
// 回溯:从当前和中减去当前候选数字,并从当前路径中移除它
sum -= candidates[i];
path.pop_back();
}
}
// 定义combinationSum2函数,用于求解组合求和问题
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
// 清空当前路径path和结果集result
path.clear();
result.clear();
// 将候选数字candidates按升序排序,以便在回溯过程中可以跳过重复的数字
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0); // 调用backtracking函数,开始回溯过程
return result;// 返回结果集result,其中包含了所有满足条件的组合
}
};
//测试
int main()
{
Solution s;
vector<vector<int>> result;
vector<int>candidates = { 10,1,2,7,6,1,5 };
int target = 8;
result = s.combinationSum2(candidates, target);
cout << "所有的组合有:" << endl;
for (auto& ans : result) {
for (auto& i : ans) {
cout << i << " ";
}
cout << endl;
}
cout << endl;
return 0;
}
三、分割回文串
1.题目
Leetcode:第 131 题
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是
回文串
。返回 s
所有可能的分割方案。
示例 1:
输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a" 输出:[["a"]]
2.解题思路
使用回溯算法来解决分割回文串问题。partition
是主函数,它初始化路径和结果集,并开始回溯过程。backtracking
函数是回溯算法的核心,它尝试将字符串分割为多个子字符串,并递归地继续寻找满足条件的分区。isPalindrome
函数用于检查一个子字符串是否是回文串。通过这种方式,backtracking
函数能够找到所有可能的满足条件的分区。
3.实现代码
#include <iostream>
#include <vector>
using namespace std;
// 一、分割回文串
class Solution1 {
public:
// 定义一个二维字符串数组result,用于存储所有满足条件的分区结果
vector<vector<string>> result;
// 定义一个一维字符串数组path,用于存储当前的分区路径
vector<string> path;
// 定义backtracking函数,用于实现回溯算法
void backtracking(const string& s, int startIndex) {
// 如果当前起始索引startIndex已经到达字符串s的末尾,说明已经处理完字符串
if (startIndex >= s.size()) {
result.push_back(path);// 将当前路径path添加到结果集result中
return;
}
// 遍历字符串s,从startIndex开始,直到遍历完字符串
for (int i = startIndex; i < s.size(); i++) {
// 检查从startIndex到i的子字符串是否是回文串
if (isPalindrome(s, startIndex, i)) {
// 从字符串s中截取从startIndex到i的子字符串,并保存到局部变量str中
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);// 将子字符串str添加到当前路径path中
}
else {
continue;
}
backtracking(s, i + 1);// 递归调用backtracking函数,尝试使用下一个分区
path.pop_back(); // 回溯:从当前路径中移除最后一个元素
}
}
// 定义isPalindrome函数,用于检查一个子字符串是否是回文串
bool isPalindrome(const string& s, int start, int end) {
// 使用双指针方法,从字符串的两端开始向中间遍历
for (int i = start, j = end; i < j; i++, j--) {
// 如果在遍历过程中发现字符不相等,则该子字符串不是回文串
if (s[i] != s[j]) {
return false;
}
}
return true;// 如果所有字符都相等,则该子字符串是回文串
}
// 定义partition函数,用于求解字符串分区问题
vector<vector<string>> partition(string s) {
// 清空当前路径path和结果集result
result.clear();
path.clear();
backtracking(s, 0); // 调用backtracking函数,开始回溯过程
return result; // 返回结果集result,其中包含了所有满足条件的分区结果
}
};
// 二、分割回文串(优化)
class Solution2 {
public:
// 定义一个二维字符串数组result,用于存储所有满足条件的分区结果
vector<vector<string>> result;
// 定义一个一维字符串数组path,用于存储当前的分区路径,即已经找到的回文子串
vector<string> path;
// 定义一个二维布尔数组isPalindrome,用于存储字符串中每个子串是否是回文串的预先计算结果
vector<vector<bool>> isPalindrome;
// 定义backtracking函数,用于实现回溯算法
void backtracking(const string& s, int startIndex) {
// 如果当前起始索引startIndex已经到达或超过字符串s的末尾,说明已经找到了一组分区方案
if (startIndex >= s.size()) {
// 将当前路径path添加到结果集result中
result.push_back(path);
return;
}
// 遍历字符串s,从startIndex开始,直到遍历完字符串
for (int i = startIndex; i < s.size(); i++) {
// 如果从startIndex到i的子串是回文串(根据预先计算的isPalindrome向量)
if (isPalindrome[startIndex][i]) {
// 从字符串s中截取从startIndex到i的子串,并保存到局部变量str中
string str = s.substr(startIndex, i - startIndex + 1);
// 将子串str添加到当前路径path中
path.push_back(str);
}
else {
continue;
}
backtracking(s, i + 1);// 递归调用backtracking函数,尝试使用下一个起始位置i+1进行分区
path.pop_back();// 回溯过程,从当前路径中移除最后一个元素
}
}
// 定义computePalindrome函数,用于预先计算字符串中所有子串是否是回文串
void computePalindrome(const string& s) {
// 根据字符串s的大小,初始化isPalindrome二维向量,所有元素初始为false
isPalindrome.resize(s.size(), vector<bool>(s.size(), false));
// 从后向前遍历字符串s,计算每个子串是否是回文串
for (int i = s.size() - 1; i >= 0; i--) {
// 由于是检查子串,所以需要从后向前计算,以确保在计算i行时,i+1行已经计算好了
for (int j = i; j < s.size(); j++) {
// 如果子串的长度为1,则它是回文串
if (j == i) {
isPalindrome[i][j] = true;
}
// 如果子串的长度为2,则检查两个字符是否相等
else if (j - i == 1) {
isPalindrome[i][j] = (s[i] == s[j]);
}
// 如果子串的长度大于2,则检查首尾字符是否相等,并且中间子串是否是回文串
else {
isPalindrome[i][j] = (s[i] == s[j] && isPalindrome[i + 1][j - 1]);
}
}
}
}
// 定义partition函数,用于求解字符串分区问题
vector<vector<string>> partition(string s) {
// 清空当前路径path和结果集result
result.clear();
path.clear();
computePalindrome(s);// 预先计算字符串s中所有子串是否是回文串
backtracking(s, 0); // 调用backtracking函数,开始回溯过程,以0作为起始位置
return result;// 返回结果集result,其中包含了所有满足条件的分区结果
}
};
//测试
int main()
{
Solution1 p;
vector<vector<string>> result;
string s = "aab";
result = p.partition(s);
cout << "所有的组合有:" << endl;
for (auto& ans : result) {
for (auto& i : ans) {
cout << i << " ";
}
cout << endl;
}
cout << endl;
return 0;
}
ps:以上皆是本人在探索算法旅途中的浅薄见解,诚挚地希望得到各位的宝贵意见与悉心指导,若有不足或谬误之处,还请多多指教。