【算法进阶】回溯(backtracking)基本逻辑,以及常见回溯问题(全排列、解数独、八皇后)

news2024/9/23 19:25:11

文章目录

    • 一、引言
    • 二、回溯法基本逻辑
    • 三、回溯法代码模板
    • 三、回溯法常见问题
      • 3.1 组合
        • 逻辑
        • 代码
      • 3.2 子集
        • 逻辑
        • 代码
      • 3.3 子集Ⅱ(未解答)
        • 逻辑
        • 代码
      • 3.4 分割回文串
        • 逻辑
        • 代码
      • 3.5 组合总和Ⅰ
        • 逻辑
        • 代码
      • 3.6 组合总和Ⅱ(未解答)
        • 逻辑
        • 代码
      • 3.7 组合总和Ⅲ
        • 逻辑
        • 代码
      • 3.8 全排列Ⅰ
        • 逻辑
        • 代码
        • 优化
        • 代码
        • 优化
        • 代码
      • 3.9 全排列Ⅱ(未解答)
        • 逻辑
        • 代码
      • 3.10 解数独(未解答)
        • 逻辑
        • 代码
      • 3.11 N皇后Ⅰ(未解答)
        • 逻辑
        • 代码
      • 3.12 N皇后Ⅱ(未解答)
        • 逻辑
        • 代码

一、引言

许多情况下,回溯算法相当于暴力搜索的方式 进行实现,性能一般不理想。但是对于某些问题,就算采用最复杂的时间复杂度能求得结果,也是一种里程碑式的进步。

回溯算法一个实际的具体例子就是在一套新房子内摆放家具的问题。
开始什么也不拜访,之后是每件家具被摆放到室内的某个部分,如果所有家具都摆好且都满意,则算法结束。
如果摆放了某一个家具之后,但是对于当前拜访的方式不理想,那么我们必须撤销这一步,尝试其他的摆放方式。如果我们一直撤销,直到撤销到第一个摆放的家具,那么不存在满意的家具摆放的方法;否则我们将在满意的摆放位置上结束算法。
在摆放过程中,直接不去考虑某些必然不满意的摆放方法,例如将沙发摆进厨房必然是不满意的摆放方法。这种直接不考虑不合理子集的方式就叫做 剪枝

二、回溯法基本逻辑

回溯的原理就是采用递归的方式 ,将问题看作为解空间树的形式。注意是看作空间树的形式,具体的数据结构可能是列表、树、字符串等等
在解空间树中,按照深度优先的方式,从根结点出发进行搜索,搜索至任意结点时,先判断该结点是否包含问题的解①如果不包含,则向祖先节点回溯(即退出该层递归)②如果包含,则进入该子树,继续进行深度优先。

三、回溯法代码模板

// t表示当前递归深度
// n为树高,用来控制递归深度
// f(n,t)表示在当前扩展结点处未搜索过的子树的起始编号
// g(n,t)表示在当前扩展结点处未搜索过的子树的终止编号
// h(i)表示在当前扩展结点处x[t]的第i个可选值
// Constraint(t)表示在当前扩展节点处的约束函数
// Bound(t)表示在当前扩展节点处的界限函数
void backtracking(int t){
	if(t>n){//表示搜索到了叶节点
		outPut(x);//输出可行解x
	}
	else{
		// 
		for(int i = f(n,t) ; i <= g(n,t) ; i++){
			x[t] = h(i); // 收集结果
			if(Constraint(t)&&Bound(t)){// 通过约束和界限进行剪枝操作,满足时才继续向下递归
				backtracking(t+1);
			}
		}
	}
}

三、回溯法常见问题

3.1 组合

力扣题库序号77

逻辑

以n = 4, k=2为例
在这里插入图片描述

代码

class Solution {
public:
	// 最终返回结果列表
    vector<vector<int>> rst;
    // 每一次结果
    vector<int> path;
	
	// n: 数字1~n进行组合	k:组合列表的大小    
    vector<vector<int>> combine(int n, int k) {
        backtracking(n,k,1);
        return rst;
    }
    
