LeetCode刷题记录|LeetCode刷题总结((6)递归和回溯的相关问题)

递归和回溯的相同点都是自顶向下的,但是递归更注重的是递归回来的状态,而回溯更关注的是通过回溯遍历到所有的解空间的状态,会根据具体的问题场景对所有解空间做相应的标记或者说处理。
17. 电话号码的字母组合 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
LeetCode刷题记录|LeetCode刷题总结((6)递归和回溯的相关问题)
文章图片

示例:

输入:"23" 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
// 思路: 抽象成一个具有递归结构的问题 class Solution {private: const string letterMap[10] = { " ",// 0 "",// 1 "abc",// 2 "def",// 3 "ghi",// 4 "jkl",// 5 "mno",// 6 "pqrs",// 7 "tuv",// 8 "wxyz"// 9 }; vector res; // s中保存了此时从digits[0...index-1]翻译得到的一个字母字符串 // 寻找和digits[index]匹配的字母,获得digits[0...index]翻译得到的解 void findCombination(const string& digits, int index, const string& s) {// 终止条件 if (index == digits.size()) { res.push_back(s); return; }char c = digits[index]; assert(c >= '2' && c <= '9'); string letters = letterMap[c - '0']; // 当前数字对应的可选择的字母串 // 将当前的可选择的字母串中的字母加到字母字符串后面,进行下一次迭代 for (int i = 0; i < letters.size(); i++) { findCombination(digits, index + 1, s + letters[i]); }return; }public: vector letterCombinations(string digits) {res.clear(); if (digits == "") return res; findCombination(digits, 0, ""); return res; } };


46. 全排列 给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

// 思路: 全排列,其实回溯法最常解决的问题就是全排列问题,本质上就是不断暴力地以回溯的方式枚举出所有的情况 // 这里和上一题的不同点是这里排列的数字选取相互之间是有冲突的,要保证不能重复出现,方法是维护好一个vector uesd 向量,包括前向的维护和回溯时的维护 class Solution1 {private: vector> res; vector used; // p中保存了一个有index个元素的排列 // 向这个排列的末尾添加第index+1个元素,以获得一个有index+1个元素的排列 void generatePermutation(const vector& nums, int index, vector& p) {if (index == nums.size()) { res.push_back(p); return; }// 回溯法 for (int i = 0; i < nums.size(); i++) { if (!used[i]) { p.push_back(nums[i]); used[i] = true; generatePermutation(nums, index + 1, p); // 继续寻找排列中的下一位数 // 回溯到这里的时候要把当前排列位之后的被使用的元素也回溯,不然被占用,那后续的排列就不能进行了 p.pop_back(); used[i] = false; } }// 遍历完了所有的排序 return; }public: vector> permute(vector& nums) {res.clear(); if (nums.size() == 0) return res; used = vector(nums.size(), false); vector p; generatePermutation(nums, 0, p); return res; } };


77. 组合 给定两个整数 nk,返回 1 ... n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]

// 思路: 在屡不清递归关系的时候,可以自己先画出递归的组合图(树状),不方便画,这里就不画了 class Solution2 {private: vector> res; // 求解C(c,k),当前已经找到的组合存贮在c中,程序需要从start开始搜索新的元素以填进c void generateCombinations(int n, int k, int start, vector& c) {if (c.size() == k) { res.push_back(c); return; }开始回溯 //for (int i = start; i <= n; i++) { // c.push_back(i); // generateCombinations(n, k, i + 1, c); // // 回溯到未推入数据之前的状态,以尝试下一个待选元素 // c.pop_back(); //}// 回溯部分的剪枝优化 // 当前我们的c的size为c.size(),要求的c的最终的长度为k,所以待放置的长度为k-c.size() // 即要求当前的i和n之间[i,n]相差最少为k-c.size()个元素,那么才有可能填满c,不然就没必要继续搜索这条枝了 for (int i = start; i <= n - (k - c.size()) + 1; i++) { c.push_back(i); generateCombinations(n, k, i + 1, c); // 回溯到未推入数据之前的状态,以尝试下一个待选元素 c.pop_back(); }// 遍历完所有的情况 return; }public: vector> combine(int n, int k) {res.clear(); // 边界条件判断 if (n <= 0 || k <= 0 || k > n) return res; vector c; generateCombinations(n, k, 1, c); return res; } };


