【C++】算法集锦(3):回溯,从入门到入土-程序员宅基地

技术标签: 程序员  算法  c++  java  

    • 思路
  • 代码实现

前言


回溯算法,之前也是写过的,感觉还不错。但是之前分成两篇写了,现在重新整理一下,顺便我自己也回顾一下。

递归


要玩得转回溯算法,递归思想就要融入骨子里。

正好早上我整理了一篇递归相关的,不妨看一下:【C++】算法集锦(2):递归

N叉树的遍历


我们从N叉树的遍历入手,来看一下回溯算法。

思考一下二叉树的回顾一下二叉树的遍历方式:

前序遍历 - 首先访问根节点,然后遍历左子树,最后遍历右子树;

中序遍历 - 首先遍历左子树,然后访问根节点,最后遍历右子树;

后序遍历 - 首先遍历左子树,然后遍历右子树,最后访问根节点;

层序遍历 - 按照从左到右的顺序,逐层遍历各个节点。

N 叉树的中序遍历没有标准定义,中序遍历只有在二叉树中有明确的定义。

我们跳过 N 叉树中序遍历的部分。

在这里插入图片描述

节点设计

class Node {

public:

int val;

vector<Node*> children; //注意,这里不再是左右子节点了

Node() {}

Node(int _val) {

val = _val;

}

Node(int _val, vector<Node*> _children) {

val = _val;

children = _children;

}

};


N叉树的前序遍历

给定一个 N 叉树,返回其节点值的前序遍历。

class Solution {

public:

void preorderDFS(Node* root, int index, vector& ret) {

if (root == NULL)

return;

ret.push_back(root->val);

int sz = (root->children).size();

while (index < sz) {

preorderDFS(root->children[index], 0, ret); //认真捋一下这一步

index += 1;

}

}

vector preorder(Node* root) {

vector ret;

preorderDFS(root, 0, ret);

return ret;

}

};

修改于 2021.8.22:

//经测试,这段代码更简洁:

void preorder(Node* node) {

cout << “value:” << node->val << endl;

for (Node* n : node->children) {

preorder(n);

}

return;

}

后序我就不调了吧,一行代码位置的事情。


后序遍历

给定一个 N 叉树,返回其节点值的后序遍历。

class Solution {

public:

void postorderDFS(Node* root, int index, vector& ret) {

if (root == NULL)

return;

int sz = (root->children).size();

while (index < sz) {

postorderDFS(root->children[index], 0, ret);

index += 1;

}

ret.push_back(root->val);

}

vector postorder(Node* root) {

vector ret;

postorderDFS(root, 0, ret);

return ret;

}

};


层序遍历

给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。

class Solution {

public:

vector<vector> result;

void dfs(Node* root, int dep){

if(!root) return;

if(dep == result.size()) result.emplace_back();

result[dep].push_back(root->val);

auto children = root->children;

for(auto ele:children){

dfs(ele, dep+1);

}

}

vector<vector> levelOrder(Node* root) {

dfs(root, 0);

return result;

}

};


岛屿最大面积、八皇后问题、括号生成感觉比较简单,所以思路讲的就比较简陋,适合入门练手,建议看其他题目的讲解(全排列那题)。


回溯例题精讲


岛屿最大面积

给定一个包含了一些 0 和 1 的非空二维数组 grid 。

一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)

示例 1:

[[0,0,1,0,0,0,0,1,0,0,0,0,0],

[0,0,0,0,0,0,0,1,1,1,0,0,0],

[0,1,1,0,1,0,0,0,0,0,0,0,0],

[0,1,0,0,1,1,0,0,1,0,1,0,0],

[0,1,0,0,1,1,0,0,1,1,1,0,0],

[0,0,0,0,0,0,0,0,0,0,1,0,0],

[0,0,0,0,0,0,0,1,1,1,0,0,0],

[0,0,0,0,0,0,0,1,1,0,0,0,0]]

对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。

示例 2:

[[0,0,0,0,0,0,0,0]]

对于上面这个给定的矩阵, 返回 0。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/max-area-of-island

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

这题其实我自己做出来了,把它放在第一题嘛,我觉得因为它是比较简单的。

