【刷题版】掌握算法的一揽子计划——深度优先搜索和回溯
创始人
2025-05-30 05:30:39

文章目录

  • 深搜和回溯总结
    • 基本概念
    • 常见例题
      • 自然数的拆分
      • 排列型枚举
        • 全排列 I
        • 全排列 II
      • 组合型枚举
        • 组合 I
        • 组合 II
      • N皇后问题
      • 一些简单的树和图上的问题
        • 二叉树的遍历
        • 二叉树的所有路径
        • 岛屿的最大面积
    • 参考资料

深搜和回溯总结

基本概念

深搜

深度优先搜索(Depth First Search,DFS)属于图论中的概念。在图论中主要用于遍历树或者图上的节点,其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次(一些详细的步骤和好看的图可以参考下边的几个链接)。而在搜索算法中主要通过递归方便地实现暴力枚举。

  • https://en.wikipedia.org/wiki/Depth-first_search
  • https://www.hackerearth.com/practice/algorithms/graphs/depth-first-search/tutorial/
  • https://www.algotree.org/algorithms/tree_graph_traversal/depth_first_search/

回溯

回溯法也称试探法,它的基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”。回溯属于一种深搜的技巧,常常用在深搜中。

常见例题

自然数的拆分

题目链接

任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。

当n=7共14种拆分方法:

7=1+1+1+1+1+1+1
7=1+1+1+1+1+2
7=1+1+1+1+3
7=1+1+1+2+2
7=1+1+1+4
7=1+1+2+3
7=1+1+5
7=1+2+2+2
7=1+2+4
7=1+3+3
7=1+6
7=2+2+3
7=2+5
7=3+4
total=14

我们可以先将 7 拆分成 1+6,然后对 6 进行拆分,变成 1+5,依次类推,7 可以被拆分成 7=1+1+1+1+1+2。此处对于 2 的拆分有两种方式,首先1+1,然后尝试玩1打头之后,尝试2打头,还可以分成2+0(需要注意的是7不能取7=7)。

另外需要注意的是,按照此种方式,有7=3+4,此时4不可以被继续拆分,如果4被拆成1+1+1+1后,就成了7=3+1+1+1+1,与7=1+1+1+1+3重复所以我们发现一个规律,即被拆分成的数,总是升序的。也就是后边大,前边小,否则就会重复。

#include
#include
#include
#include
#define N 3000
using namespace std;
int n,r;
int a[N];
void print(int step)
{cout<>n;a[0]=1;dfs(n,1);return 0;
}

排列型枚举

全排列 I

题目链接

给出一列互不相同的整数,返回其全排列。

这个问题我们相当于一开始有 nnn 位置,然后 nnn 个球,分别被标号 1,2,…,n1, 2, \dots , n1,2,…,n。我们一开始可以选择将 1 号球放入到第一个位置,然后将 2 号球放入到第二个位置,依次,,,最后将第 nnn 号球放到第 nnn 个位置,这样便完成了一个合法的排列。然后我们取出 nnn 号球,接着取出 n−1n-1n−1 号球,将 nnn 号球放到第 n−1n-1n−1 个位置,将第 n−1n-1n−1 号球放到第 nnn 个位置,这样便生成了一个新的排列。依次类推,便可以生成所有的排列。代码的思想也是一样的,如下:

class Solution {
public:vector path;  // 相当于 n 个坑vector> ans;vector st;   //记录每一个数是否被用过vector> permutation(vector& nums) {st = vector (nums.size(), false);path = vector(nums.size());dfs(nums, 0);return ans;}// 每一层递归都是在未使用的球中选择一个新的球放到第 u 个位置//递归的深度,初始为第0层void dfs(vector& nums, int u) {if(u == nums.size()) {ans.push_back(path);return ;}//枚举每一个可以用的数for(int i = 0; i < nums.size(); i ++) {// 此处就决定了在未选择的球中进行选择if(!st[i]) {st[i] = true;path[u] = nums[i];dfs(nums, u + 1);st[i] = false; // 这里就是回溯,恢复未使用的状态,此处也相当于把 i 号球从第 u 个位置拿走}}}
};

注:此处是递归每个位置 u,选球 for 循环

全排列 II

原题链接

给定一个可能包含重复项的数字集合nums,返回所有可能的唯一排列。