    // 从[startIndex,n]中找到大小为k的排列
    // 例如:backtracking(n,k,1)   n: 数字1~n进行组合	k:组合列表的大小   1表示从第1个数开始
    void backtracking(int n,int k,int startIndex){
        // 如果当前路径大小等于组合列表的大小,表示收集到了叶节点,那么push入最后的结果
        if(path.size()==k){
            rst.push_back(path);
            return;
        }
        // 从startIndex~n
        for(int i=startIndex;i<n+1;i++){
            // 每一层将i放入
            path.push_back(i);
            // 继续从递归(从[i+1,n]中找到剩余大小的排列)
            backtracking(n,k,i+1);
            // 退出该层时将i移出,就是回溯的过程
            path.pop_back();
        }
        return ;
    }
};

可以输出path,查看path收集数据的变化。

3.2 子集

力扣题库序号78

逻辑

组合和子集区别就是,子集不限制大小,而组合规定了大小。
表现在实现的代码中就是,子集需要在递归出口时收集结果,而组合需要在数据变换后就收集结果。

在代码实现时需要注意,本题的集合是已经给出的非连续的数字列表;而上一题的组合题是给出一个数字范围。
在这里插入图片描述

代码

class Solution {
public:
    // 最终返回结果列表
    vector<vector<int>> rst;
    // 每一次结果
    vector<int> path;
    
    vector<vector<int>> subsets(vector<int>& nums) {
        rst.push_back({});
        backtracking(nums,0);
        return rst;
    }
    
    void backtracking(vector<int> &nums,int startIndex){
        for(int i = startIndex;i<nums.size();i++){
            path.push_back(nums[i]);
            // 在每一次数据变化时进行结果的收集。
            rst.push_back(path);
            // 继续从递归(i+1之后的组合)
            backtracking(nums,i+1);
            // 退出该层时将nums[i]移出,就是回溯的过程
            path.pop_back();
        }
        return;
    }
};

3.3 子集Ⅱ(未解答)

力扣题库序号90

逻辑

代码

class Solution {
public:
    // 最终返回结果列表
    vector<vector<int>> rst;
    // 每一次结果
    vector<int> path;
    
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        rst.push_back({});
        sort(nums.begin(),nums.end());
        backtracking(nums,0);
        return rst;
    }

    void backtracking(vector<int> &nums,int startIndex){
        for(int i = startIndex;i<nums.size();i++){
            if(i==startIndex||nums[i]!=nums[i-1]){
                path.push_back(nums[i]);
                // 在每一次数据变化时进行结果的收集。
                rst.push_back(path);
                // 继续从递归(i+1之后的组合)
                backtracking(nums,i+1);
                // 退出该层时将nums[i]移出,就是回溯的过程
                path.pop_back();
            }
        }
        return;
    }
};

3.4 分割回文串

力扣题库序号131

逻辑

这类题通常需要分析切割的逻辑
切割范围就是每一次的startIndex~i的范围,注意startIndex≠i,因为i是在不断变化的
剪枝的操作就是本次切割的范围产生的字符串并不是回文子串,那么本次切割不再考虑,直接考虑i+1后startIndex~i切割的字符串。
例如aabb,

第一次切割0 ~ 1 =>“a”第二次切割1 ~ 2 => “a”第三次切割2 ~ 3 => “b”第四次切割3 ~ 4 => “b”产生【“a”,“a”,“b”,“b”】
-第二次切割1 ~ 3 => “ab”
“ab”不是回文,之后不管怎么切都不可能符合要求,因此之后的分支就不再考虑,这就是剪枝操作
-第二次切割1 ~ 4 => “abb”
"abb"不是回文,之后不管怎么切都不可能符合要求,剪枝。
第一次切割0 ~ 2 => “aa”第二次切割2 ~ 3 => “b”第三次切割3 ~ 4 => “b”产生【“aa”,“b”,“b”】
第一次切割0 ~ 3 => “aab”
"aab"不是回文,之后不管怎么切都不可能符合要求,剪枝
第一次切割0-4 => “aabb”产生【“aabb”】

