回溯,说实话,难搞,反正我现在也在路上,还没如土。 前期准备,要玩得转回溯,递归的基础还是要有的,所以前些日子我就先把递归部分给办了。 如果不是大佬,咱还是老老实实先把递归过一遍吧。 岛屿最大面积、八皇后问题、括号生成感觉比较简单,所以思路讲的就比较简陋,适合入门练手,建议看其他题目的讲解(全排列那题)。 给定一个包含了一些 0 和 1 的非空二维数组 grid 。 一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。 找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。) 示例 1: 对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。 示例 2: 对于上面这个给定的矩阵, 返回 0。 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/max-area-of-island 这题其实我自己做出来了,把它放在第一题嘛,我觉得因为它是比较简单的。 八皇后问题是一个古老的问题,于1848年由一位国际象棋棋手提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,如何求解? 将每一个棋子放在当前列的最前端,再放下一个棋子。 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。 示例: 来源:力扣(LeetCode) 如果左括号数量不大于 nnn,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号。 给定一个 没有重复 数字的序列,返回其所有可能的全排列。 示例: 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/permutations “全排列”就是一个非常经典的“回溯”算法的应用。我们知道,N 个数字的全排列一共有 N!N!N! 这么多个。 大家可以尝试一下在纸上写 3 个数字、4 个数字、5 个数字的全排列,相信不难找到这样的方法。 以数组 [1, 2, 3] 的全排列为例。 我们只需要按顺序枚举每一位可能出现的情况,已经选择的数字在接下来要确定的数字中不能出现。按照这种策略选取就能够做到不重不漏,把可能的全排列都枚举出来。 使用编程的方法得到全排列,就是在这样的一个树形结构中进行编程,具体来说,就是执行一次深度优先遍历,从树的根结点到叶子结点形成的路径就是一个全排列。 说明: 1、每一个结点表示了“全排列”问题求解的不同阶段,这些阶段通过变量的“不同的值”体现; 下面我们解释如何编码: 1、首先这棵树除了根结点和叶子结点以外,每一个结点做的事情其实是一样的,即在已经选了一些数的前提,我们需要在剩下还没有选择的数中按照顺序依次选择一个数,这显然是一个递归结构; 2、递归的终止条件是,数已经选够了,因此我们需要一个变量来表示当前递归到第几层,我们把这个变量叫做 depth; 3、这些结点实际上表示了搜索(查找)全排列问题的不同阶段,为了区分这些不同阶段,我们就需要一些变量来记录为了得到一个全排列,程序进行到哪一步了,在这里我们需要两个变量: 我们把这两个变量称之为“状态变量”,它们表示了我们在求解一个问题的时候所处的阶段。 4、在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。 5、另外,因为是执行深度优先遍历,从较深层的结点返回到较浅层结点的时候,需要做“状态重置”,即“回到过去”、“恢复现场”,我们举一个例子。 从 [1, 2, 3] 到 [1, 3, 2] ,深度优先遍历是这样做的,从 [1, 2, 3] 回到 [1, 2] 的时候,需要撤销刚刚已经选择的数 3,因为在这一层只有一个数 3 我们已经尝试过了,因此程序回到上一层,需要撤销对 2 的选择,好让后面的程序知道,选择 3 了以后还能够选择 2。 这种在遍历的过程中,从深层结点回到浅层结点的过程中所做的操作就叫“回溯”。 作者:liweiwei1419 1、为什么使用深度优先遍历? (1)首先是正确性,只有遍历状态空间,才能得到所有符合条件的解; (2)在深度优先遍历的时候,不同状态之间的切换很容易,可以再看一下上面有很多箭头的那张图,每两个状态之间的差别只有 1 处,因此回退非常方便,这样全局才能使用一份状态变量完成搜索; (3)如果使用广度优先遍历,从浅层转到深层,状态的变化就很大,此时我们不得不在每一个状态都新建变量去保存它,从性能来说是不划算的; (4)如果使用广度优先遍历就得使用队列,然后编写结点类。使用深度优先遍历,我们是直接使用了系统栈,系统栈帮助我们保存了每一个结点的状态信息。于是我们不用编写结点类,不必手动编写栈完成深度优先遍历。大家可以尝试使用广度优先遍历实现一下,就能体会到这一点。 2、不回溯可不可以? 可以。搜索问题的状态空间一般很大,如果每一个状态都去创建新的变量,时间复杂度是 O(N)。在候选数比较多的时候,在非叶子结点上创建新的状态变量的性能消耗就很严重。 就本题而言,只需要叶子结点的那个状态,在叶子结点执行拷贝,时间复杂度是 O(N)。路径变量在深度优先遍历的时候,结点之间的转换只需要 O(1)。 最后,由于回溯算法的时间复杂度很高,因此,如果在遍历的时候,如果能够提前知道这一条分支不能搜索到满意的结果,就可以提前结束,这一步操作称之为剪枝。 回溯算法会大量应用“剪枝”技巧达到以加快搜索速度。有些时候,需要做一些预处理工作(例如排序)才能达到剪枝的目的。预处理工作虽然也消耗时间,但一般而且能够剪枝节约的时间更多。还有正是因为回溯问题本身时间复杂度就很高,所以能用空间换时间就尽量使用空间。否则时间消耗又上去了。 作者:liweiwei1419 第 1 步都是先画图,画图是非常重要的,只有画图才能帮助我们想清楚递归结构,想清楚如何剪枝。就拿题目中的示例,想一想人手动操作是怎么做的,一般这样下来,这棵递归树都不难画出。 即在画图的过程中思考清楚: 作者:liweiwei1419 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 说明: 来源:力扣(LeetCode) 在前面的一些步骤做出一些修改,并重新尝试找到可行解。 给出如下回溯函数 backtrack(combination, next_digits) ,它将一个目前已经产生的组合 combination 和接下来准备要输入的数字 next_digits 作为参数。 如果没有更多的数字需要被输入,那意味着当前的组合已经产生好了。 其实代码里的备注还更加全全面 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 说明:解集不能包含重复的子集。 示例: 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/subsets 给定一个二维网格和一个单词,找出该单词是否存在于网格中。 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 示例: 给定 word = “ABCCED”, 返回 true 提示: 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/word-search 绕了一圈,又绕回“岛屿最大面积”这种二维铺开搜索的题目了。 作者:liweiwei1419 等着二刷吧,太难了。。。文章目录
前期准备
【LeetCode】递归 原理入门+复杂度计算+练手试题
接下来直接上试题,我都亲手抄过好几遍。。。岛屿最大面积
[[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]]
[[0,0,0,0,0,0,0,0]]
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。思路
从头开始遍历,遇到“1”就展开上下左右搜索,搜空了就回溯,在搜索过程中计数,并将搜索到的位置全部置0,防止二次污染。代码实现
int checkIsland(vector<vector<int>>& 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<int>>& 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; }
八皇后问题
思路
如果下一个棋子没地方放了,就回溯到前一个棋子,将其往后一个适宜位置挪动。
具体思路都在代码里了。代码实现
#include<iostream> 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 = 3 输出:[ "((()))", "(()())", "(())()", "()(())", "()()()" ]
链接:https://leetcode-cn.com/problems/generate-parentheses
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。思路
代码实现
vector<string> generateParenthesis(int n) { vector<string> res; generateParenthesisDFS(n, n, "", res); return res; } void generateParenthesisDFS(int left, int right, string out, vector<string> &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] ]
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。思路
我们先写以 1 开头的全排列,它们是:[1, 2, 3], [1, 3, 2]; 再写以 2 开头的全排列,它们是:[2, 1, 3], [2, 3, 1]; 最后写以 3 开头的全排列,它们是:[3, 1, 2], [3, 2, 1]。
在枚举第一位的时候,有 3 种情况。 在枚举第二位的时候,前面已经出现过的数字就不能再被选取了; 在枚举第三位的时候,前面 2 个已经选择过的数字就不能再被选取了。
2、这些变量的不同的值,也称之为“状态”;
3、使用深度优先遍历有“回头”的过程,在“回头”以后,状态变量需要设置成为和先前一样;
4、因此在回到上一层结点的过程中,需要撤销上一次选择,这个操作也称之为“状态重置”;
5、深度优先遍历,可以直接借助系统栈空间,为我们保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈。
6、深度优先遍历通过“回溯”操作,实现了全局使用一份状态变量的效果。
(1)已经选了哪些数,到叶子结点时候,这些已经选择的数就构成了一个全排列;
(2)一个布尔数组 used,初始化的时候都为 false 表示这些数还没有被选择,当我们选定一个数的时候,就将这个数组的相应位置设置为 true ,这样在考虑下一个位置的时候,就能够以 O(1) 的时间复杂度判断这个数是否被选择过,这是一种“以空间换时间”的思想。
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。代码实现
vector<vector<int>> permute(vector<int>& nums) { if(nums.empty()) return {}; // 存放最后结果 vector<vector<int>> ans; // 存放某一个排列 vector<int> temp; // 判断该数字是否被使用过 vector<bool> used(nums.size(),false); // 进行递归求解 dfs(ans,temp,used,nums); return ans; } void dfs(vector<vector<int>>& ans,vector<int>& temp,vector<bool>& used, const vector<int>& 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(); } }
再说两句
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。解回溯题的一般步骤
1、分支如何产生;
2、题目需要的解在哪里?是在叶子结点、还是在非叶子结点、还是在从跟结点到叶子结点的路径?
3、哪些搜索是会产生不需要的解的?例如:产生重复是什么原因,如果在浅层就知道这个分支不能产生需要的结果,应该提前剪枝,剪枝的条件是什么,代码怎么写?
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。电话号码的字母组合
示例:输入:"23" 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
链接:https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。思路
如果还有数字需要被输入:
遍历下一个数字所对应的所有映射的字母。
将当前的字母添加到组合最后,也就是 combination = combination + letter 。
重复这个过程,输入剩下的数字: backtrack(combination + letter, next_digits[1:]) 。代码实现
//1. 用map记录每个数字按键对应的所有字母 map<char, string> M = { {'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5', "jkl"}, {'6', "mno"}, {'7', "pqrs"}, {'8', "tuv"}, {'9', "wxyz"} }; //2. 存储最终结果和临时结果的变量 vector<string> ans; string current; //3. DFS函数,index是生成临时结果字串的下标, //每一个digits[index]数字对应临时结果current[index]的一位字母 void DFS(int index, string digits) { //出口 if(index == digits.size()) { ans.push_back(current); return; } //递归调用 //对于当前输入的第index号数字(digits[index]), //枚举其对应的所有字母(M[digits[index]][i]) for(int i = 0; i < M[digits[index]].size(); i++) { current.push_back(M[digits[index]][i]); //临时结果压入一个字母 DFS(index + 1, digits); //以在当前位置压入该字母这一“情况”为前提,构造此“分支”的后续结果 current.pop_back(); //状态还原,例如临时结果从 "ab" -> "a",下一次循环尝试"ac" } } vector<string> letterCombinations(string digits) { if(digits.size() == 0) { return ans; } DFS(0, digits); return ans; }
子集
输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。思路
定义一个回溯方法 backtrack(first, curr),第一个参数为索引 first,第二个参数为当前子集 curr。如果当前子集构造完成,将它添加到输出集合中。 否则,从 first 到 n 遍历索引 i。 将整数 nums[i] 添加到当前子集 curr。 继续向子集中添加整数:backtrack(i + 1, curr)。 从 curr 中删除 nums[i] 进行回溯。
代码实现
class Solution { private: vector<vector<int>> ret; vector<int> temp; public: void DFS(vector<int>& nums,int pre,int level) { //退出条件:前面插入的是数组中最后一个元素 if (pre == nums.back()) { temp.clear(); return; } for (int i = 0; i < nums.size(); i++) { if (nums[i] <= pre) continue; temp.push_back(nums[i]); ret.push_back(temp); DFS(nums, nums[i],i+1); if (level) return; } } vector<vector<int>> subsets(vector<int>& nums) { ret.push_back({}); if (nums.empty()) return ret; DFS(nums, INT_MIN,0); return ret; } };
单词搜索
board = [ ['A','B','C','E'], ['S','F','C','S'], ['A','D','E','E'] ]
给定 word = “SEE”, 返回 true
给定 word = “ABCB”, 返回 falseboard 和 word 中只包含大写和小写英文字母。 1 <= board.length <= 200 1 <= board[i].length <= 200 1 <= word.length <= 10^3
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。思路
链接:https://leetcode-cn.com/problems/word-search/solution/zai-er-wei-ping-mian-shang-shi-yong-hui-su-fa-pyth/
来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。代码实现
int dir[4][4]={{-1,0},{1,0},{0,-1},{0,1}}; bool exist(vector<vector<char>>& board, string word) { int m=board.size(); int n=board[0].size(); vector<vector<bool>> visited(m,vector<bool>(n)); for(int i=0;i<m;i++){ for(int j=0;j<n;j++){ if(dfs(i,j,0,board,word,visited)) return true; } } return false; } bool dfs(int x,int y,int index,vector<vector<char>>& board,string &word,vector<vector<bool>>& visited){ if(index==word.size()-1){ return word[index]==board[x][y]; } if(word[index]==board[x][y]){ visited[x][y]=true; for(int i=0;i<4;i++){ int new_x=x+dir[i][0]; int new_y=y+dir[i][1]; if(new_x>=0&&new_x<board.size()&&new_y>=0&&new_y<board[0].size()&&!visited[new_x][new_y]) if(dfs(new_x,new_y,index+1,board,word,visited)) return true; } visited[x][y]=false; } return false; }
本网页所有视频内容由 imoviebox边看边下-网页视频下载, iurlBox网页地址收藏管理器 下载并得到。
ImovieBox网页视频下载器 下载地址: ImovieBox网页视频下载器-最新版本下载
本文章由: imapbox邮箱云存储,邮箱网盘,ImageBox 图片批量下载器,网页图片批量下载专家,网页图片批量下载器,获取到文章图片,imoviebox网页视频批量下载器,下载视频内容,为您提供.
阅读和此文章类似的: 全球云计算