这个问题是全排列 I 的变体,由于包含重复数字,那么如果按照上述方法进行枚举,则有可能出现重复的排列。For example,上述取出 nnn 号球,取出 n−1n-1n−1 号球,将 nnn 号球放到第 n−1n-1n−1 个位置,将第 n−1n-1n−1 号球放到第 nnn 个位置的操作,如果 n−1n-1n−1 号球和 nnn 号球的标号一样,那么其实并没有生成新的排列,我们和上一次的排列是一样的。

这个问题如何解决呢,如果手动进行判重,那老麻烦了,这里有一个比较巧妙的方法,算法的过程如下:

  1. 先将所有数从小到大排序(特别重要),使得相同的数会排在一起
  2. 我们对数无需判重,取而代之的是,枚举数的顺序是从前往后的,这里可以结合递归的层数的顺序实现,即数在选坑。
  3. 从左到右依次枚举每个数,每次将它放在一个空位上
  4. 我们在dfs时记录一个额外的状态,记录上一个相同数存放的位置 start,我们在枚举当前数时,只能枚举 start+1,start+2,…,nstart+1,start+2,…,n这些位置,在上一个数的后面,如果上一个数和当前数是不同的,那么当前数就可以从start = 0 开始选,选到没有被用过坑填入即可。

全排列 I 的变化是:全排列 I 递归每一个位置,尝试把每一个球放到该位置。全排列 II 递归每一个球,for 循环选择可以放该球的位置

**正确性:**我们知道按 全排列I 的方法产生的弊端是,会产生重复的排列,那么为什么 II 这样做就是对的呢,假设一个序列 1,2,3(1),3(2),4,5,如果不加限制,对于每一个球会尝试所有的位置(0-5),3(2)会跑到3(1)的前边,比如 1,2,3(2),3(1),4,5 这实际上和原序列就重复了。而我们加上第四步的限制后,就保证了3(2)不会跑到 3(1)前边,也就避免了重复。

class Solution {
public:vector path;  //每一个坑填的数字是几vector> ans;vector st;  //每一个坑是否被用过vector> permutation(vector& nums) {sort(nums.begin(), nums.end());  // 至关重要st = vector(nums.size(), false);path = vector(nums.size());  //开出这么多坑dfs(nums, 0, 0);return ans;}//u->表示递归到第几个坑//start->从哪个坑开始填起void dfs(vector & nums, int u, int start){if(u == nums.size()){ans.push_back(path);return ;}for(int i = start; i < nums.size(); i ++){if(!st[i]){st[i] = true;path[i] = nums[u];if(u + 1 < nums.size() && nums[u] == nums[u + 1])dfs(nums, u + 1, i + 1);// 二是,如果前后两个元素不相等else // 双重含义,一所有坑都被填满了,已经指向最后一个元素dfs(nums, u + 1, 0);st[i] = false;  //还原现场}}}
};

注:此处是递归每个球 u,选位置 for 循环,开始位置 start

组合型枚举

组合 I

题目链接

从 1∼n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案。

这个还比较简单,相当于 m 个位置,每一个位置,对未选择的数进行选或不选的操作就行,同样为了防止重复,第 i 次,选择了 j,第 i+1 次,只能从 j+1 及之后进行选择,因为(2,3)和(3,2)是同一组组合。这种处理方式和 全排列 II 避免重复的处理方式一致。

#include 
using namespace std;
const int N = 35;
int a[N];
int n, m;
void dfs(int u, int start)
{if(u > m){for(int i = 1; i <= m; i ++){printf("%d ", a[i]);}puts("");return ;}for(int i = start; i <= n; i ++){a[u] = i;dfs(u + 1, i + 1);}
}
int main()
{cin >> n >> m;dfs(1, 1);return 0;
}

组合 II

题目链接

给定一个长度为 n 的可包含重复数字的序列,从中随机选取 m 个数字,输出所有可能的选择方案。

先 mark 一下,丢份题解,一时没看明白,[\touda]

题解

N皇后问题

题目链接

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。间不能直接吃掉对方。要求输出所有的可行解

经典题了,解法很多,这里只贴最原始的深搜解法。