手绘以aab为例
在这里插入图片描述

代码

class Solution {
public:
    // 最终返回结果列表
    vector<vector<string>> rst;
    // 每一次结果
    vector<string> path;
    vector<vector<string>> partition(string s) {
        backtracking(s,0);
        return rst;
    }
    void backtracking(string s, int startIndex){
        // 递归出口:割到最后一位,收集结果
        if(startIndex>=s.size()){
            rst.push_back(path);
        }
        for(int i=startIndex;i<s.size();i++){
            string a;
            // a = s[startIndex~i]
            for(int k=startIndex;k<i+1;k++){
                a += s[k];
            }
            // 剪枝操作,如果是回文串才加入并且向后切割,否则不再向下递归
            if(isCycle(a)){
                // 将已经切的回文字符串加入结果,并且递归切割从i之后开始的字符串
                path.push_back(a);
                backtracking(s,i+1);
                // 退出该层时将a移出,就是回溯的过程
                path.pop_back();
            }
        }
    }
    // 判断是否回文
    bool isCycle(string s){
        for(int i=0;i<s.size()/2;i++){
            if(s[i]!=s[s.size()-i-1])return false;
        }
        return true;
    }
};

3.5 组合总和Ⅰ

力扣题库序号39

逻辑

代码

class Solution {
public:
    // 最终返回结果列表
    vector<vector<string>> rst;
    // 每一次结果
    vector<string> path;
    
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtracking(candidates, target, 0);
        return rst;
    }
    // candidates 从 startIndex开始 寻找总和为 target 的组合
    void backtracking(vector<int>& candidates, int target, int startIndex){
    	// 如果要求总和为0,代表path内的数字已经满足总和
        if(target==0){
            rst.push_back(path);
            return;
        }else{
            for(int i=startIndex;i<candidates.size();i++){
            	// 剪枝操作:当candidate[i] <= target时才满足,
            	// 			否则代表本次组合必然不符合target, 因为没有candidate中的数均为正数
                if(target>=candidates[i]){
                    path.push_back(candidates[i]);
                    // 因为可以重复选取,因此还是从i开始;如果不能重复,则从i+1开始。
                    backtracking(candidates,target-candidates[i],i);
                    path.pop_back();  
                }
            }
        }
    }
};

3.6 组合总和Ⅱ(未解答)

力扣题库序号40

逻辑

去重

代码

class Solution {
public:
    vector<vector<int>> rst;
    vector<int> path;
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        backtracking(candidates,target,0);
        return rst;
    }
    
    void backtracking(vector<int>& candidates, int target,  int startIndex){
        if(target==0){
            rst.push_back(path);
            return;
        }else{
            for(int i = startIndex;i<candidates.size()&&candidates[i]<=target;i++){
                if(i==startIndex||candidates[i]!=candidates[i-1]){
                    path.push_back(candidates[i]);
                    backtracking(candidates,target-candidates[i],i+1);
                    path.pop_back();
                }
            }
        }
    }
};

3.7 组合总和Ⅲ

力扣题库序号216

逻辑

代码

class Solution {
public:
    vector<vector<int>> rst;
    vector<int> path;
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k, n ,1);
        return rst;
    }
    void backtracking(int k, int n ,int startIndex){
        if(n==0&&k==0){
            rst.push_back(path);
            return;
        }
        if(n!=0&&k!=0){
            for(int i=startIndex;i<10;i++){
                if(n>=i){
                    path.push_back(i);
                    backtracking(k-1, n-i,i+1);
                    path.pop_back();  
                }
            }
        }
    }
};

3.8 全排列Ⅰ

力扣题库序号46

逻辑

选择没有选取过的元素作为path数组,当path数组大小等于nums的大小的时候进行结果收集。
因此思路就是建立used数组标记是否使用过nums中的某个元素。
在这里插入图片描述

代码

// 通过used标记使用过的元素
class Solution {
public:
    // 最终返回结果列表
    vector<vector<int>> rst;
    // 每一次结果
    vector<int> path;

