全排列
横向做选择,纵向搜索下一层的元素,直到集合中包含所有元素。
used数组用于控制每层中元素的互斥使用,例如,第一层已经选择了1,将used[0]=true。 第二层根据used数组继续判断每个元素是否可以用。
#include<bits/stdc++.h>
using namespace std;
vector<vector<int>> result;
void backtracking(vector<int> &nums,vector<int> &path,vector<bool> &used,int i,int n)
{
if(i==n)
{
result.push_back(path);
return;
}
for(int j=0;j<n;j++)
{
if(used[j])
{
continue;
}
used[j] = true;
path.push_back(nums[j]);
backtracking(nums,path,used,i+1,n);
used[j] = false;
path.pop_back();
}
}
int main()
{
vector<int> v = {1,2,3};
int n = v.size();
vector<int> path;
vector<bool> used(n,false);
backtracking(v,path,used,0,n);
for(auto vec : result)
{
for(auto num : vec)
{
cout<< num << " ";
}
cout << endl;
}
}
如果数据中有重复的数据,枚举出来的排列会有重复值,需要先对数据进行排序,并在回溯时做剪枝
#include<bits/stdc++.h>
using namespace std;
vector<vector<int>> result;
void backtracking(vector<int> &nums,vector<int> &path,vector<bool> &used,int i,int n)
{
if(i==n)
{
result.push_back(path);
return;
}
for(int j=0;j<n;j++)
{
//添加剪枝
if(used[j] || (i>0 && !used[i-1] && nuns[i]==nums[i-1]))
{
continue;
}
used[j] = true;
path.push_back(nums[j]);
backtracking(nums,path,used,i+1,n);
used[j] = false;
path.pop_back();
}
}
int main()
{
vector<int> v = {1,2,3};
int n = v.size();
vector<int> path;
vector<bool> used(n,false);
//排序
sort(v.begin(),v.end());
backtracking(v,path,used,0,n);
for(auto vec : result)
{
for(auto num : vec)
{
cout<< num << " ";
}
cout << endl;
}
}
回溯具体执行过程
package backtrack;
import com.sun.org.apache.xpath.internal.operations.Bool;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class lc46 {
public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
// 使用一个动态数组保存所有可能的全排列
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
// boolean[] used = new boolean[len];
List<Boolean> used = new ArrayList<>();
for (int i=0;i<len;i++)
{
used.add(false);
}
List<Integer> path = new ArrayList<>();
dfs(nums, len, 0, path, used, res);
return res;
}
private void dfs(int[] nums, int len, int depth,
List<Integer> path, List<Boolean> used,
List<List<Integer>> res) {
if (depth == len) {
res.add(new ArrayList<>(path));
return;
}
// 在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。
for (int i = 0; i < len; i++) {
if (!used.get(i)) {
path.add(nums[i]);
used.set(i, true);
System.out.println("前往 depth-> "+(depth+1)+"层 dfs 前=>:"+path+" used:"+used);
dfs(nums, len, depth + 1, path, used, res);
// 注意:下面这两行代码发生 「回溯」,回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
used.set(i, false);
path.remove(path.size() - 1);
System.out.println("回退至 depth->"+(depth)+"层 dfs 后=>:"+path+" used:"+used);
}
}
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
lc46 solution = new lc46();
List<List<Integer>> lists = solution.permute(nums);
System.out.println(lists);
depth=0 i=1
第 1 层 第 1 个元素
前往 depth-> 1层 dfs 前=>:[1] used:[true, false, false]
depth=1 i=1 //used[i]保证元素在不同层使用互斥
depth=1 i=2
第 2 层 第 2 个元素
前往 depth-> 2层 dfs 前=>:[1, 2] used:[true, true, false]
depth=2 i=1
depth=2 i=2
depth=2 i=3
第 3 层 第 3 个元素
前往 depth-> 3层 dfs 前=>:[1, 2, 3] used:[true, true, true]
回退至 depth->2层 dfs 后=>:[1, 2] used:[true, true, false]
回退至 depth->1层 dfs 后=>:[1] used:[true, false, false]
depth=1 i=3
第 2 层 第 3 个元素
前往 depth-> 2层 dfs 前=>:[1, 3] used:[true, false, true]
depth=2 i=1
depth=2 i=2
第 3 层 第 2 个元素
前往 depth-> 3层 dfs 前=>:[1, 3, 2] used:[true, true, true]
回退至 depth->2层 dfs 后=>:[1, 3] used:[true, false, true]
depth=2 i=3
回退至 depth->1层 dfs 后=>:[1] used:[true, false, false]
回退至 depth->0层 dfs 后=>:[] used:[false, false, false]
depth=0 i=2
第 1 层 第 2 个元素
前往 depth-> 1层 dfs 前=>:[2] used:[false, true, false]
depth=1 i=1
第 2 层 第 1 个元素
前往 depth-> 2层 dfs 前=>:[2, 1] used:[true, true, false]
depth=2 i=1
depth=2 i=2
depth=2 i=3
第 3 层 第 3 个元素
前往 depth-> 3层 dfs 前=>:[2, 1, 3] used:[true, true, true]
回退至 depth->2层 dfs 后=>:[2, 1] used:[true, true, false]
回退至 depth->1层 dfs 后=>:[2] used:[false, true, false]
depth=1 i=2
depth=1 i=3
第 2 层 第 3 个元素
前往 depth-> 2层 dfs 前=>:[2, 3] used:[false, true, true]
depth=2 i=1
第 3 层 第 1 个元素
前往 depth-> 3层 dfs 前=>:[2, 3, 1] used:[true, true, true]
回退至 depth->2层 dfs 后=>:[2, 3] used:[false, true, true]
depth=2 i=2
depth=2 i=3
回退至 depth->1层 dfs 后=>:[2] used:[false, true, false]
回退至 depth->0层 dfs 后=>:[] used:[false, false, false]
depth=0 i=3
第 1 层 第 3 个元素
前往 depth-> 1层 dfs 前=>:[3] used:[false, false, true]
depth=1 i=1
第 2 层 第 1 个元素
前往 depth-> 2层 dfs 前=>:[3, 1] used:[true, false, true]
depth=2 i=1
depth=2 i=2
第 3 层 第 2 个元素
前往 depth-> 3层 dfs 前=>:[3, 1, 2] used:[true, true, true]
回退至 depth->2层 dfs 后=>:[3, 1] used:[true, false, true]
depth=2 i=3
回退至 depth->1层 dfs 后=>:[3] used:[false, false, true]
depth=1 i=2
第 2 层 第 2 个元素
前往 depth-> 2层 dfs 前=>:[3, 2] used:[false, true, true]
depth=2 i=1
第 3 层 第 1 个元素
前往 depth-> 3层 dfs 前=>:[3, 2, 1] used:[true, true, true]
回退至 depth->2层 dfs 后=>:[3, 2] used:[false, true, true]
depth=2 i=2
depth=2 i=3
回退至 depth->1层 dfs 后=>:[3] used:[false, false, true]
depth=1 i=3
回退至 depth->0层 dfs 后=>:[] used:[false, false, false]
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
子集
#include<bits/stdc++.h>
using namespace std;
vector<vector<int>> result;
void backtracking(vector<int> &nums,vector<int> &path,vector<bool> &used,int i,int n)
{
result.push_back(path);
for(int j=i;j<n;j++)
{
path.push_back(nums[j]);
backtracking(nums,path,used,j+1,n);
path.pop_back();
}
}
int main()
{
vector<int> v = {1,2,3};
int n = v.size();
vector<int> path;
vector<bool> used(n,false);
backtracking(v,path,used,0,n);
for(auto vec : result)
{
for(auto num : vec)
{
cout<< num << " ";
}
cout << endl;
}
}
=== 2023.3.1更新 ===
回溯法总结
//1.子集型回溯
//78.子集
/*
给定一个数组,输出数组所有的子集
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
分析: 子集,需要记录在递归路径上所有的path, 并将path加入结果集中
*/
class Solution1 {
private:
vector<vector<int>> ans;
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<int> path;
dfs(nums, path, 0);
return ans;
}
void dfs(vector<int> &nums, vector<int> &path, int j) {
//下面这个if条件为什么可以不加?
if (j > nums.size()) {
return;
}
//纵向添加一个元素
ans.push_back(path);
//横向遍历多叉树
for (int i = j; i < nums.size(); i++) {
path.push_back(nums[i]);
dfs(nums, path, i+1);
path.pop_back();
}
}
};
// 131. 分割回文串
/*
问题:给定一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
分析: 分割回文串,需要枚举每个满足条件的分割位置,用|分割, 满足条件的aab分割
aab
a| aa|b aab|
a|a|b aa|b aa|b
问题2: 满足条件,只有当枚举出来的子字符串是回文串,才能开始枚举下一个子串
判断子串是否是回文字符串, 用相向双指针法,或者把字符串反转过来
*/
class Solution3 {
private:
vector<vector<string>> ans;
public:
vector<vector<string>> partition(string s) {
vector<string> path;
dfs(s, path, 0);
return ans;
}
void dfs(string &s, vector<string> &path, int j) {
int n = s.size();
if (j == n) {
//每个字母都必须在答案中,因此,需要等到都分割完了,再加入结果集
ans.push_back(path);
return;
}
for (int i = j; i < n; i++) {
string substr = s.substr(j,i-j+1);
if (substr != string(substr.rbegin(), substr.rend())) {
continue;
}
path.push_back(substr);
dfs(s, path, i+1);
path.pop_back();
}
}
};
//2.组合型回溯 , 找到满足某一条件的路径组合
//77. 组合
/*
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
思路:回溯, 用一个path数组记录路径上的数,当path.size() == 2 时, 加入到结果集
从数字1开始递归,回溯终止条件: 遍历的下边 > n
*/
class Solution {
private:
vector<vector<int>> ans;
public:
vector<vector<int>> combine(int n, int k) {
vector<int> path;
dfs(n, k, 1, path);
return ans;
}
void dfs(int n, int k, int cur, vector<int> &path) {
if (k == path.size()) {
ans.push_back(vector<int>(path.begin(), path.end()));
return;
}
if (cur > n) {
return;
}
for (int i = cur; i <= n; i++) {
path.push_back(i);
dfs(n,k,i+1,path);
path.pop_back();
}
}
};
//剑指 Offer II 104. 排列的数目
/*
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
思路: 回溯 + 记忆化搜索,
从target自顶往下搜索,当target <= 0时返回
每次需要记录当前搜索的targte的方案数是多少,然后再递归返回的时候将组成当前层的target的方案数返回给上一层的数(target+nums[i])
构建一棵递归树
4
-1/ |-2 \-3
3 2 1
-1/ -2| -3\
2 1 0
-1/ -2| -3\
1 0 -1
-1 /-2|-3\
0 -1 -2
剪枝:
对nums进行升序排序, 当target - nums[i] < 0, 跳出循环,放弃枚举下一个数,因为nums[i] < nums[i+1]; target-nums[i+1] < target-nums[i] < 0
*/
class Solution {
private:
int memo[1005];
public:
int combinationSum4(vector<int>& nums, int target) {
memset(memo, -1, sizeof(memo));
dfs(nums, target);
return memo[target];
}
int dfs(vector<int> &nums, int target) {
if (target <= 0) {
if (target == 0) {
return 1;
}
return 0;
}
if (memo[target] != -1) {
return memo[target];
}
int n = nums.size();
//初始化组成target的方案数为0
memo[target] = 0;
for (int i = 0; i < n; i++) {
memo[target] += dfs(nums, target-nums[i]);
}
return memo[target];
}
};
/*
414 目标和:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
思路1:回溯法,从0开始加和减, 递归达到target, 结果+1, 回溯返回的条件是树的深度(数组下标)为n时,不能继续遍历下一个数
返回条件:当前遍历的数的下标i == 数组长度n
子问题:dfs(nums, i+1, sum + nums[i], target) 和 dfs(nums, i+1, sum-nums[i], target)
注意: 需要所有所有数的参与,因此,需要i==n时,才能统计答案
使用down-top的方式画出递归搜索树
0
+1/ -1\
1 -1
+1/-1\
2 0
+1/-1\
3 1 i=3, sum == target, 但是还没有完成所有数的搜索,走的是从根节点到叶子节点的路径
+1/-1\
4 2
+1/-1\
5 3 i=5, sum == targte, 走到根节点,4-1=3, 满足条件; ans += 1;
问题1:求解的目标是否需要数组的所有元素的参与?
是 : 遍历完所有的数
否: 用路径上的值求解
问题2:搜索是top-down 还是 down-top ?
top-down: 原问题 dfs(n), 子问题dfs(n-1), 递归停止条件 n < 0
down-top: 原问题 dfs(i), 子问题dfs(i+1), 递归停止条件 i == n
参考104题
下一层可选的数是所有的数,还是当前层(下标)的下一个数?
使用top-down的形式
104 题是可选所有数,即数字可以重复使用,每层往下一层遍历做选择都是,遍历一个长度为n的多叉树
4
-1/ -2| -3\
3 2 1
-1/ -2| -3\
2 1 0
-1/ -2\ -1|
1 0 0
思路2: 记忆化搜索
记录每个sum的组合个数,由于到达下标i时, sum可正可负; 存的应该是 memo[i_sum] = value; key为i_sum, 即下标_sum的对应的组合个数
需要在每一层,搜索sum的组合种数
key = to_string(i)+"_"+to_string(sum);
res = 0;
res += dfs(nums, i+1, sum+nums[i], target);
res += dfs(nums, i-1, sum-nums[i], target);
memo[key] = res;
return res;
*/
class Solution {
int ans = 0;
unordered_map<string, int> memo;
public:
//方法1: 回溯法
int findTargetSumWays1(vector<int>& nums, int target) {
dfs1(nums, 0, 0, target);
return ans;
}
void dfs1(vector<int> &nums, int i , int sum, int target) {
int n = nums.size();
if (i == n) {
//细节:需要所有元素都包括进来,如果+-所有元素的和==target, 则ans += 1;
if (target == sum) {
ans += 1;
return;
}
return;
}
dfs1(nums, i+1,sum+nums[i], target);
dfs1(nums, i+1,sum-nums[i], target);
return;
}
//方法2:记忆化搜索
int findTargetSumWays2(vector<int>& nums, int target) {
return dfs2(nums, 0, 0, target);
}
int dfs2(vector<int> &nums, int i , int sum, int target) {
string key = to_string(i) + "_" + to_string(sum);
if (memo.find(key) != memo.end()) { //sum 可能为-1,需要添加idx, idx_sum
return memo[key];
}
int n = nums.size();
if (i == n) {
//需要所有元素都包括进来,如果+-所有元素的和==target, 则ans += 1;
if (target == sum) {
// ans += 1;
return 1;
}
return 0;
}
int res = 0;
res += dfs2(nums, i+1,sum+nums[i], target);
res += dfs2(nums, i+1,sum-nums[i], target);
memo[key] = res;
return res;;
}
};
//回溯算法的时间复杂度 = sum (叶子节点数 * 叶子节点到根节点的路径长度)
参考: https://www.bilibili.com/video/BV1xG4y1F7nC/?p=15&spm_id_from=pageDriver