从头开始遍历,遇到“1”就展开上下左右搜索,搜空了就回溯,在搜索过程中计数,并将搜索到的位置全部置0,防止二次污染。

代码实现

int checkIsland(vector<vector>& grid, int x, int y) {

int count = 0;

if (grid[x][y] == 0)

return count;

else {

grid[x][y] = 0;

count++;

//上

if (x - 1 >= 0)

count += checkIsland(grid, x - 1, y);

//右

if ((y + 1) < grid[0].size())

count += checkIsland(grid, x, y + 1);

//下

if ((x + 1) < grid.size())

count += checkIsland(grid, x + 1, y);

//左

if ((y - 1) >= 0)

count += checkIsland(grid, x, y - 1);

}

return count;

}

int maxAreaOfIsland(vector<vector>& grid) {

if (grid.size() == 0)

return 0;

int max = 0;

for (int i = 0; i < grid.size(); i++) {

for (int j = 0; j < grid[0].size(); j++) {

int temp = checkIsland(grid, i, j);

if (temp > max)

max = temp;

}

}

}

return max;

}


八皇后问题

八皇后问题是一个古老的问题,于1848年由一位国际象棋棋手提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,如何求解?在这里插入图片描述

思路

将每一个棋子放在当前列的最前端,再放下一个棋子。

如果下一个棋子没地方放了,就回溯到前一个棋子,将其往后一个适宜位置挪动。

具体思路都在代码里了。

代码实现

#include

using namespace std;

#define MAX_NUM 8 //皇后数量

int queen[MAX_NUM][MAX_NUM] = { 0 };

bool check(int x, int y) { //检查一个坐标是否可以放置

for (int i = 0; i < y; i++) {

if (queen[x][i] == 1) { //这一行是否可以存在

return false;

}

if (x - 1 - i > 0 && queen[x - 1 - i][y - 1 - i] == 1) { //检查左斜列

return false;

}

if (x + 1 + i < MAX_NUM && queen[x + 1 + i][y + 1 + i] == 1) { //检查右斜列

return false;

}

}

queen[x][y] = 1;

return true;

}

void showQueen() {

for (int i = 0; i < MAX_NUM; i++) {

for (int j = 0; j < MAX_NUM; j++) {

cout << queen[i][j] << " ";

}

cout << endl;

}

cout << endl;

}

bool settleQueen(int x) {

if (x == MAX_NUM) { //遍历完毕,找到答案

return true;

}

for (int i = 0; i < MAX_NUM; i++) {

for (int y = 0; y < MAX_NUM; y++) {

queen[y][x] = 0; //清空当前列,省的回溯的时候被打扰

}

if (check(i,x)) { //如果这行找着了

queen[i][x] = 1;

showQueen(); //直观测试结果

if (settleQueen(x + 1)) { //是时候往左了

return true; //一路往左

}

}

}

return false; //如果不行,就退回来

}


括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例:

输入:n = 3

输出:[

“((()))”,

“(()())”,

“(())()”,

“()(())”,

“()()()”

]

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/generate-parentheses

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

如果左括号数量不大于 n,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号。

代码实现

vector generateParenthesis(int n) {

vector res;

generateParenthesisDFS(n, n, “”, res);

return res;

}

void generateParenthesisDFS(int left, int right, string out, vector &res) {

if (left > right) return;

if (left == 0 && right == 0) res.push_back(out);

else {

if (left > 0) generateParenthesisDFS(left - 1, right, out + ‘(’, res);

if (right > 0) generateParenthesisDFS(left, right - 1, out + ‘)’, res);

}

}


全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]

输出:

[

[1,2,3],

[1,3,2],

[2,1,3],

[2,3,1],

[3,1,2],

[3,2,1]

]

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/permutations

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

怎么用,是不是感觉千头万绪,但是又捋不出一个头绪来,很难受。

思路

“全排列”就是一个非常经典的“回溯”算法的应用。我们知道,N 个数字的全排列一共有 N! 这么多个。

大家可以尝试一下在纸上写 3 个数字、4 个数字、5 个数字的全排列,相信不难找到这样的方法。

