算法训练营 - 递归回溯

简介: 算法训练营 - 递归回溯

回溯算法

回溯法

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。也可以称为剪枝点,所谓的剪枝,指的是把不会找到目标,或者不必要的路径裁剪掉。

许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。

若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。

而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

除过深度优先搜索,常用的还有广度优先搜索。

深度优先搜索(Depth First Search------ 一条道走到黑

同学们先思考这样一个问题:


假如有编号为1~3的3张扑克牌和编号为1~3的3个盒子,现在需要将3张牌分别放到3个盒子中去,且每个盒子只能放一张牌,一共有多少种不同的放法。


当走到一个盒子面前的时候,到底要放那一张牌呢?在这里应该把所有的牌都尝试一遍。假设这里约定一个顺序,按牌面值从小到大依次尝试。在这样的假定下,当走到第一个盒子的时候,放入1号牌。


放好之后,继续向后走,走到第二个盒子面前,此时还剩2张牌,牌面值最小的为2号牌,按照约定的规则,把2号牌放入第二个盒子。


此时,来到第三个盒子面前,只剩一张牌,放入第三个盒子。此时手中的牌已经用完。


继续向后走,走到了盒子的尽头,后面再也没有盒子,并且也没有可用的牌了,此时,一种放法已经完成了,但是这只是一种放法,这条路已经走到了尽头,还需要折返,重新回到上一个盒子。


这里回到第三个盒子,把第三个盒子中的牌取出来,再去尝试能否再放其它的牌,这时候手里仍然只有一张3号牌,没有别的选择了,所以还需要继续向后回退,回到2号盒子面前。


收回2号盒子中的2号牌,现在手里有两张牌,2,3,按照约定,再把3号牌放入2号盒子,放好之后,继续向后走,来到3号盒子。


此事手里只有一张2号牌,把它放入3号盒子,继续向后走。


此时这条路又一次走到了尽头,一个新的放法又产生了,继续向上折返,尝试其它可能,按照上述步骤,依次会产生所有结果。


代码如何实现这种过程呢?最主要的事情,向面前的盒子里放每一种牌,一个for循环搞定。这里还需考虑,现在手里有没有这张牌,用一个数组st标记手里是否有这张牌

#include<iostream>
using namespace std;
const int N=10;
int n;
int path[N];
bool st[N];
void dfs(int u)
{
    if(u==n)
    {
        for(int i=0;i<n;i++) cout<<path[i]<<" ";
        puts("");
        return  ;
    }
    for(int i=1;i<=n;i++)
    {
        if(!st[i])
        {
            path[u]=i;
            st[i]=true;
            dfs(u+1);
            st[i]=false;
        }
    }
}
int main()
{
    cin>>n;
    dfs(0);
    return 0;
}

输入样例:

3

输出样例:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

示例1.dfs带权值的n叉树

力扣

/*
// Definition for Employee.
class Employee {
public:
    int id;
    int importance;
    vector<int> subordinates;
};
*/
class Solution {
public:
    int dfs(unordered_map<int,Employee*>& info,int id)
    {
        int num=info[id]->importance;//领导(子根节点)
        for(const auto& sid:info[id]->subordinates)
        {
            num+=dfs(info,sid);//加上其直系下属
        }
        return num;
    }
    int getImportance(vector<Employee*> employees, int id) {
        if(employees.empty()) return 0;
        unordered_map<int,Employee*> info;//用哈希存储唯一id对应的信息
        for(const auto& e: employees) info[e->id]=e;
        return dfs(info,id); 
    }
};

示例2.dfs查找所有连接方块

力扣

思路:


非常简单,就是把图块的四个方向都搜索一遍,对于每个相邻的同色图块修改成新色即可


我们从给定的起点开始,进行深度优先搜索。每次搜索到一个方格时,如果其与初始位置的方格颜色相同,就将该方格的颜色更新,以防止重复搜索;如果不相同,则进行回溯。


注意:因为初始位置的颜色会被修改,所以我们需要保存初始位置的颜色,以便于之后的更新操作。

class Solution {
public:
    const int dx[4]={1,-1,0,0};
    const int dy[4]={0,0,1,-1};
    void dfs(vector<vector<int>>& image,int x,int y,int old,int color)
    {
        if(image[x][y]==old)
        {
            image[x][y]=color;
            for(int i=0;i<4;i++)
            {
                int mx=x+dx[i],my=y+dy[i];
                if(mx>=0&&mx<image.size()&&my>=0&&my<image[0].size()){
                    dfs(image,mx,my,old,color);//在范围内则继续搜索
                }
            }
        }
    }
    vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
        int old=image[sr][sc];//原来颜色
        if(old!=color) dfs(image,sr,sc,old,color);
        return image;
    }
};

示例3.逐一遍历变量并组合

力扣

思路:

