回溯算法总结

全排列

横向做选择,纵向搜索下一层的元素,直到集合中包含所有元素。
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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值