以数组 [1, 2, 3] 的全排列为例。

我们先写以 1 开头的全排列,它们是:[1, 2, 3], [1, 3, 2];

再写以 2 开头的全排列,它们是:[2, 1, 3], [2, 3, 1];

最后写以 3 开头的全排列,它们是:[3, 1, 2], [3, 2, 1]。

我们只需要按顺序枚举每一位可能出现的情况,已经选择的数字在接下来要确定的数字中不能出现。按照这种策略选取就能够做到不重不漏,把可能的全排列都枚举出来。

在枚举第一位的时候,有 3 种情况。

在枚举第二位的时候,前面已经出现过的数字就不能再被选取了;

在枚举第三位的时候,前面 2 个已经选择过的数字就不能再被选取了。

使用编程的方法得到全排列,就是在这样的一个树形结构中进行编程,具体来说,就是执行一次深度优先遍历,从树的根结点到叶子结点形成的路径就是一个全排列。

在这里插入图片描述

说明:

1、每一个结点表示了“全排列”问题求解的不同阶段,这些阶段通过变量的“不同的值”体现;

2、这些变量的不同的值,也称之为“状态”;

3、使用深度优先遍历有“回头”的过程,在“回头”以后,状态变量需要设置成为和先前一样;

4、因此在回到上一层结点的过程中,需要撤销上一次选择,这个操作也称之为“状态重置”;

5、深度优先遍历,可以直接借助系统栈空间,为我们保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈。

6、深度优先遍历通过“回溯”操作,实现了全局使用一份状态变量的效果。

下面我们解释如何编码:

1、首先这棵树除了根结点和叶子结点以外,每一个结点做的事情其实是一样的,即在已经选了一些数的前提,我们需要在剩下还没有选择的数中按照顺序依次选择一个数,这显然是一个递归结构;

2、递归的终止条件是,数已经选够了,因此我们需要一个变量来表示当前递归到第几层,我们把这个变量叫做 depth;

3、这些结点实际上表示了搜索(查找)全排列问题的不同阶段,为了区分这些不同阶段,我们就需要一些变量来记录为了得到一个全排列,程序进行到哪一步了,在这里我们需要两个变量:

(1)已经选了哪些数,到叶子结点时候,这些已经选择的数就构成了一个全排列;

(2)一个布尔数组 used,初始化的时候都为 false 表示这些数还没有被选择,当我们选定一个数的时候,就将这个数组的相应位置设置为 true ,这样在考虑下一个位置的时候,就能够以 O(1) 的时间复杂度判断这个数是否被选择过,这是一种“以空间换时间”的思想。

我们把这两个变量称之为“状态变量”,它们表示了我们在求解一个问题的时候所处的阶段。

4、在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。

5、另外,因为是执行深度优先遍历,从较深层的结点返回到较浅层结点的时候,需要做“状态重置”,即“回到过去”、“恢复现场”,我们举一个例子。

从 [1, 2, 3] 到 [1, 3, 2] ,深度优先遍历是这样做的,从 [1, 2, 3] 回到 [1, 2] 的时候,需要撤销刚刚已经选择的数 3,因为在这一层只有一个数 3 我们已经尝试过了,因此程序回到上一层,需要撤销对 2 的选择,好让后面的程序知道,选择 3 了以后还能够选择 2。

这种在遍历的过程中,从深层结点回到浅层结点的过程中所做的操作就叫“回溯”。

作者:liweiwei1419

链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/

来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


代码实现

vector<vector> permute(vector& nums) {

if(nums.empty()) return {};

// 存放最后结果

vector<vector> ans;

// 存放某一个排列

vector temp;

// 判断该数字是否被使用过

vector used(nums.size(),false);

// 进行递归求解

dfs(ans,temp,used,nums);

return ans;

}

void dfs(vector<vector>& ans,vector& temp,vector& used,

const vector& nums)