    vector<vector<int>> permute(vector<int>& nums) {
    	// 标记nums中使用过的元素
        vector<bool> used(nums.size(),false);
        backtracking(nums,used);
        return rst;
    }
    void backtracking(vector<int>& nums, vector<bool> used){
    	// 当path数组大小==nums数组大小的时候,表示元素已经被全部使用
        if(path.size()==nums.size()){
            rst.push_back(path);
            return ;
        }else{
            for(int i=0;i<nums.size();i++){
    			// 如果没有被使用,则加入path数组,并且对used进行标记
                if(used[i]==false){
                    path.push_back(nums[i]);
                    used[i] = true;
                    // 向下递归
                    backtracking(nums,used);
                    // 回溯
                    used[i] = false;
                    path.pop_back();
                }
            }
        }
    }
};

优化

在这里插入图片描述

本题的思路就是如何判断某元素是否被使用,因此可以通过交换元素在nums中的位置来判断元素是否被使用
nums中靠前的元素被使用,靠后的元素没有被使用,被使用元素的个数就是path数组的大小。
因此backtracking调用时,startIndex使用当前path数组的大小。

代码

// 将使用过的元素放置
class Solution {
public:
    // 最终返回结果列表
    vector<vector<int>> rst;
    // 每一次结果
    vector<int> path;
    
    vector<vector<int>> permute(vector<int>& nums) {
        backtracking(nums,0);
        return rst;
    }
    
    void backtracking(vector<int>& nums, int startIndex){
        if(startIndex==nums.size()){
            rst.push_back(path);
            return ;
        }else{
            for(int i=startIndex;i<nums.size();i++){
            	// 将使用过的nums[i]前置
                path.push_back(nums[i]);
                swap(nums[startIndex],nums[i]);
                
                backtracking(nums,path.size());
                
                // 回溯:移出元素并且恢复原来的元素位置关系
                swap(nums[startIndex],nums[i]);
                path.pop_back();
            }
        }
    }

};

优化

继续优化,backtracking被调用时其startIndex均为调用函数的startIndex+1,
而最后只需要startIndex指向最后一位时,就可以进行元素收集,因为nums是进行交换产生的,那么直接收集当前nums的元素就可以。
在这里插入图片描述

代码

class Solution {
public:
    vector<vector<int>> rst;

    vector<vector<int>> permute(vector<int>& nums) {
        backtracking(nums,0);
        return rst;
    }
    
    void backtracking(vector<int>& nums, int startIndex){
    	// 交换至最后一位进行结果的收集
        if(startIndex==nums.size()){
            rst.push_back(nums);
            return ;
        }else{
            for(int i=startIndex;i<nums.size();i++){
                swap(nums[startIndex],nums[i]);
                backtracking(nums,startIndex+1);// 被调用的startIndex参数 = 调用函数的startIndex+1
                swap(nums[startIndex],nums[i]);
            }
        }
    }

};

3.9 全排列Ⅱ(未解答)

力扣题库序号47

逻辑

代码

class Solution {
public:
    vector<vector<int>> rst;
    vector<int> path;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        vector<int> used(nums.size(),0);
        backtracking(nums,0,used);
        return rst;
    }
    void backtracking(vector<int>& nums, int startIndex, vector<int> used){
        if(startIndex == nums.size()){
            rst.emplace_back(path);
            return;
        }
        for(int i = 0; i < (int)nums.size(); i++){

            // if(used[i] == 1)continue;

            // if(i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){
            //     continue;
            // }

            if(i==0||nums[i-1]!=nums[i]||used[i-1]==1){
                if(used[i]==0){
                    path.emplace_back(nums[i]);
                    used[i] = 1;
                    backtracking(nums,startIndex+1,used);
                    path.pop_back();
                    used[i] = 0;
                }
            }

        }
    }
};

3.10 解数独(未解答)

力扣题库序号37

逻辑

代码

3.11 N皇后Ⅰ(未解答)

力扣题库序号51

逻辑

代码

3.12 N皇后Ⅱ(未解答)

力扣题库序号52

逻辑

代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/392381.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mycat