class Solution {private char[][] map;private int size;private List> list = new ArrayList<>();public List> solveNQueens(int n) {map = new char[n + 5][n + 5];size = n;initMap();putQueens(0);return list;}/*** 初始化地图*/public void initMap(){for(int i = 0; i < size; ++i){for(int j = 0; j < size; ++j){map[i][j] = '.';}}}/*** 放置皇后* @param row 表示放的是第几行,从 0 开始,到 (size - 1) 行结束*/public void putQueens(int row){if(row == size){List tmpList = new ArrayList<>();for(int i = 0; i < size; ++i){tmpList.add(new String(map[i], 0, size));}list.add(tmpList);return;}for(int i = 0; i < size; ++i){if(Judge(row, i)){map[row][i] = 'Q';putQueens(row + 1);map[row][i] = '.';}}return;}/*** 判断当前位置是否可以放皇后* @param row 行* @param col 列* @return 是否可以放皇后*/public boolean Judge(int row, int col){//判断上方for(int i = row - 1; i >= 0; --i){if(map[i][col] == 'Q'){return false;}}//判断左上for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j){if(map[i][j] == 'Q'){return false;}}//判断右上for(int i = row - 1, j = col + 1; i >= 0 && j < size; --i, ++j){if(map[i][j] == 'Q'){return false;}}return true;}
}

一些简单的树和图上的问题

二叉树的遍历

题目链接:

  • 前序
  • 中序
  • 后序
// 前序
class Solution {
public:vector ans;void pre_traverse(TreeNode* root) {if (root == nullptr) {return;}ans.push_back(root->val);pre_traverse(root->left);pre_traverse(root->right);return;}vector preorderTraversal(TreeNode* root) { ans.clear();pre_traverse(root);return ans;}
};// 中序
class Solution {
public:void dfs(TreeNode *root, vector& ans) {if (root == nullptr) return;dfs(root->left, ans);ans.push_back(root->val);dfs(root->right, ans);return;}vector inorderTraversal(TreeNode* root) {vector ans;ans.clear();dfs(root, ans);return ans;}
};// 后序
class Solution {
public:void dfs(TreeNode *root, vector& ans) {if (root == nullptr) return;dfs(root->left, ans);dfs(root->right, ans);ans.push_back(root->val);return;}vector postorderTraversal(TreeNode* root) {vector ans;ans.clear();dfs(root, ans);return ans;}
};

二叉树的所有路径

题目链接

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

就是树的前序遍历,每次将经过的节点记录,到达叶子节点的时候,将答案统计一下。

class Solution {vector ans;vector stack;
public:void dfs(TreeNode* root) {stack.push_back(root->val);if (root && root->left == nullptr && root->right == nullptr) {string tmp = "";for (int i = 0; i < stack.size() - 1; ++i) {tmp += to_string(stack[i]);tmp += "->";}tmp += to_string(stack.back());ans.push_back(tmp);return;}if (root && root->left) {dfs(root->left);if (stack.size()) stack.pop_back();}if (root && root->right) {dfs(root->right);if (stack.size()) stack.pop_back();}return;}vector binaryTreePaths(TreeNode* root) {ans.clear();stack.clear();if (root) dfs(root);return ans;}
};

岛屿的最大面积

题目链接

我们想知道网格中每个连通形状的面积,然后取最大值。如果我们在一个土地上,以 4 个方向探索与之相连的每一个土地(以及与这些土地相连的土地),那么探索过的土地总数将是该连通形状的面积。为了确保每个土地访问不超过一次,我们每次经过一块土地时,将这块土地的值置为 0。这样我们就不会多次访问同一土地。

class Solution {int dfs(vector>& grid, int cur_i, int cur_j) {if (cur_i < 0 || cur_j < 0 || cur_i == grid.size() || cur_j == grid[0].size() || grid[cur_i][cur_j] != 1) {return 0;}grid[cur_i][cur_j] = 0;int di[4] = {0, 0, 1, -1};int dj[4] = {1, -1, 0, 0};int ans = 1;for (int index = 0; index != 4; ++index) {int next_i = cur_i + di[index], next_j = cur_j + dj[index];ans += dfs(grid, next_i, next_j);}return ans;}
public:int maxAreaOfIsland(vector>& grid) {int ans = 0;for (int i = 0; i != grid.size(); ++i) {for (int j = 0; j != grid[0].size(); ++j) {ans = max(ans, dfs(grid, i, j));}}return ans;}
};

参考资料