39. 组合总和 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
  • 所有数字(包括 target)都是正整数。
  • 解集不能包含重复的组合。
示例 1:
输入: candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ]

示例 2:
输入: candidates = [2,3,5], target = 8, 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]

// 思路: 其实还是回溯法暴力穷举所有的组合,只不过,回溯的时候要注意不能有重复组合的条件限制,所以程序在回溯部分的遍历是需要做一点处理的,不然会出现重复组合的情况 // 其实我感觉正确的写好回溯的一个小技巧是写程序前先画出递归的树形图,就能一目了然树形图的变化和限制条件 class Solution3 {private: vector> res; // 寻找组合使得和为target,sum为当前已找到组合c的和,在此基础上,找寻下一个数, // 考虑到不能有重复的组合,所以回溯法时候的起始遍历起点会有点不一样,其中index表示当前回溯遍历的candidates的起始索引 void generateCombination(vector& candidates, int target, int index, int sum, vector& c) {if (sum == target) { res.push_back(c); return; }// 由于题目已经说明candidates中的数据都是正整数,所以找不到的终止条件可以这么定义 if (sum > target || index >= candidates.size()) return; // 回溯法找组合,回溯的时候只找当前索引以及之后的,这样就能防止出现重复组合的情况,因为重复的情况在之前的索引中已经遍历到了 for (int i = index; i < candidates.size(); i++) { c.push_back(candidates[i]); sum += candidates[i]; generateCombination(candidates, target, i, sum, c); // 数据回溯,以寻找下一种组合 c.pop_back(); sum -= candidates[i]; }return; }public: vector> combinationSum(vector& candidates, int target) {res.clear(); if (candidates.size() == 0) return res; vector c; generateCombination(candidates, target, 0, 0, c); return res; } };


40. 组合总和 II 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
  • 所有数字(包括目标数)都是正整数。
  • 解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]

示例 2:
输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ]