Mycat 1.概述 1.Mycat是数据中间件2.中间件:连接软件组件和应用的计算机软件,便于软件和各部件的交互3.数据中间件:连接Java应用程序与数据库的软件2.适用场景 1.Java与数据库紧耦合(直接连接)2.高访问量高并发对数据库压力(集群)3.读写请求数据不一致(读写分离+主从复制)3.…

关于Gooey复选框CheckBox的使用

折腾了我一下午 官网也没发现具体的使用方法 老是报错 索引超出范围 我就很疑惑 百度也没有答案后来我修改成了非必参 加-- 这是不选中操作这是选中操作他说必须要有一个参数 我有啊 没搞懂 后来 我就这样(根据他报错提示来的)果真就没了问题这样也没问题 具体我还是没搞懂 反正…

K_A16_001 基于STM32等单片机驱动HX711称重模块 串口与OLED0.96双显示

K_A16_001 基于STM32等单片机驱动HX711称重模块 串口与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明对应程序:四、部分代码说明1、接线引脚定义1.1、STC89C52RCHX711称重模块1.2、STM32F103C8T6HX711称重模块五、基础知识学习与相关资料下载六、视频效果展…

项目实战典型案例17——环境混用来带的影响

环境混用来带的影响一&#xff1a;背景介绍背景出现的事故二&#xff1a;思路&方案环境混用的危害如何彻底避免环境混用的问题四&#xff1a;总结五&#xff1a;升华一&#xff1a;背景介绍 本篇博客是对对项目开发中出现的环境混用来带的影响进行的总结并进行的改进。目的…

你想知道的OSPF协议知识点都在这里了

1、OSPF协议概述 1)为什么需要动态路由协议&#xff1f; 静态路由是由工程师手动配置和维护的路由条目&#xff0c;命令行简单明确&#xff0c;适用于小型或稳定的网络。静态路由有以下问题&#xff1a;a)无法适应规模较大的网络&#xff1a;随着设备数量增加&#xff0c;配置量…

SpringBoot学习笔记(三)整合Logback日志框架

一、日志框架介绍1、常见日志框架目前我们常见的日志框架为Log4j、Log4j2、Logback这3种&#xff0c;并且现在很多的工具包里面都会自带日志框架&#xff0c;因此我们使用要格外小心日志框架的冲突。2、三种日志框架之间的关系最先有Log4j&#xff0c;然后因为Log4j有很大的性能…

uniapp系列-图文并茂手把手教你hbuilder进行uniapp云端打包 - 安心打包

什么是安心打包 提交App的模块配置信息到云端&#xff0c;在云端打包机生成原生代码包 为什么使用云打包 更安全&#xff1a;打包时不提交应用代码、证书等信息更快速&#xff1a;非首次打包时不用提交云端打包机排队等待&#xff0c;本地直接出包省流量&#xff1a;减少了打…

Linux开发环境配置--正点原子阿尔法开发板

Linux开发环境配置–正点原子阿尔法开发板 文章目录Linux开发环境配置--正点原子阿尔法开发板1.网络环境设置1.1添加网络适配器1.2虚拟网络编辑器设置1.3Ubuntu和Windows网络信息设置Ubuntu网络信息配置方式&#xff1a;1.系统设置->网络->选项2.配置网络文件2源码准备2.…

Vuex 状态管理

文章目录Vuex概述安装单向数据流Vuex核心概念StatemapState 辅助函数扩展运算符GettermapGetters 辅助函数Mutation提交载荷提交载荷对象对象风格提交使用常量替代mutation事件类型Action异步分发Module命名空间Vuex 概述 Vuex 是一个状态管理库&#xff0c;用于管理 Vue.js …

CRM系统是什么?为什么使用它?

CRM系统是什么&#xff1f;为什么使用它&#xff1f;这篇来简单说下&#xff0c;CRM系统是什么&#xff1f;能帮助我们做什么&#xff1f;有什么好处&#xff1f; 01 CRM系统是什么&#xff1f; 我总结了7种关于CRM的概念&#xff0c;任意一个解释得其实都没什么问题&#xff…

【数据结构】核心数据结构之二叉堆的原理及实现