  • https://oi-wiki.org/search/dfs/
    -https://baike.baidu.com/item/%E5%9B%9E%E6%BA%AF/23724802?fr=aladdin
  • https://www.acwing.com/solution/content/114737/
  • https://blog.csdn.net/qq_43738331/article/details/109280390
  • 695. 岛屿的最大面积 - 力扣(Leetcode)

相关内容

热门资讯

Python基础(十七):装饰... 闭包闭包(英语:Closure),又称词法闭...
计算机科学导论笔记(十四) 目录 十六、安全 16.1 引言 16.1.1 安全目标 16.1.2 攻击 16.1.2.1 威...
@Transactional导... 首先我有一个Class A和Class B,A和B存在循环依赖。 @Servi...
HTML5-表单 HTML5-表单 一、Form 1.action 属性 action 属性用于指定表单...
【小猫爪】AUTOSAR学习笔... 【小猫爪】AUTOSAR学习笔记05-Communication Stack之CanSM模块前言1 ...
c# 使用AutoResetE...         做项目时有一个需求。用一个线程去执行耗时操作。另一个线程需要使用第一个线程的操作结果...
在pycharm中使用chat... 目录 前言 一、插件安装 二、使用步骤 总结 前言 ChatGPT是目前最强大的AI,...
Codeforces Roun... G. Subsequence Addition 标签 规律、数学 链接 传送门、 结论 当前前缀和小...
算法leetcode|42. ... 文章目录42. 接雨水:样例 1:样例 2:提示ÿ...
【项目设计】负载均衡在线OJ 🎇Linux: 博客主页:一起去看日落吗分享博主的在L...
Java开发 | 重写 | 多... 前言 大家好,我是程序猿爱打拳,今天给大家带来的是面向对象之封装继承多...
【Unity】NavMesh ... 在Unity中,可以使用自带导航系统(Navigation System...
由文心一言发布会引发的思考,聊... 文章目录前言一. 文心一言的试用1.1 文心一言发布会1.2 文心一言图片生成功能试用1.3 文心一...
java线程之Thread类的... Thread类的基本用法1. Thread类的构造方法2. Thread的几个常见属性常见属性线程中...
css实现3D弹性按钮以及bo... box-shadow 在实现案例之前先了解css的阴影属性box-shadow,该属性...
【Linux】基础命令大全、实... 个人简介:Java领域新星创作者;阿里云技术博主、星级博主、专家博主&#...
R语言基础教程4:列表和数据框 文章目录列表数据帧表头 R语言系列:1 编程基础💎2 循环语句...
Git基础知识 Git基础知识前言一、Git基本概念1、分布式版本控制系统--Git2、Git配置命令3、Git原理...
【JavaWeb】MySQL 一、数据库的相关概念 1.数据库(DB) ==存储和管...
CPU 是如何执行程序的 代码写了那么多,你知道 a = 1 + 2 这条代码是怎么被 CPU ...
从产品的角度看缓存 文章目录 1. What——什么是缓存?2. Why——为什么需要使用缓存?2.1 什么是用户体验2...
vivado 开发过程中所遇错...  [Synth 8-4556] 开辟的数组内存空间大小问题 [Synth 8-4556] size...
1.4 K8S入门之POD和网... POD 分类 自主式POD控制器管理的POD 容器 每个容器独立存在,有自己的IP地址...
【二】一起算法---队列:ST... 纸上得来终觉浅,绝知此事要躬行。大家好!我是霜淮子,欢迎订...
在使用fastjson中遇到的... 一、在使用fastjson中遇到的问题 导论:最近在写一个JavaFx项目的时候使用...
HJ31 单词倒排 描述 对字符串中的所有单词进行倒排。 说明: 1、构成单词的字符只有26个大写或小写英...
普通插槽、具名插槽、作用域插槽 插槽 插槽就是子组件提供给父组件的占位符,用slot来表示,父组件可以在...
Go语言必知必会100问题-0... 减少代码的嵌套层数 软件开发中的“心智模型”用于描述开发人员在编码时心理活动,每段代码...
CSRF漏洞的概念、利用方式、... CSRF漏洞1.CSRF的概念1.1 什么是CSRF?1.2 基本攻击流程2.CSRF...
基于springboot开发的... 基于springboot开发的学生考勤管理系统 如需更多资料请前往文章底部获取联系方式 系统设计主要...