刷题笔记
- 递归回溯算法leetcode专栏
- leetcode 77 组合 (组合问题)
- leetcode 39 组合总和(组合问题)
- leetcode 40 组合总和II(组合问题)
- leetcode 216 组合总和III(组合问题)
- leetcode 17 电话号码的字母组合(组合问题)
- leetcode 131 分割回文串(分割问题)
- leetcode 93 复原IP地址(分割问题)
- leetcode 139 单词拆分(分割问题)
- leetcode 78 子集(子集问题)
- leetcode 90 子集II(子集问题)
- leetcode 491 非递减子序列 (子集问题)
- leetcode 46 全排列 (排列问题)
- leetcode 47 全排列II (排列问题)
- leetcode 784 字母大小写全排列
- leetcode 51 N皇后(棋盘问题)
- leetcode 698 划分为k个相等的子集
- leetcode 416 分割等和子集
- leetcode 473 火柴拼正方形
- leetcode 241 为运算表达式设计优先级
- leetcode 22 括号生成
- leetcode 79 单词搜索
递归回溯算法leetcode专栏
回溯算法是对树形或者图形结构执行一次深度优先遍历,实际上类似枚举的搜索尝试过程,在遍历的过程中寻找问题的解。但当探索到某一步时,发现原先选择达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。
深度优先遍历有个特点:当发现已不满足求解条件时,就返回,尝试别的路径。此时对象类型变量就需要重置成为和之前一样,称为「状态重置」。
许多复杂的,规模较大的问题都可以使用回溯法,有「通用解题方法」的美称。实际上,回溯算法就是暴力搜索算法,它是早期的人工智能里使用的算法,借助计算机强大的计算能帮助我们找到问题的解。
leetcode 77 组合 (组合问题)
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3],
[1,4], ]
示例 2:
输入:n = 1, k = 1 输出:[[1]]
提示:
1 <= n <= 20
1 <= k <= n
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combinations
class Solution {
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> result;
vector<int> path;
backtracking(1, n, k, path, result);
return result;
}
//设置Index的目的是该问题为组合问题,而且所选择的元素不重复
//每一层可以在数据集合中所选择的数据范围都是缩小的
//例如本题在第一层在(1,4)之间选择了1之后,下一层只能在(2,4)之间进行选择了,而不能再选择1.
void backtracking(int index, int n, int k, vector<int>& path, vector<vector<int>>& result)
{
if (path.size() == k) //回溯函数的终止条件
{
result.push_back(path);
return;
}
for (int i = index; i <= n; i++) //探索每一层的所有可能的路径选择
{
path.push_back(i); //对当前所选择的节点数据进行处理
backtracking(i + 1, n, k, path, result); //递归继续处理子问题
path.pop_back(); //从该节点往下的情况处理完毕,撤销对该节点所做的处理
} //for循环会选择该层的下一个节点继续搜索
}
//情况1:选择该数据,剩下的递归处理
//情况2:不选择该数据,for循环判断下一个数据
};
class Solution {
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> result;
vector<int> item;
backtracking(1, k, n, item, result);
return result;
}
void backtracking(int i, int k, int n, vector<int>& item, vector<vector<int>>& result) {
if (item.size() == k) {
result.push_back(item);
return;
}
if (i > n) return; //这个判断必须放在下面,不然会导致错误
item.push_back(i);
backtracking(i + 1, k, n, item, result);
item.pop_back();
backtracking(i + 1, k, n, item, result);
}
};
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 输出: []
示例 4:
输入: candidates = [1], target = 1 输出: [[1]]
示例 5:
输入: candidates = [1], target = 2 输出: [[1,1]]
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都 互不相同
1 <= target <= 500
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> result;
vector<int> vec;
backtracking(0, 0, vec, result, candidates, target);
return result;
}
void backtracking(int Index, int sum, vector<int>& vec, vector<vector<int>>& result, vector<int>& candidates, int target)
{
if (sum == target) //回溯函数获得正确结果的终止条件
{
result.push_back(vec);
return;
}
if (sum > target) //回溯函数走的这条路径所产生的结果行不通,需要返回
{
return;
}
for (int i = Index; i < candidates.size(); i++)
{
sum += candidates[i]; //对该层所选择的数据节点进行处理
vec.push_back(candidates[i]);
backtracking(i, sum, vec, result, candidates, target); //这里传入参数i的原因是因为该数据节点在下一层的处理中可以重复选取
sum -= candidates[i];
vec.pop_back(); //撤销,对该层所选择的数据节点的处理,for循环选择该层的下一个数据节点继续向下探索
}
}
};
leetcode 40 组合总和II(组合问题)
给定一个数组 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] ]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum-ii
注意:这道题主要新增加了对下面这种情况的去重代码,因为candidates数组中的元素是乱序的,并且存在有重复的元素。需要先对candidates数组排序,将相同的元素放在一起,在递归过程中,于某一层选取数据节点时,如果发现后面的一个元素和前面的一个元素相同,那么就跳过该元素的选取,不然就会导致最终选择出来的数据有下面重复的情况。
输入 [10,1,2,7,6,1,5] 8 输出 [[1,2,5],[1,7],[1,6,1],[2,6],[2,1,5],[7,1]]
预期结果 [[1,1,6],[1,2,5],[1,7],[2,6]]
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<vector<int>> result;
vector<int> path;
// set<int> st(candidates.begin(), candidates.end());
// candidates.assign(st.begin(), st.end());
sort(candidates.begin(), candidates.end());
backtracking(0, 0, target, candidates, path, result);
return result;
}
void backtracking(int index, int sum, int target, vector<int>& candidates, vector<int>& path, vector<vector<int>>& result)
{
if (sum == target)
{
result.push_back(path);
return;
}
if(sum > target) //剪枝
{
return;
}
for (int i = index; i < candidates.size(); i++)
{
if(i > index && candidates[i] == candidates[i-1])
{
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(i + 1, sum, target, candidates, path, result);
sum -= candidates[i];
path.pop_back();
}
}
};
该题如果用集合set去重复,最后一个测试用例会超时
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<vector<int>> result;
vector<int> item;
sort(candidates.begin(), candidates.end());
set<vector<int>> st; //用于对结果去重的集合
backtracking(0, target, 0, candidates, item, result, st);
return result;
}
void backtracking(int start, int target, int sum, vector<int>& candidates, vector<int>& item, vector<vector<int>>& result, set<vector<int>>& st) {
if (sum == target) {
if (st.find(item) == st.end()) {
result.push_back(item);
st.insert(item);
}
return;
}
if (sum > target) {
return;
}
for (int i = start; i < candidates.size(); i++) {
sum += candidates[i];
item.push_back(candidates[i]);
backtracking(i + 1, target, sum, candidates, item, result, st);
item.pop_back();
sum -= candidates[i];
}
}
};
leetcode 216 组合总和III(组合问题)
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum-iii
class Solution {
public:
vector<vector<int>> combinationSum3(int k, int n) {
vector<vector<int>> result;
vector<int> path;
backtracking(1, 0, k, n, path, result);
return result;
}
void backtracking(int index, int sum, int k, int n, vector<int>& path, vector<vector<int>>& result)
{
if (path.size() == k && sum == n)
{
result.push_back(path);
return;
}
if (path.size() > k || sum > n) //回溯过程中的剪枝,在该层的处理的过程中发现结果已经不符合我们要求就直接返回,节约时间。
{
return;
}
for (int x = index; x <= 9; x++) //每个数字的取值范围为1-9,并且是数字不重复的组合
{
sum += x;
path.push_back(x);
backtracking(x + 1, sum, k, n, path, result);
sum -= x;
path.pop_back();
}
}
};
class Solution {
public:
vector<vector<int>> combinationSum3(int k, int n) {
vector<vector<int>> result;
vector<int> path;
backtracking(1, 0, k, n, path, result);
return result;
}
void backtracking(int x, int sum, int k, int n, vector<int>& path, vector<vector<int>>& result) {
if (path.size() == k && sum == n) {
result.push_back(path);
return;
}
if (path.size() > k || sum > n) { //回溯过程中的剪枝,在该层的处理的过程中发现结果已经不符合我们要求就直接返回,节约时间。
return;
}
if (x > 9) return;
path.push_back(x); //放x的情况
backtracking(x + 1, sum + x, k, n, path, result);
path.pop_back(); //不放x的情况
backtracking(x + 1, sum, k, n, path, result);
}
};
leetcode 17 电话号码的字母组合(组合问题)
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23” 输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “” 输出:[]
示例 3:
输入:digits = “2” 输出:[“a”,“b”,“c”]
提示:
0 <= digits.length <= 4 。digits[i] 是范围 [‘2’, ‘9’] 的一个数字。
class Solution {
public:
vector<string> letterCombinations(string digits) {
unordered_map<char, string> ump = //存储数字字符对应字母字符串的映射
{
{'2',"abc"}, {'3',"def"}, {'4',"ghi"}, {'5',"jkl"},
{'6',"mno"}, {'7',"pqrs"}, {'8',"tuv"}, {'9',"wxyz"}
};
vector<string> result;
string path;
if(digits.size() == 0)
{
return result;
}
backtracking(0, path, result, ump, digits);
return result;
}
void backtracking(int index, string& path, vector<string>& result, unordered_map<char, string>& ump, string& digits)
{
if (index == digits.length())
{
result.push_back(path);
return;
}
string tmp = ump[digits[index]]; //index指的是递归树的深度,或者说所按数字字符串的下标
for (int i = 0; i < tmp.size(); i++)
{
path.push_back(tmp[i]);
backtracking(index + 1, path, result, ump, digits);
path.pop_back();
}
}
};
leetcode 131 分割回文串(分割问题)
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = “aab” 输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a” 输出:[[“a”]]
对于字符串aab,先切割a,再递归处理其余部分,再切割aa,再递归处理其余部分,最后切割aab,再递归处理其余部分。
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> result;
vector<string> path;
backtracking(0, s, path, result);
return result;
}
void backtracking(int startIndex, string s, vector<string>& path, vector<vector<string>>& result)
{
if (startIndex >= s.size())
{
result.push_back(path);
return;
}
for (int i = startIndex; i < s.size(); i++)
{
if (isPalindrome(s, startIndex, i)) //[startIndex,i]为回文串并切割下来
{
string st = s.substr(startIndex, i - startIndex + 1);
path.push_back(st); //保存此次切割结果
}
else //这里过滤掉非回文串的子串,相当于剪枝
{
continue;
}
backtracking(i + 1, s, path, result); //递归处理剩余的字符串,下次的起始位置startIndex = i + 1
path.pop_back(); //状态回退,选择新的第一次要切割下来的子串
}
}
bool isPalindrome(const string& str, int start, int end) //判断子串是否为回文串
{
bool flag = true;
for (int i = start, j = end; i < j; i++, j--)
{
if (str[i] != str[j])
{
flag = false;
}
}
return flag;
}
};
leetcode 93 复原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 中的任何数字。你可以按任何顺序返回答案。
示例 1:
输入:s = “25525511135” 输出:[“255.255.11.135”,“255.255.111.35”]
示例 2:
输入:s = “0000” 输出:[“0.0.0.0”]
示例 3:
输入:s = “1111” 输出:[“1.1.1.1”]
示例 4:
输入:s = “010010” 输出:[“0.10.0.10”,“0.100.1.0”]
示例 5:
输入:s = “101023”
输出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]
提示:
0 <= s.length <= 20
s 仅由数字组成
class Solution {
public:
vector<string> restoreIpAddresses(string s) {
vector<string> result;
backtracking(0, 0, s, result);
return result;
}
//在分割过程中不能重复分割,startIndex用来记录下一层递归分割的起始位置
//pointNum记录添加逗点的数量
void backtracking(int startIndex, int pointNum, string& s, vector<string>& result)
{
if (pointNum == 3 ) //terminate
{ //用分割的段数作为终止条件,pointNum为3说明字符串分成4段了
if (IsCorrect(s, startIndex, s.size() - 1))
{
result.push_back(s);
}
return;
}
for (int i = startIndex; i < s.size(); i++)
{
if (IsCorrect(s, startIndex, i))
{
s.insert(s.begin() + i + 1, '.'); //在下标i的后面插入一个小数点
pointNum++;
backtracking(i + 2, pointNum, s, result); //插入小数点后,下一次的startindex为i+2
pointNum--; //回溯
s.erase(s.begin() + i + 1);
}
else
{
break; //如果在某层截取过程中遇到不合法的数字直接结束本层的搜索,剪掉分支
}
}
}
bool IsCorrect(const string& s, int start, int end)
{
if(start > end)
{
return false;
}
string su = s.substr(start, end - start + 1);
if (su.size() > 1 && su[0] == '0') //处理字符串前导0,或者该字符串中含有非数字不能转成Int
{
return false;
}
int num = atoi(su.c_str());
if (num == -1) //atoi可能因为数字过大报错返回-1
{
return false;
}
if (num >= 0 && num <= 255)
{
return true;
}
else
{
return false;
}
}
};
leetcode 139 单词拆分(分割问题)
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”] 输出: true 解释: 返回 true
因为 “leetcode” 可以由 “leet” 和 “code” 拼接成。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”] 输出: true 解释: 返回
true 因为 “applepenapple” 可以由 “apple” “pen” “apple” 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
提示:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 仅由小写英文字母组成
wordDict 中的所有字符串 互不相同
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
// dp[i] : 字符串长度为i的话s[0-i-1],dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。否则为false,这里的i代表的是长度。
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
// 对于本题而言,是存在顺序的,所求的是排列数。因此必须先遍历背包,再遍历物体。
for (int i = 1; i <= s.length(); i++) {
//if 从j到i切割下来的子串在我们的wordDict中,同时dp[j]为true ----> dp[i] = true
for (int j = 0; j < i; j++) {
String word = s.substring(j, i); // word是我们截取的物品
if (wordDict.contains(word) && dp[j]) {
dp[i] = true;
}
}
}
return dp[s.length()];
}
}
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
// 将字典转换为 HashSet,方便快速查找
Set<String> wordSet = new HashSet<>(wordDict);
// 记忆化数组,memo[i] 表示从索引 i 开始的子串是否可以被拆分
Boolean[] memo = new Boolean[s.length()];
return backtracking(s, 0, wordSet, memo);
}
private boolean backtracking(String s, int startIndex, Set<String> wordSet, Boolean[] memo) {
// 如果已经到达字符串末尾,返回 true
if (startIndex == s.length()) {
return true;
}
// 如果已经计算过当前索引的结果,直接返回
if (memo[startIndex] != null) {
return memo[startIndex];
}
// 尝试所有可能的拆分
for (int i = startIndex; i < s.length(); i++) {
String word = s.substring(startIndex, i + 1);
// 如果当前单词在字典中,递归检查剩余部分
if (wordSet.contains(word) && backtracking(s, i + 1, wordSet, memo)) {
memo[startIndex] = true; // 记录结果
return true;
}
}
// 如果没有找到任何拆分方式,记录并返回 false
memo[startIndex] = false;
return false;
}
}
leetcode 78 子集(子集问题)
给你一个整数数组 nums,数组中的元素互不相同 。返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集。你可以按任意顺序返回解集。
采用回溯法生产子集,即对于每个元素,都有试探放入或不放入集合两个选择:
选择放入该元素,递归的进行后续元素的选择,完成放入该元素后续所有元素的试探;
之后将其拿出来,即再进行一次选择不放入该元素,递归的进行后续元素的选择,完成不放入该元素后续所有元素的试探。(递归树)
本来选择放入,再选择一次不放入的这个过程,称为回溯试探法。
例如:
元素数组: nums = [1,2,3,4,5],子集生成数组path[] = []
对于元素1,
选择放入path,path = [1],继续递归处理后续[2,3,4,5]元素;path = [1,……]
选择不放入path,path = [],继续递归处理后续[2,3,4,5]元素;path = [……]
在递归树中,递归回溯的时候,需要path.pop_back(),比如说,我们现在放入了1、2、3,碰到递归结束条件return,返回到上一层,这时候我们需要状态重置,将3弹出来,处理不放入3的结果。处理完不放3的情况后,递归树再次返回到上一层,然后处理不放入2的情况。
注:如果把子集问题、组合问题、分割问题都抽象成一颗树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找到树的所有节点。
注:既然是无序,取过的元素不会重复选取,写回溯算法的时候,for就要从startIndex开始,而不是0开始。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0] 输出:[[],[0]]
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<int> path;
vector<vector<int>> result;
backtracking(0, path, result, nums);
return result;
}
void backtracking(int startIndex, vector<int>& path, vector<vector<int>>& result, vector<int>& nums)
{
result.push_back(path); //收集子集要放在terminate上面,否则会丢失[]集合
if (startIndex >= nums.size()) { //terminate
return;
}
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracking(i + 1, path, result, nums);
path.pop_back();
}
}
};
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
vector<int> item;
result.push_back(item);
backtracking(0, nums, item, result);
return result;
}
void backtracking(int i, vector<int>& nums, vector<int>& item, vector<vector<int>>& result) {
if (i >= nums.size()) { //terminate
return;
}
item.push_back(nums[i]);
result.push_back(item);
backtracking(i + 1, nums, item, result);
item.pop_back();
backtracking(i + 1, nums, item, result);
}
};
leetcode 90 子集II(子集问题)
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
注:相比于上面的子集问题多了去重的操作。
示例 1:
输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0] 输出:[[],[0]]
有两种重复原因:
1、不同位置的元素组成的集合是同一个子集,顺序相同:
例如:[2,1,2,2]
选择第1,2,3个元素组成的子集:[2,1,2]
选择第1,2,4个元素组成的子集:[2,1,2]
2、不同位置的元素组成的集合是同一个子集,虽然顺序不同,但仍然代表了同一个子集,因为集合中的元素是无序的。
选择第1,2,3个元素组成的子集:[2,1,2]
选择第2,3,4个元素组成的子集:[1,2,2]
解决方案:可以先对原始nums数组进行排序,再使用set去重。
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<vector<int>> result;
vector<int> item;
set<vector<int>> res_set;
sort(nums.begin(), nums.end());
result.push_back(item);
backtracking(0, nums, item, result, res_set);
return result;
}
void backtracking(int i, vector<int>& nums,
vector<int>& item,
vector<vector<int>>& result,
set<vector<int>>& res_set) {
if (i >= nums.size()) { //terminate
return;
}
item.push_back(nums[i]);
//result.push_back(item);
if(res_set.find(item) == res_set.end()) {
result.push_back(item);
res_set.insert(item);
}
backtracking(i + 1, nums, item, result, res_set);
item.pop_back();
backtracking(i + 1, nums, item, result, res_set);
}
};
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<int> path;
vector<vector<int>> result;
sort(nums.begin(), nums.end());
backtracking(0, path, result, nums);
return result;
}
void backtracking(int startIndex, vector<int> path, vector<vector<int>>& result, vector<int>& nums)
{
result.push_back(path);
if (startIndex >= nums.size())
{
return;
}
for (int i = startIndex; i < nums.size(); i++)
{
if (i > startIndex && nums[i] == nums[i - 1])
{
continue;
}
path.push_back(nums[i]);
backtracking(i + 1, path, result, nums);
path.pop_back();
}
}
};
leetcode 491 非递减子序列 (子集问题)
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素 。你可以按任意顺序返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
注:在子集II中我们是通过排序,再判断本层遍历的前后元素是否相同来进行去重的。而本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。所以不能用之前的去重逻辑。
注:本题也可以根据-100 <= nums[i] <= 100条件来用数组做哈希映射来判断该元素是否被使用过进行去重。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1] 输出:[[4,4]]
提示:
1 <= nums.length <= 15
-100 <= nums[i] <= 100
class Solution {
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
vector<int> path;
vector<vector<int>> result;
backtracking(0, path, result, nums);
return result;
}
void backtracking(int startIndex, vector<int> path, vector<vector<int>>& result, vector<int>& nums)
{
if (path.size() >= 2)
{
result.push_back(path);
}
if (startIndex >= nums.size())
{
return;
}
unordered_set<int> uset; //记录本层元素是否重复使用,在新的一层set会被重新定义,所以set只负责本层
for (int i = startIndex; i < nums.size(); i++)
{
if (uset.find(nums[i]) != uset.end()) //该元素在这一层已经使用了,不能再使用了
{
continue;
}
if (!path.empty() && nums[i] < path.back())
{
continue;
}
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(i + 1, path, result, nums);
path.pop_back();
}
}
};
class Solution {
/*
为了避免重复的子序列,代码在每一层使用 Set 集合记录已经使用过的元素,
这是因为在回溯的过程中,同一层的递归可能会遇到相同的元素,而这些相同的元素会导致生成重复的子序列。
为什么需要每一层去重?
同一层的重复元素会导致重复子序列:
在回溯的过程中,每一层的递归会尝试从当前位置开始,选择不同的元素来构建子序列。
如果数组中存在重复的元素(例如 [4, 6, 7, 7] 中的两个 7),在同一层的递归中,可能会多次选择相同的值(比如两个 7),从而导致生成重复的子序列。
去重的范围是当前层:
去重的目的是为了避免在同一层的递归中选择相同的元素,而不是全局去重。
例如,在 [4, 6, 7, 7] 中,当递归到第二个 7 时,如果第一个 7 已经被使用过,那么在同一层中再次选择 7 会导致重复的子序列(如 [4, 7] 和 [4, 7])。
通过使用 Set 记录当前层已经使用过的元素,可以避免这种情况。
不同层的相同元素不需要去重:
如果相同的元素出现在不同的递归层中,它们是可以被选择的,因为它们是不同的选择路径。
例如,在 [4, 6, 7, 7] 中,第一个 7 和第二个 7 分别属于不同的递归层,它们可以被选择来生成不同的子序列(如 [4, 7] 和 [4, 7, 7])。
举例说明
以数组 [4, 6, 7, 7] 为例:
当递归到第一个 7 时,会生成子序列 [4, 7] 和 [4, 7, 7]。
当递归到第二个 7 时,如果不进行去重,会再次生成子序列 [4, 7] 和 [4, 7, 7],导致重复。
通过使用 Set 记录当前层已经使用过的元素,可以避免在第二个 7 时再次生成相同的子序列。
*/
public List<List<Integer>> findSubsequences(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtracking(nums, 0, new ArrayList<>(), result);
return result;
}
private void backtracking(int[] nums, int startIndex, List<Integer> current, List<List<Integer>> result) {
// 如果当前子序列的长度大于等于2,加入结果集
if (current.size() >= 2) {
result.add(new ArrayList<>(current));
}
// 使用一个集合来记录当前层已经使用过的元素,避免重复
Set<Integer> used = new HashSet<>();
for (int i = startIndex; i < nums.length; i++) {
// 如果当前元素小于子序列的最后一个元素,或者已经使用过,则跳过
if (!current.isEmpty() && nums[i] < current.get(current.size() - 1) || used.contains(nums[i])) {
continue;
}
// 选择当前元素
used.add(nums[i]);
current.add(nums[i]);
// 递归进入下一层
backtracking(nums, i + 1, current, result);
// 撤销选择
current.remove(current.size() - 1);
}
}
}
leetcode 46 全排列 (排列问题)
回溯法可以用来解决组合问题和排列问题,但它们的核心区别在于元素的顺序是否重要。组合问题关注的是元素的选择,而排列问题关注的是元素的顺序。这种区别会直接影响回溯法的实现方式。
组合问题(Combination)
定义:从一组元素中选择若干个元素,不考虑顺序。
特点:元素的顺序不重要,例如 [1, 2] 和 [2, 1] 是同一个组合。
通常需要避免重复的组合。
回溯实现:
使用一个 start 参数来控制递归的起始位置,确保不会重复选择之前的元素。
每一层递归从 start 开始遍历,避免选择已经处理过的元素。
排列问题(Permutation)
定义:从一组元素中选择若干个元素,考虑顺序。
特点:元素的顺序重要,例如 [1, 2] 和 [2, 1] 是不同的排列。
通常需要避免重复的排列(如果输入包含重复元素)。
回溯实现:
不需要 start 参数,因为每一层递归都可以从第一个元素开始选择。
使用一个 used 数组或集合来记录已经使用过的元素,避免重复选择。
组合问题和排列问题的联系:
组合问题和排列问题都可以通过回溯法解决。
组合问题可以看作是排列问题的一个子集,因为排列问题中包含了所有可能的顺序。
如果组合问题中需要去重(例如输入包含重复元素),可以使用排序 + 剪枝的方式,类似于排列问题中的去重逻辑。
组合问题去重:
如果输入包含重复元素(例如 [1, 2, 2]),需要对输入数组进行排序,并在回溯时跳过重复元素。
示例代码:if (i > start && nums[i] == nums[i - 1]) continue;
排列问题去重:
如果输入包含重复元素,需要使用 used 数组记录已经使用过的元素,并在同一层递归中跳过重复元素。
示例代码:if (used[i] || (i > 0 && nums[i] == nums[i - 1] && used[i - 1]) == true) continue;
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1] 输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1] 输出:[[1]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums 中的所有整数 互不相同
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<int> path;
vector<vector<int>> result;
vector<bool> used(nums.size(), false);
backtracking(nums, used, path, result);
return result;
}
void backtracking(vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& result)
{
if(path.size() == nums.size())
{
result.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++)
{
if (!used[i])
{
path.push_back(nums[i]);
used[i] = true;
backtracking(nums, used, path, result);
used[i] = false;
path.pop_back();
}
}
}
};
leetcode 47 全排列II (排列问题)
给定一个可包含重复数字的序列 nums ,按任意顺序返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<int> path;
vector<vector<int>> result;
vector<bool> used(nums.size(), false);
sort(nums.begin(), nums.end());
backtracking(nums, used, path, result);
return result;
}
void backtracking(vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& result)
{
if(path.size() == nums.size())
{
result.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++)
{
if(i > 0 && nums[i] == nums[i-1] && used[i-1] == true)
{
continue;
}
if (!used[i])
{
path.push_back(nums[i]);
used[i] = true;
backtracking(nums, used, path, result);
used[i] = false;
path.pop_back();
}
}
}
};
leetcode 784 字母大小写全排列
给定一个字符串 s ,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。
返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。
示例 1:
输入:s = “a1b2”
输出:[“a1b2”, “a1B2”, “A1b2”, “A1B2”]
示例 2:
输入: s = “3z4”
输出: [“3z4”,“3Z4”]
提示:
1 <= s.length <= 12
s 由小写英文字母、大写英文字母和数字组成
回溯法:
1、使用递归生成所有可能的组合。
2、对于每个字符,如果是字母,则尝试将其转换为大写或小写,并递归处理后续字符。
3、如果是数字,则直接保留,并递归处理后续字符。
4、当处理完所有字符时,将当前组合加入结果集。
class Solution {
public List<String> letterCasePermutation(String s) {
List<String> result = new ArrayList<>();
backtrack(s.toCharArray(), 0, result);
return result;
}
private void backtrack(char[] chars, int index, List<String> result) {
// 如果处理完所有字符,将当前组合加入结果集
if (index == chars.length) {
result.add(new String(chars));
return;
}
// 获取当前字符
char currentChar = chars[index];
// 如果是字母,尝试转换为大写或小写
if (Character.isLetter(currentChar)) {
// 转换为小写
chars[index] = Character.toLowerCase(currentChar);
backtrack(chars, index + 1, result);
// 转换为大写
chars[index] = Character.toUpperCase(currentChar);
backtrack(chars, index + 1, result);
} else {
// 如果是数字,直接保留,继续递归
backtrack(chars, index + 1, result);
}
}
}
leetcode 51 N皇后(棋盘问题)
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个不同的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1 输出:[[“Q”]]
提示:
1 <= n <= 9
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
string line;
vector<string> record;
vector<vector<string>> result;
vector<vector<bool>> mark; //使用二维数组mark[][]表示一张空的棋盘
for(int i = 0; i < n; i++) //对棋盘进行初始化
{
mark.push_back(vector<bool>());
for(int j = 0; j < n; j++)
{
mark[i].push_back(false);
}
record.push_back("");
record[i].append(n, '.');
}
backtracking(0, result, record, mark);
return result;
}
//index是递归的深度,指的是棋盘的行数,在特定的行尝试把棋子放在不同的列进行探索,正确的解
void backtracking(int index, vector<vector<string>>&result, vector<string>&record, vector<vector<bool>>&mark)
{
if (index == mark.size()) //terminate
{
result.push_back(record);
return;
}
for(int i = 0; i < mark.size(); i++)
{
if(!mark[index][i])
{
vector<vector<bool>> markbak = mark;
putDownTheQueue(index, i, mark);
record[index][i] = 'Q';
backtracking(index+1, result, record, mark);
mark = markbak;
record[index][i] = '.';
}
}
}
//put_down_the_queen()函数的功能就是将皇后放置在坐标(x,y)处,并且对mark数组进行修改
//使得该位置的八个方向上所有位置坐标值都为1,这些方向都不能再放置皇后了
void putDownTheQueue(int x, int y, vector<vector<bool>>& mark)
{
static const int direct[][2] = { {1,0}, {1,1}, {0,1}, {-1,1}, {-1,0}, {-1,-1}, {0,-1}, {1,-1} };
mark[x][y] = true;
for (int i = 1; i < mark.size(); i++)
{
for (int j = 0; j < 8; j++) //8个方向每个方向向外延申1-N-1
{
int newx = x + i * direct[j][0];
int newy = y + i * direct[j][1];
if (newx >= 0 && newx < mark.size() && newy >= 0 && newy < mark.size())
{
mark[newx][newy] = true;
}
}
}
}
};
leetcode 698 划分为k个相等的子集
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
示例 2:
输入: nums = [1,2,3,4], k = 3
输出: false
提示:
1 <= k <= len(nums) <= 16
0 < nums[i] < 10000
每个元素的频率在 [1,4] 范围内
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
int target = 0;
for(int i = 0; i < nums.size(); i++) {
target += nums[i];
}
if(target % k != 0) return false;
target = target / k;
sort(nums.begin(), nums.end(), greater<int>()); //降序排列
if(nums.front() > target) return false; //第一个数放在哪个桶都不合适
bool flag = false;
vector<bool> visited(nums.size(), false);
BackTracking(k, 0, 0, target, nums, flag, visited);
return flag;
}
void BackTracking(int k, int i, int curSum, int target, vector<int>& nums, bool& flag, vector<bool>& visited) {
if (k == 0) {
flag = true;
return;
}
if (curSum == target) {
BackTracking(k - 1, 0, 0, target, nums, flag, visited);
return;
}
if (i >= nums.size()) {
return;
}
if(!visited[i] && curSum + nums[i] <= target) {
visited[i] = true;
BackTracking(k, i + 1, curSum + nums[i], target, nums, flag, visited);
visited[i] = false;
}
if (flag == true) return;
BackTracking(k, i + 1, curSum, target, nums, flag, visited);
}
};
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
int target = 0;
for(int i = 0; i < nums.size(); i++) {
target += nums[i];
}
if(target % k != 0) return false;
target = target / k;
sort(nums.begin(), nums.end(), greater<int>());
if(nums.front() > target) return false;
vector<bool> visited(nums.size(), false);
return backtracking(k, 0, target, 0, nums, visited);
}
bool backtracking(int k, int startIndex, int target, int curSum, vector<int>& nums, vector<bool>& visited) {
if(k == 0) return true;
if(curSum == target) {
return backtracking(k-1, 0, target, 0, nums, visited);
}
for(int i = startIndex; i < nums.size(); i++) { //从startIndex开始,第一次选过的元素第二次就不能选择了
if(visited[i]) continue;
if(curSum + nums[i] > target) continue;
visited[i] = true;
curSum += nums[i];
if(backtracking(k, i+1, target, curSum, nums, visited)) {
return true;
}
curSum -= nums[i];
visited[i] = false;
}
return false;
}
};
代码摘自leetcode题解区
// 其实题 416(k = 2) 以及 473(k = 4) 都是这题的特例
// 把 nums 里的数字想象成一个个石头, 石头的总重量为 total
// 定义一个大小为 k 的数组表示有 k 个背包
// 每个背包的载重都相等, 为 total / k
// 现在问我们能不能把这些石头都装进背包里, 显然当且仅当每个背包都装满
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
int target = 0;
for(int i = 0; i < nums.size(); i++) {
target += nums[i];
}
if(target % k != 0) return false;
target = target / k;
sort(nums.begin(), nums.end(), greater<int>());
// 定义 k 个背包
vector<int> bags(k, 0);
// 每个背包的容量为 total / k, 从第一块石头开始装
return backtracking(0, target, nums, bags);
}
bool backtracking(int index, int capacity, vector<int>& nums, vector<int>& bags) {
if(index >= nums.size()) return true; // 如果所有石头都被装进去了, 就找到了一种可能的方案
// 遍历每个包, 检查包的剩余容量
for(int i = 0; i < bags.size(); i++) {
if(bags[i] + nums[index] <= capacity) { //如果当前石头可以装进这个包
bags[i] += nums[index];
if(backtracking(index+1, capacity, nums, bags)) { //继续去装下一块石头
return true;
}
bags[i] -= nums[index]; //这种方案不行, 把石头从包里拿出来
}
if(bags[i] == 0) break;
}
// 非常重要的剪枝!!!
// 如果这个包里什么都没装, 那么就不在下一个包里装
// 因为必须每个包都装才行啊, 跳过这个包去装之后的包肯定不会搜索到可能的方案
return false; //如果不存在一种组合可以使每个背包都正好放下,那么返回false
}
};
class Solution {
public boolean canPartitionKSubsets(int[] nums, int k) {
// 计算数组的总和
int sum = Arrays.stream(nums).sum();
// 如果总和不能被 k 整除,直接返回 false
if (sum % k != 0) {
return false;
}
// 每个子集的目标和
int target = sum / k;
// 对数组进行排序,从大到小尝试分配
Arrays.sort(nums);
reverse(nums);
// 记录每个子集的当前和
int[] subsetSums = new int[k];
return backtrack(nums, 0, subsetSums, target);
}
private boolean backtrack(int[] nums, int index, int[] subsetSums, int target) {
// 如果所有元素都分配完毕,检查是否所有子集的和都等于 target
if (index == nums.length) {
for (int subsetSum : subsetSums) {
if (subsetSum != target) {
return false;
}
}
return true;
}
// 尝试将当前元素放入每个子集
for (int i = 0; i < subsetSums.length; i++) {
// 如果当前子集的和加上当前元素不超过 target,则尝试放入
if (subsetSums[i] + nums[index] <= target) {
subsetSums[i] += nums[index];
// 递归处理下一个元素
if (backtrack(nums, index + 1, subsetSums, target)) {
return true;
}
// 回溯,撤销选择
subsetSums[i] -= nums[index];
}
// 如果当前子集的和为 0,说明无法放入,直接剪枝
if (subsetSums[i] == 0) {
break;
}
}
return false;
}
// 反转数组,使其从大到小排序
private void reverse(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
}
leetcode 416 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
class Solution {
public boolean canPartition(int[] nums) {
if(nums == null || nums.length == 0) return false;
int sum = Arrays.stream(nums).sum();
if (sum % 2 != 0) return false;
int target = sum / 2;
int n = nums.length;
int[] dp = new int[target+1];
for (int j = nums[0]; j <= target; j++) {
dp[j] = nums[0];
}
for (int i = 1; i < n; i++) {
for (int j = target; j >= nums[i]; j++) {
dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
}
}
if (dp[target] == target) return true;
else return false;
}
}
class Solution {
public boolean canPartition(int[] nums) {
int target = 0;
for (int i = 0; i < nums.length; i++) {
target += nums[i];
}
if (target % 2 == 1) return false;
else target = target / 2;
int n = nums.length;
boolean[][] dp = new boolean[n+1][target+1];
for (int j = 1; j <= target; j++) {
dp[1][j] = (nums[0] == j) ? true : false;
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= target; j++) {
if (nums[i-1] > j) {
dp[i][j] = dp[i-1][j];
} else {
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][target];
}
}
class Solution {
public:
bool canPartition(vector<int>& nums) {
int res = 0;
for (int i = 0; i < nums.size(); i++) {
res += nums[i];
}
if (res % 2 == 1) return false;
int target = res / 2;
bool flag = false;
BackTracking(0, target, nums, flag);
return flag;
}
void BackTracking(int i, int target, vector<int>& nums, bool& flag) {
if (i >= nums.size()) {
return;
}
if (target == 0) {
flag = true;
return;
}
else if (target < 0) {
return;
}
BackTracking(i + 1, target - nums[i], nums, flag);
BackTracking(i + 1, target, nums, flag);
}
};
class Solution {
public boolean canPartition(int[] nums) {
// 计算数组的总和
int sum = 0;
for (int num : nums) {
sum += num;
}
// 如果总和是奇数,直接返回 false
if (sum % 2 != 0) {
return false;
}
// 目标子集的和
int target = sum / 2;
// 使用回溯法尝试找到和为 target 的子集
return backtrack(nums, 0, target);
}
private boolean backtrack(int[] nums, int index, int target) {
// 如果 target 减到 0,说明找到了一个满足条件的子集
if (target == 0) {
return true;
}
// 如果遍历完所有元素仍未找到,返回 false
if (index == nums.length || target < 0) {
return false;
}
// 选择当前元素,递归尝试
if (backtrack(nums, index + 1, target - nums[index])) {
return true;
}
// 不选择当前元素,递归尝试
return backtrack(nums, index + 1, target);
}
}
leetcode 473 火柴拼正方形
你将得到一个整数数组 matchsticks ,其中 matchsticks[i] 是第 i 个火柴棒的长度。你要用 所有的火柴棍 拼成一个正方形。你 不能折断 任何一根火柴棒,但你可以把它们连在一起,而且每根火柴棒必须 使用一次 。
如果你能使这个正方形,则返回 true ,否则返回 false 。
示例 1:
输入: matchsticks = [1,1,2,2,2]
输出: true
解释: 能拼成一个边长为2的正方形,每边两根火柴。
示例 2:
输入: matchsticks = [3,3,3,3,4]
输出: false
解释: 不能用所有火柴拼成一个正方形。
提示:
1 <= matchsticks.length <= 15
1 <= matchsticks[i] <= 108
每次拿到一个物品,都尝试将它放进背包中(背包的顺序是从1到4),在前面的时候,我们做了对物品的排序,使其物品的重量从大到小,这样就可以先尝试放入最大重量的物品,如果最大重量的物品超过了背包的容量,那么肯定就是false,相当于做了一些剪枝优化。如果在尝试的过程中,一个物品在4个背包中都放不进去,加入这个物品,4个背包都会出现超重的情况,那么就说明前面我们4个背包中已经放置好的物品的方案并不合理,就需要回溯,即返回到前面的结果,回退到前面的状态,然后再进行往下的递归尝试。如果尝试完了所有结果,物品刚好可以将所有的背包可以放满,那么就返回true,否则就是不存在返回false.
//对于正方形来说存在4条相等的边,这里抽象成4个背包
//对于所给数组中的元素,这里抽象成所给物品的重量
//则题目转化成了,判断将所有物品装入这四个背包中,能否恰好把背包装满
//每次拿到一个物品就尝试
class Solution {
public:
bool makesquare(vector<int>& matchsticks) {
vector<int> bags(4,0);
int sum = 0;
for(auto ele : matchsticks) {
sum += ele;
}
if(sum % 4 != 0) {
return false;
}
sort(matchsticks.begin(), matchsticks.end(), greater<int>()); //排序优化
return backtracking(0, matchsticks, sum/4, bags);
}
bool backtracking(int i, vector<int>& matchsticks, int capacity,vector<int>& bags) {
if(i >= matchsticks.size()) {
return true;
}
for(int k = 0; k < bags.size(); k++) {
if(bags[k] + matchsticks[i] <= capacity) {
bags[k] += matchsticks[i];
if(backtracking(i+1, matchsticks, capacity, bags)) {
return true;
}
bags[k] -= matchsticks[i];
}
if(bags[k] == 0) return false;
}
return false; //如果不存在一种组合可以使每个背包都正好放下,那么返回false
}
};
在处理过程中,也可以先用物品将一个背包填满,然后这些物品就不能使用了,再使用其他的物品将第二个背包填满,这样当四个背包都填满时,并且所有的物品刚好用完时,返回true。
class Solution {
public:
bool makesquare(vector<int>& matchsticks) {
vector<int> bags(4,0);
int sum = 0;
for(auto ele : matchsticks) {
sum += ele;
}
if(sum % 4 != 0) {
return false;
}
sort(matchsticks.begin(), matchsticks.end(), greater<int>()); //排序优化
vector<bool> used(matchsticks.size(), 0);
return backtracking(0, 4, 0, sum/4, matchsticks, used);
}
bool backtracking(int start, int k, int curSum, int capacity, vector<int>& matchsticks, vector<bool>& used) {
if(k == 0) {
return true;
}
if(curSum == capacity) {
return backtracking(0, k-1, 0, capacity, matchsticks, used);
}
for(int i = start; i < matchsticks.size(); i++) {
if(used[i]) {
continue;
}
if(curSum + matchsticks[i] > capacity) {
continue;
}
used[i] = true;
curSum += matchsticks[i];
if(backtracking(i+1, k, curSum, capacity, matchsticks, used)) {
return true;
}
curSum -= matchsticks[i];
used[i] = false;
}
return false;
}
};
leetcode 241 为运算表达式设计优先级
给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。
生成的测试用例满足其对应输出值符合 32 位整数范围,不同结果的数量不超过 104 。
输入:expression = “2-1-1”
输出:[0,2]
解释:
((2-1)-1) = 0
(2-(1-1)) = 2
链接:https://leetcode.cn/problems/different-ways-to-add-parentheses
class Solution {
public:
vector<int> diffWaysToCompute(string expression) {
vector<int> res;
for (int i = 0; i < expression.size(); i++) {
char c = expression[i];
if (c == '+' || c == '-' || c == '*') {
auto res1 = diffWaysToCompute(expression.substr(0, i));
auto res2 = diffWaysToCompute(expression.substr(i+1));
for (auto num1 : res1) {
for (auto num2 : res2) {
if (c == '+') {
res.push_back(num1 + num2);
}
else if (c == '-') {
res.push_back(num1 - num2);
}
else {
res.push_back(num1 * num2);
}
}
}
}
}
if (res.empty()) { //result数组中没有运算符
res.push_back(stoi(expression));
}
return res;
}
};
leetcode 22 括号生成
解题思路
问题分析:给定一个整数 n,表示生成 n 对括号。
要求生成所有有效的括号组合,例如 n = 2 时,输出 [“(())”, “()()”]。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
回溯法:
1、使用递归生成所有可能的括号组合。
2、在递归过程中,记录当前字符串中左括号和右括号的数量。
3、如果左括号数量小于 n,可以添加一个左括号。
4、如果右括号数量小于左括号数量,可以添加一个右括号。
5、当字符串长度达到 2 * n 时,将其加入结果集。
剪枝优化:在递归过程中,确保左括号数量不超过 n,右括号数量不超过左括号数量。
class Solution {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
backtrack(result, "", 0, 0, n);
return result;
}
private void backtrack(List<String> result, String current, int open, int close, int max) {
// 如果当前字符串长度达到 2 * max,加入结果集
if (current.length() == 2 * max) {
result.add(current);
return;
}
// 如果左括号数量小于 max,可以添加一个左括号
if (open < max) {
backtrack(result, current + "(", open + 1, close, max);
}
// 如果右括号数量小于左括号数量,可以添加一个右括号
if (close < open) {
backtrack(result, current + ")", open, close + 1, max);
}
}
}
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> result;
generate("", n, n, result);
return result;
}
private:
// item为生成字符串,left当前还可以放置左括号的数目,right为当前可以放置右括号的数目
void generate(string item, int left, int right, vector<string> &result){
if(left == 0 && right == 0){
result.push_back(item);
return;
}
if(left > 0){
generate(item + '(', left - 1, right, result);
}
if(left < right){
generate(item + ')', left, right - 1, result);
}
}
};
leetcode 79 单词搜索
LeetCode 79 题要求在一个二维字符网格中搜索是否存在一个单词,单词可以由相邻单元格的字母组成。这是一个典型的回溯问题,可以通过深度优先搜索(DFS)和回溯来解决。以下是详细的解题思路和 Java 代码实现。
解题思路
问题分析:
给定一个二维字符网格 board 和一个单词 word。
判断是否可以在网格中找到一条路径,使得路径上的字母按顺序组成单词。
路径可以由相邻单元格(上下左右)的字母组成,且每个单元格的字母只能使用一次。
回溯法:
遍历网格中的每个单元格,作为搜索的起点。
对于每个起点,使用 DFS 尝试匹配单词的每个字符。
在 DFS 过程中,如果当前字符匹配,则继续搜索相邻单元格。
如果匹配失败,则回溯到上一步,尝试其他路径。
剪枝优化:
使用一个 visited 数组记录已经访问过的单元格,避免重复使用。
如果当前字符不匹配,直接剪枝。
class Solution {
public boolean exist(char[][] board, String word) {
if (board == null || board.length == 0 || board[0].length == 0) {
return false;
}
int rows = board.length;
int cols = board[0].length;
boolean[][] visited = new boolean[rows][cols]; // 记录访问状态
// 遍历每个单元格,作为搜索的起点
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (dfs(board, i, j, word, 0, visited)) {
return true;
}
}
}
return false;
}
private boolean dfs(char[][] board, int i, int j, String word, int index, boolean[][] visited) {
// 如果已经匹配完单词的所有字符,返回 true
if (index == word.length()) {
return true;
}
// 检查边界条件和当前字符是否匹配
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length ||
visited[i][j] || board[i][j] != word.charAt(index)) {
return false;
}
// 标记当前单元格为已访问
visited[i][j] = true;
// 向四个方向递归搜索
boolean found = dfs(board, i + 1, j, word, index + 1, visited) ||
dfs(board, i - 1, j, word, index + 1, visited) ||
dfs(board, i, j + 1, word, index + 1, visited) ||
dfs(board, i, j - 1, word, index + 1, visited);
// 回溯,撤销当前单元格的访问标记
visited[i][j] = false;
return found;
}
}