1.大顶堆和小顶堆原理 什么是堆 堆&#xff08;Heap&#xff09;是计算机科学中一类特殊的数据结构&#xff0c;通常是一个可以被看作一颗完全二叉树的数组对象。 完全二叉树 只有最下面两层节点的度可以小于2&#xff0c;并且最下层的叶节点集中在靠左连续的边界 只允许最后…

2023FL Studio最新中文版电子音乐、混音和母带制作DAW

水果具有独特的底层逻辑&#xff0c;其开创了编曲“块”的思维。用FL Studio编曲的流程是在把一个样式编辑好&#xff0c;然后将编辑好的样式当做音频块&#xff0c;在播放列表中像“搭积木”一样任意编排&#xff0c;形成一首歌&#xff0c;这种模式非常利于电子音乐编曲。 2…

Apinto V0.12 发布:新增流量镜像与 Mock 插件,路由特性更丰富!

Hello~ 各位开发者朋友们好呀&#xff0c; Eolink 旗下开源网关 Apinto 本周又更新啦&#xff01;这次的更新我们给大家带来了 2个好用的插件&#xff0c;且目前已经支持静态资源路由了&#xff01;希望新的功能能让大家的开发工作更加高效 &#xff5e; 1、新增流量镜像插件 …

学习streamlit-1

Streamlit A faster way to build and share data apps streamlit在几分钟内就可以将数据脚本转换为可共享的web应用程序&#xff0c;并且是纯python编程&#xff0c;无需前端经验。 快速开始 streamlit非常容易上手&#xff0c;运行demo只需2行代码&#xff1a; pip install…

0306spring--复习

一&#xff0c;spring是什么 Spring是一个轻量级的控制反转&#xff08;IOC&#xff09;和面向切面编程&#xff08;AOP&#xff09;的容器框架 理念&#xff1a;使现有的技术更加容易使用&#xff0c;本身是一个大杂烩&#xff0c;整合了现有的技术框架 优点&#xff1…

Windows系统利用Qemu仿真ARM64平台

Windows系统利用Qemu仿真ARM64平台0 写在最前1 Windows安装Qemu1.1 下载Qemu1.2 安装Qemu1.3 添加环境变量1.4测试安装是否成功2. Qemu安装Ubuntu-Server-Arm-642.1 安装前的准备2.2 安装Ubuntu server arm 64位镜像3 Windows配置Qemu网络和传输文件3.1 参考内容3.2 Windows安装…

正版Scrivener 3 论文/小说写作工具神器软件

一款非常优秀的写作软件&#xff0c;提供了各种写作辅助功能&#xff0c;如标注多个文档、概述介绍、全屏幕编辑、快照等&#xff0c;能够轻松、便捷的辅助作者从作品构思、搜集资料、组织结构、增删修改到排版输出的整个写作流程。 作为一个专业的写作软件&#xff0c;Scriven…

spring 的invokeBeanFactoryPostProcessors()

在spring启动过程中有个10多个关键的方法其中invokeBeanFactoryPostProcessors&#xff08;&#xff09;在函数在BeanDefinition完全加载后执行&#xff0c;实现的是spring bean的后置增强器BeanFactoryPostProcessor。 BeanFactoryPostProcessor在bean的生命周期中&#xff0…

企业移动内容管理(MCM)

1.什么是移动内容管理 &#xff08;MCM&#xff09; 移动内容管理 &#xff08;MCM&#xff09; 是指在组织中使用的移动设备上分发和管理企业文件的过程&#xff0c;以确保授权用户可以通过批准的设备安全地访问所需的资源。MCM 允许 IT 管理员在其组织员工使用的智能手机、平…

音频基础知识简述 esp-sr 上手指南

此篇博客先对音频基础知识进行简要叙述&#xff0c;然后帮助读者入门 esp-sr SDK。 1 音频的基本概念 1.1 声音的本质 声音的本质是波在介质中的传播现象&#xff0c;声波的本质是一种波&#xff0c;是一种物理量。 两者不一样&#xff0c;声音是一种抽象的&#xff0c;是声…