先按题意打表存储所有字符串,然后按digits数字循序提取数字所对应的字符串,按序提取每个字母利用递归组合,当深度(id)达到digits的长度时,即完成了一个排列组合,回溯上述操作得到所有排列组合。

string nums[10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
class Solution {
public:
    void dfs(string& digits,int id,string str,vector<string>& ret)
    {
        if(id==digits.size())
        {
            ret.push_back(str);
            return ;
        }
        int num=digits[id]-'0';
        string cur=nums[num];
        for(auto& ch:cur)
        {
            dfs(digits,id+1,str+ch,ret);
        }
    }
    vector<string> letterCombinations(string digits) {
        vector<string> ret;
        if(digits.empty()) return ret;
        dfs(digits,0,"",ret);
        return ret;
    }
};

示例4.dfs构造排列组合

力扣

此题相加的元素可以重复,所以去下一个元素的位置可以从当前位置开始, DFS + 回溯


为了保证组合不重复(顺序不同,元素相同,也算重复),不再从当前位置向前看


1. 从第一个元素开始相加


2. 让局部和继续累加候选的剩余值


3. 局部和等于目标值,保存组合,向上回退,寻找其它组合

class Solution {
public:
    void dfs(vector<int>& candidates,int target,vector<vector<int>>&res,
    vector<int>& path,int sum,int start)
    {
       if(sum>target) return ;
       if(sum==target) res.push_back(path);
       for(int i=start;i<candidates.size();i++)
       {
           //单个值已经大于目标,直接跳过(剪枝避免数据重复)
           if(candidates[i] > target) continue;
           path.push_back(candidates[i]);//按序尝试路径
           dfs(candidates,target,res,path,sum+candidates[i],i);
           path.pop_back();//回溯
       }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> res;//符合条件的组合
        vector<int> path;//查找组合的路径
        int sum=0,start=0;
        dfs(candidates,target,res,path,0,0);
        return res;
    }
};

小小练手.去重全排列

力扣

思路:


此题组合的长度不唯一,最小组合长度为1, 最大组合长度为tiles的长度。


按照题意tiles中每一个位置的字符在组合中只能出现一次, 所以可以用一个标记辅助


当去组合新的组合时,可以与tiles中的每一个位置组合,但是如果当前位置已经在当前组合中出现过,则跳过


虽然此题中每一个位置的字符在组合中只能出现一次,但是tiles中可能有相同的字符,所以需要考虑重复的组合


而unordered_set可以天然去重,可以用其去重


DFS + 回溯:


当前组合不为空, 则插入set中

继续给当前组合拼接新的组合,尝试拼接tiles每一个位置的字符

如果当前位置已在组合中出现过,返回到2,否则标记当前位置,继续拼接更长的组合

回溯,尝试组合其它位置,返回2

当所有位置都已经使用过时,当前递归就结束了,继续向上层DFS回退


最终返回set的大小即为组合数目。

class Solution {
public:
    void dfs(string& tiles,string s,vector<int>& st,unordered_set<string>& tot)
    {
        if(!s.empty()) tot.insert(s);//只要有就算一种
        for(int i=0;i<tiles.size();i++)
        {
            if(st[i]) continue;
            st[i]=1;
            dfs(tiles,s+tiles[i],st,tot);
            st[i]=0;
        }
    }
    int numTilePossibilities(string tiles) {
        if(tiles.empty()) return 0;
        unordered_set<string> tot;//利用set性质去重
        vector<int> st(tiles.size(),0);//状态,表示有无用过
        dfs(tiles,"",st,tot);
        return tot.size();
    }
};
相关文章
|
8天前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
数据结构与算法系列学习之串的定义和基本操作、串的储存结构、基本操作的实现、朴素模式匹配算法、KMP算法等代码举例及图解说明;【含常见的报错问题及其对应的解决方法】你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
|
27天前
|
存储 机器学习/深度学习 算法
蓝桥杯练习题(三):Python组之算法训练提高综合五十题
蓝桥杯Python编程练习题的集合,涵盖了从基础到提高的多个算法题目及其解答。
50 3
蓝桥杯练习题(三):Python组之算法训练提高综合五十题
|
8天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习(8)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
8天前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习之顺序表【无论是王道考研人还真爱粉都能包会的;不然别给我家鸽鸽丢脸好嘛?】
顺序表的定义和基本操作之插入;删除;按值查找;按位查找等具体详解步骤以及举例说明
|
7天前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
8天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
8天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
8天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
8天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之顺序表习题精讲【无论是王道考研人还真爱粉都能包会的;不然别给我家鸽鸽丢脸好嘛?】
顺序表的定义和基本操作之插入;删除;按值查找;按位查找习题精讲等具体详解步骤以及举例说明
|
8天前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习【无论是王道考研人还真爱粉都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构的基本概念;算法的基本概念、特性以及时间复杂度、空间复杂度等举例说明;【含常见的报错问题及其对应的解决方法】