class Solution4 {private: vector> res; // 寻找组合使得和为target,sum为当前已找到组合c的和,在此基础上,找寻下一个数, // 这一题和上一题的区别是candidates中有重复的数字,并且只能使用一次, // 只能使用一次只需要在递归遍历的时候index+1即可, // 但是candidates中重复数字,又要解决重复组合,该怎么办呢? // 办法是首先我们队candidates排好序,然后在查询到当前位时,检查是否等于当前位之前的数值,如果相同,但是之前那一位没有用过就是重复了,如果用过那就没问题 void generateCombination(vector& candidates, int target, int index, int sum, vector& c, vector& used) {if (sum == target) { res.push_back(c); return; }// sum已经大于target,加后续的正数是没有用的,退出;索引完所有的数字后还不够达到target,退出 if (sum > target || index >= candidates.size()) return; // 回溯部分 for (int i = index; i < candidates.size(); i++) { // 首先判断一下之前的相同的数字是否已经使用过,当然candidates的第一位没必要检查,从第二位开始检查 if (i >= 1 && candidates[i] == candidates[i - 1] && !used[i - 1]) // 发现之前有重复的没使用的相同的数字,那么跳过,因为就算是递归出结果也是重复的组合 continue; c.push_back(candidates[i]); used[i] = true; sum += candidates[i]; generateCombination(candidates, target, i + 1, sum, c, used); // 回溯 c.pop_back(); used[i] = false; sum -= candidates[i]; }return; }public: vector> combinationSum2(vector& candidates, int target) {res.clear(); if (candidates.size() == 0) return res; // 首先对candidates排序一下 sort(candidates.begin(), candidates.end()); vector c; vector used(candidates.size(), false); generateCombination(candidates, target, 0, 0, c, used); return res; } };


79. 单词搜索 给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board = [ ['A','B','C','E'], ['S','F','C','S'], ['A','D','E','E'] ]给定 word = "ABCCED", 返回 true. 给定 word = "SEE", 返回 true. 给定 word = "ABCB", 返回 false.

// 思路: 是一个二维平面搜索的经典例题 // 使用index指向当前要搜索的字母,使用startX和startY指向当前的搜索网格,使用回溯法搜索遍历,其中要注意搜索的约束条件 class Solution5 {private: // 搜索映射表 int d[4][2] = { {-1,0},{0,1},{1,0},{0,-1} }; // 网格访问记录表 vector> visited; // 网格长宽 int m; // 行数 int n; // 列数 // 判断一个坐标(x,y)是否在网格中 bool inArea(int x, int y) { if (x >= 0 && x < m && y >= 0 && y < n) return true; return false; } // (startX,startY)指向当前要搜索的网格,index指向当前要搜索的字母,程序要做的是从board[startX][startY]开始,寻找word[index...word.size()) bool searchWord(const vector>& board, const string& word, int index, int startX, int startY) {// 终止条件 if (index == word.size() - 1) return board[startX][startY] == word[index]; // 找到了当前的的对应网格,开始回溯法搜索 if (board[startX][startY] == word[index]) { visited[startX][startY] = true; // 从startX和startY出发,向四周寻找下一个匹配网格 for (int i = 0; i < 4; i++) { int newX = startX + d[i][0]; int newY = startY + d[i][1]; if (inArea(newX, newY) && !visited[newX][newY]) { if (searchWord(board, word, index + 1, newX, newY)) return true; } } // 数据回溯,当前网格往下没有符合的路径 visited[startX][startY] = false; }return false; }public: bool exist(vector>& board, string word) {// 数据初始化 m = board.size(); n = board[0].size(); visited = vector>(m, vector(n, false)); // 路径搜索起始点遍历所有网格 for (int i = 0; i < board.size(); i++) { for (int j = 0; j < board[i].size(); j++) { if (searchWord(board, word, 0, i, j)) return true; } }return false; } };


200. 岛屿的个数 给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入: 11110 11010 11000 00000输出: 1

示例 2:
输入: 11000 11000 00100 00011输出: 3

// 思路: 其实和上一题是差不多的,只不过不需要相关信息的回溯了,是一个floodfill问题 class Solution6 {private: // 移动数组 int d[4][2] = { {0,1},{1,0},{0,-1},{-1,0} }; int m, n; vector> visited; bool inArea(int x, int y) { if (x >= 0 && x < m && y >= 0 && y < n) return true; return false; } // 从当前的格点grid[x][y]进行floodfill,即grid[x][y]确认是一个符合条件的栅格点! void dfs(const vector>& grid, int x, int y) {visited[x][y] = true; for (int i = 0; i < 4; i++) { int newX = x + d[i][0]; int newY = y + d[i][1]; if (inArea(newX, newY) && !visited[newX][newY] && grid[newX][newY] == '1') dfs(grid, newX, newY); }return; }public: int numIslands(vector>& grid) {if (grid.size()==0) return 0; m = grid.size(); n = grid[0].size(); visited = vector>(m, vector(n, false)); int res = 0; for (int i = 0; i < grid.size(); i++) { for (int j = 0; j < grid[i].size(); j++) { if (grid[i][j]=='1' && !visited[i][j]) {// 发现可搜寻的新岛屿的起点 res++; dfs(grid, i, j); // floodfill } } }return res; } };


130. 被围绕的区域 给定一个二维的矩阵,包含 'X''O'(字母 O)。
找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O''X' 填充。
示例:
X X X X X O O X X X O X X O X X

运行你的函数后,矩阵变为:
X X X X X X X X X X X X X O X X

解释:
被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
// 思路: 其实还是floodfill问题,只不过floodfill出来的区域要求就是有效的'o'区域,最后翻转这块区域即可 class Solution7 { // 移动数组 int d[4][2] = { { 0,1 },{ 1,0 },{ 0,-1 },{ -1,0 } }; int m, n; vector> visited; bool inArea(int x, int y) { if (x >= 0 && x < m && y >= 0 && y < n) return true; return false; } // 从当前的格点board[x][y]进行floodfill,即board[x][y]确认是一个符合条件的栅格点,所以dfs下来会找出所有联通的'o'区域 // 但是o是有限制的,如果其中有在边上的o,那么这一块'o'区域就是无效的,rightFlag设置为false,所以后续程序就不会翻转这一块区域 // 但是这一块区域检索出来其实是有必要的,在主程序中就不会多次在这个区域上进行dfs了,优化了程序 void dfs(const vector>& board, int x, int y, bool& rightFlag) {// 首先检测一下是不是在边上了 if (x == 0 || x == m - 1 || y == 0 || y == n - 1) rightFlag = false; // 下面的程序还是一样 执行floodfill visited[x][y] = true; for (int i = 0; i < 4; i++) { int newX = x + d[i][0]; int newY = y + d[i][1]; if(inArea(newX, newY) && !visited[newX][newY] && board[newX][newY] == 'O') dfs(board, newX, newY, rightFlag); }return; } // 由于visited中的true存储的是当前所有的连通区域,不能代表最终的有效区域,所以要根据rightFlag进行相应的有效区域的更新 void rightOAreaUpdate(const vector>& visited, vector>& rightOArea, bool rightFlag) { for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (visited[i][j]) { if (rightFlag && rightOArea[i][j] == 0) rightOArea[i][j] = 1; if (!rightFlag && rightOArea[i][j] == 0) rightOArea[i][j] = -1; } } } }public: void solve(vector>& board) {if (board.size() <= 2) return; if (board[0].size() <= 2) return; m = board.size(); n = board[0].size(); visited = vector>(m, vector(n, false)); vector> rightOArea = vector>(m, vector(n, 0)); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (board[i][j] == 'O' && !visited[i][j]) { bool rightFlag = true; dfs(board, i, j, rightFlag); // 用visited更新rightOArea rightOAreaUpdate(visited, rightOArea, rightFlag); } } }// 最后进行相应的翻转 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (rightOArea[i][j] == 1) board[i][j] = 'X'; } }return; } };


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

上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。
示例:
输入: 4 输出: [ [".Q..",// 解法 1 "...Q", "Q...", "..Q."], ["..Q.",// 解法 2 "Q...", "...Q", ".Q.."] ] 解释: 4 皇后问题存在两个不同的解法。

// 思路: n皇后问题,我们每一行每一行的放置皇后,通过检索纵向,对角线的约束来寻找可能的皇后放置位置 class Solution8 {private: vector col; vector dia1, dia2; vector res; // 尝试在一个n皇后问题中,摆放第index行的皇后,结果存储在row[index]=k,代表第index行的皇后放置在第k列 void putQueen(int n, int index, vector& row) {if (index == n) { res.push_back(generateBoard(n, row)); }// 回溯法 for (int i = 0; i < n; i++) { if (!col[i] && !dia1[index + i] && !dia2[index - i + n - 1]) { // 纵向和两种对角线的判断(一种对角线上的横纵坐标和相等,另一种横坐标减去纵坐标相等) row.push_back(i); col[i] = true; dia1[index + i] = true; dia2[index - i + n - 1] = true; putQueen(n, index + 1, row); // 数据回溯 col[i] = false; dia1[index + i] = false; dia2[index - i + n - 1] = false; row.pop_back(); } }return; } // 将vector row这种数据形式整理成要输出的格式 vector generateBoard(int n, const vector& row) {assert(row.size() == n); vector res(n, string(n, '.')); for (int i = 0; i < n; i++) { res[i][row[i]] = 'Q'; }return res; }public: vector solveNQueens(int n) {res.clear(); col = vector(n, false); dia1 = vector(2 * n - 1, false); dia2 = vector(2 * n - 1, false); vector row; putQueen(n, 0, row); return res; } };

【LeetCode刷题记录|LeetCode刷题总结((6)递归和回溯的相关问题)】

    推荐阅读