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 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();
		}
	}
};

leetcode 46 全排列

给定一个不含重复数字的数组 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 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个相等的子集

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
    }
};

leetcode 473 火柴拼正方形

每次拿到一个物品,都尝试将它放进背包中(背包的顺序是从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;
    }
};
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要学好数据结构与算法,以下是一些建议和方法: 1. 充分理解数据结构和算法的概念:数据结构指的是一组数据的存储结构,而算法是操作数据的一组方法。数据结构为算法提供了基础和支持。[1] 2. 学习相关课程或教材:有很多优秀的在线课程和教材可供学习,如《算法与数据结构之美》等。可以通过这些课程和教材系统地学习数据结构和算法的知识。 3. 理解常用的数据结构和算法:常用的数据结构包括数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Tire树等。常用的算法包括递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法回溯算法、动态规划、字符串匹配算法等。 4. 学习算法的复杂度分析:了解算法的时间复杂度和空间复杂度,掌握用大O复杂度表示法来评估算法的性能。这有助于选择最优的算法来解决问题。 5. 实践和练习:通过编写代码实现数据结构和算法,解决实际的问题。可以参加编程竞赛、刷LeetCode等平台上的题目,不断进行实践和练习,提高自己的编程能力和算法思维。 6. 参考优秀的开源项目和代码:学习优秀的开源项目和代码,可以帮助理解和应用不同的数据结构和算法。 7. 与他人讨论和交流:参加算法交流群、论坛或社区,与其他人一起讨论和交流学习经验,互相帮助和分享知识。 总而言之,学好数据结构与算法需要不断学习和实践,理论与实践相结合。通过深入理解数据结构和算法的概念,学习常用的数据结构和算法,分析算法的复杂度,进行实践和练习,与他人交流,可以逐步提升自己的数据结构与算法水平。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值