LeetCode 【数据结构与算法专栏】【回溯算法】

递归回溯算法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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值