{

// 如果当前排列数组的长度等于输入数组的长度

// 该排列已完成

// 将该排列的加入结果中,返回

if(temp.size()==nums.size())

{

ans.push_back(temp);

return;

}

// 循环的进行枚举所有状态

for(int i=0;i<nums.size();++i)

{

// 该数字已经选择过,跳过

if(used.at(i)) continue;

// 选择当前数字

temp.push_back(nums.at(i));

// 记录该数字已被选择

used.at(i)=true;

// 递归选择下一个数字

dfs(ans,temp,used,nums);

// 回溯,撤销当前选择

used.at(i)=false;

temp.pop_back();

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

由于细节内容实在太多了,为了不影响文章的观赏性,只截出了一部分知识点大致的介绍一下,每个小节点里面都有更细化的内容!

小编准备了一份Java进阶学习路线图(Xmind)以及来年金三银四必备的一份《Java面试必备指南》

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
ck();

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-9SeHDnxI-1713522447192)]

[外链图片转存中…(img-eTPHmpsp-1713522447196)]

[外链图片转存中…(img-ElXFJemn-1713522447198)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

由于细节内容实在太多了,为了不影响文章的观赏性,只截出了一部分知识点大致的介绍一下,每个小节点里面都有更细化的内容!

[外链图片转存中…(img-tdAbZJ9y-1713522447199)]

小编准备了一份Java进阶学习路线图(Xmind)以及来年金三银四必备的一份《Java面试必备指南》

[外链图片转存中…(img-B3IhivQk-1713522447201)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/2401_84010030/article/details/137974069

智能推荐

解析蓝牙原理_蓝牙原理图详解-程序员宅基地

文章浏览阅读1.4w次,点赞19次,收藏76次。1.前言市面上关于Android的技术书籍很多,几乎每本书也都会涉及到蓝牙开发,但均是上层应用级别的,而且篇幅也普遍短小。对于手机行业的开发者,要进行蓝牙模块的维护,就必须从Android系统底层,至少框架层开始,了解蓝牙的结构和代码实现原理。这方面的文档、网上的各个论坛的相关资料却少之又少。分析原因,大概因为虽然蓝牙协议是完整的,但是并没有具体的实现。蓝牙芯片公司只负责提供最底层的API_蓝牙原理图详解

从未在一起更让人遗憾_“从未在一起和最终没有在一起哪个更遗憾”-程序员宅基地

文章浏览阅读7.7k次。图/源于网络文/曲尚菇凉1.今天早上出门去逛街,在那家冰雪融城店里等待冰淇淋的时候,听到旁边两个女生在讨论很久之前的一期《奇葩说》。那期节目主持人给的辩论题是“从未在一起和最终没有在一起哪个更遗憾”,旁边其中一个女生说,她记得当时印象最深的是有个女孩子说了这样一句话。她说:“如果我喜欢一个人呢,我就从第一眼到最后一眼,把这个人爱够,把我的感觉用光,我只希望那些年让我成长的人是他,之后的那些年他喝过..._从未在一起更遗憾

【CSDN精选】基于龙芯1B200的rt-thread基础_龙芯1b200参数-程序员宅基地

文章浏览阅读927次,点赞15次,收藏13次。龙芯是中国的一款自主设计的处理器架构,由中国科学院计算技术研究所(ICT)主导研发。龙芯处理器最早的版本为Loongson-1,其后发展出Loongson-2、Loongson-3等系列。这些处理器主要用于高性能计算、服务器、嵌入式系统等领域。Loongson架构具有独立知识产权,是中国自主研发的一种指令集架构。龙芯的设计旨在实现对计算机体系结构的自主掌握,减少对外部知识产权的依赖。RT-Thread(Real-Time Thread)是一个开源的实时嵌入式操作系统。_龙芯1b200参数

Python函数知识点(详解)-程序员宅基地

文章浏览阅读6w次,点赞584次,收藏2.9k次。本篇总结了Python函数相关的基础知识点,代码案例超详细,欢迎阅读,交流!_python函数知识点

虚拟机如何在net模式下进行联网_虚拟机net网络-程序员宅基地

文章浏览阅读1.7k次,点赞3次,收藏7次。虚拟机如何在net模式下进行联网(centos7)1.首先你需要先将虚拟机安装好,如果不会可以参考[centos7安装教程](https://blog.csdn.net/qq_44714603/article/details/88829423?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522161949204316780274178621%2522%252C%2522scm%2522%253A%252220140713.130102334…_虚拟机net网络

python中的range()函数_python range函数-程序员宅基地

文章浏览阅读6.5w次,点赞59次,收藏241次。range()函数:用于生成一个整数序列;range()的三种创建方式:第一种:只有一个参数(小括号中只给了一个数)即range(stop)例如:range(10)指的是默认从0开始,步长为1,不包括10;注意:的运行结果为:;要想输出0-9的数字序列则应该是的结果为;第二种:range(start,stop) (给了两个参数,即小括号中给了两个数)r=range(1,10) print(list(r)) 运行结果为:;第三种:range(start,stop,step):._python range函数

随便推点

强化学习在制造业领域的应用:智能制造的未来-程序员宅基地

文章浏览阅读223次,点赞3次,收藏5次。1.背景介绍制造业是国家经济发展的重要引擎,其产能和质量对于国家经济的稳定和发展具有重要意义。随着工业技术的不断发展,制造业的生产方式也不断发生变化。传统的制造业通常依赖于人工操作和手工艺,这种方式的缺点是低效率、低产量和不稳定的质量。随着信息化、智能化和网络化等新技术的出现,制造业开始向智能制造迈出了第一步。智能制造的核心是通过大数据、人工智能、计算机视觉等技术,实现制造过程的智能化、自动化...

ansible--安装与使用_pip安装ansible-程序员宅基地

文章浏览阅读938次。系列文章目录文章目录系列文章目录 前言 一、ansible是什么? 二、使用步骤 1.引入库 2.读入数据 总结前言菜鸟一只,刚开始使用,仅作以后参考使用。边学习,边记录,介绍一下最基础的使用,可能会有理解不到位的地方,可以共同交流,废话不多说,走起。一、ansible 简介?ansible是自动化运维工具的一种,基于Python开发,可以实现批量系统配置,批量程序部署,批量运行命令,ansible是基于模块工作的,它本身没有批量部署的能力,真正.._pip安装ansible

RPMs系列卟啉框架材料ZnMn-RPM/AZn-RPM/FeZn-RPM/ZnPO-MOF齐岳供应金属-四羧基苯基卟啉(M-TCPPs)及三维框架卟啉材料[Cu(TPyP)Cu2Mo3O1]_fezn-5是什么意思-程序员宅基地

文章浏览阅读298次。RPMs系列卟啉框架材料ZnMn-RPM/AZn-RPM/FeZn-RPM/ZnPO-MOF齐岳供应金属-四羧基苯基卟啉(M-TCPPs)及三维框架卟啉材料[Cu(TPyP)Cu2Mo3O1]_fezn-5是什么意思

51单片机与ESP8266-01s模块通讯点灯_51单片机与eps826601s通信-程序员宅基地

文章浏览阅读9.9k次,点赞10次,收藏92次。前言 本文章为方便新手上手直接用最简单的点灯展示,关于ESP8266-01s的AT指令类就上网搜有很多这就不多讲了。接线ESP-01S USB转TTL 51单片机 VCC 3.3V 3.3V GND GND GND EN 3.3V 3.3V TX RX P3.0 RX TX P3.1 IO0(注刷固件时插) GND(注刷固件时插) 一、ESP8266-01S接US..._51单片机与eps826601s通信

麒麟820也迎来鸿蒙系统,魅族适配麒麟820系统 和鸿蒙OS,魅族要入赘华为系-程序员宅基地

文章浏览阅读188次。蜗居在珠海的小厂魅族,虽然是“小厂”,但是其一路走来的历史,一直被科技圈津津乐道,而深入简出的带头大哥黄章,更是魅友的精神领袖。魅族是一个很低调的企业,但是却有这样一种魅力,总能有意无意的成为新闻的主角,比如近日知名科技大V中国IT杂谈发布了这样一条微博:魅族正在适配麒麟820和鸿蒙系统,未来还要加入华为系。关于这条信息的真实性后面再说,我们先来讨论下华为和魅族是否真的可以成为CP,共同走向人生巅..._鸿蒙420支持麒麟820吗

推荐文章

热门文章

相关标签