九、回溯算法

学习来源:
代码随香炉:https://www.programmercarl.com/
labuladong算法:https://labuladong.github.io/algo/

回溯算法

**一般来说:
组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
**

在这里插入图片描述

递归终止条件
单层搜索的逻辑

0 回溯算法框架

解决for循环无法写出的场景,通过回溯可以暴力找出解决方法

在这里插入图片描述

1 组合

组合问题 (返回一个序列的组合)

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

在这里插入图片描述

在这里插入图片描述

减枝 优化 已经存了x个,剩下的通过总数k进行减去

在这里插入图片描述

组合总和III

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

组合总和 (数组无重复元素,无限制选取)

在这里插入图片描述
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。(无k个数的限制)
candidates 中的数字可以无限制重复被选取。
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。

不用i+1,表示可以重读选择

在这里插入图片描述
在这里插入图片描述

组合总和II (数组有重复元素,每个只能用一次)

在这里插入图片描述

startindex用了i+1,表示只能选择一次

used数组进行去重
我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。

强调一下,树层去重的话,需要对数组排序!

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。

在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
used[i - 1] == false,说明同一树层candidates[i - 1]使用过

在这里插入图片描述

在这里插入图片描述
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) // 剪枝 (前提已经排序好)

电话号码的字母组合

在这里插入图片描述
在这里插入图片描述

2 分割

终止条件的判断 1 分割线到达末尾,或者按照段数分割 2 注意break 和continue的使用,回文 IP合法的判断

分割回文串

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”]

关键点:
切割问题,有不同的切割方式
判断回文

例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中在选组第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段…。

在这里插入图片描述

需要startIndex 切割过的地方不能重复切割
在这里插入图片描述

复原IP

在这里插入图片描述

  1. 判断子串是否合法:
    主要考虑到如下三点:
    段位以0为开头的数字不合法
    段位里有非正整数字符不合法
    段位如果大于255了不合法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. 子集

但是要清楚子集问题和组合问题、分割问题的的区别,子集是收集树形结构中树的所有节点的结果。
而组合问题、分割问题是收集树形结构中叶子节点的结果。

子集 (数组元素不同)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

子集II (数组元素有相同的 子集不重复)

去重 (树枝去重 和 树层去重)
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出: [ [2], [1], [1,2,2], [2,2], [1,2], []]

去重的话一定要先排序

从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集!

在这里插入图片描述

在这里插入图片描述

用集合去重

在这里插入图片描述
本题也可以不使用used数组来去重,因为递归的时候下一个startIndex是i+1而不是0。
在这里插入图片描述

4 排列

全排列 (数组不含重复数字)

在这里插入图片描述
排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示:

在这里插入图片描述
在这里插入图片描述

全排列2 (数组含重复数字)

在这里插入图片描述

startindex不用定义,直接从0开始遍历到整个数组大小,使用used数组进行树枝去用过的元素。
去重过程中,树层去重和树枝去重都是可以的。树层去重更加高效一些。

在这里插入图片描述

回溯的时间复杂度

5 棋盘问题

在这里插入图片描述

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

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
一个for循环遍历列 递归遍历深度的行

在这里插入图片描述
在这里插入图片描述

解数独 二维递归 两个for循环

在这里插入图片描述

在这里插入图片描述

6 深搜

递增子序列

在这里插入图片描述

set去重
在这里插入图片描述

重新安排行程 !!

在这里插入图片描述
在这里插入图片描述

7 岛屿题目

一文秒杀所有岛屿题目

7.1 岛屿数量

二维矩阵四周可以认为也是被海水包围的,所以靠边的陆地也算作岛屿。

在这里插入图片描述

class Solution {
public:
    int res = 0;
    int numIslands(vector<vector<char>>& grid) {
        int row = grid.size();
        int col = grid[0].size();
         // 遍历 grid
  
      for(int i =0;i<row;i++){
            for(int j =0;j<col;j++){
                if(grid[i][j]=='1'){
                    res++;// 每发现一个岛屿,岛屿数量加一
                    dfs(grid,i,j); // 然后使用 DFS 将岛屿淹了
                }
            }
        }
        return res;
    }
    
    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(vector<vector<char>>& grid,int i,int j){
        int row = grid.size();
        int col = grid[0].size();
        if(i<0||i>=row||j<0||j>=col){
            return;
        }
        if(grid[i][j]=='0'){
            return;
        }
        grid[i][j]='0';// 将 (i, j) 变成海水
        // 淹没上下左右的陆地
        dfs(grid,i-1,j);
        dfs(grid,i+1,j);
        dfs(grid,i,j-1);
        dfs(grid,i,j+1);
    }
};

7.2 封闭岛屿的数量

靠边的陆地不算作「封闭岛屿」。

class Solution {
public:
    int closedIsland(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        for (int j = 0; j < n; j++) {
            // 把靠上边的岛屿淹掉
            dfs(grid, 0, j);
            // 把靠下边的岛屿淹掉
            dfs(grid, m - 1, j);
        }
        for (int i = 0; i < m; i++) {
            // 把靠左边的岛屿淹掉
            dfs(grid, i, 0);
            // 把靠右边的岛屿淹掉
            dfs(grid, i, n - 1);
        }
        // 遍历 grid,剩下的岛屿都是封闭岛屿
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) {
                    res++;
                    dfs(grid, i, j);
                }
            }
        }
        return res;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(vector<vector<int>>& grid, int i, int j) {
        int m = grid.size(), n = grid[0].size();
        if (i < 0 || j < 0 || i >= m || j >= n) {
            return;
        }
        if (grid[i][j] == 1) {
            // 已经是海水了
            return;
        }
        // 将 (i, j) 变成海水
        grid[i][j] = 1;
        // 淹没上下左右的陆地
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

7.3 最大的岛屿面积。

可以假设 grid 的四个边缘都被 0(代表水)包围着。

class Solution {
public:
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        // 记录岛屿的最大面积
        int res = 0;
        int m = grid.size(), n = grid[0].size();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    // 淹没岛屿,并更新最大岛屿面积
                    res = max(res, dfs(grid, i, j));
                }
            }
        }
        return res;
    }

    // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
    int dfs(vector<vector<int>>& grid, int i, int j) {
        int m = grid.size(), n = grid[0].size();
        if (i < 0 || j < 0 || i >= m || j >= n) {
            // 超出索引边界
            return 0;
        }
        if (grid[i][j] == 0) {
            // 已经是海水了
            return 0;
        }
        // 将 (i, j) 变成海水
        grid[i][j] = 0;

        return dfs(grid, i + 1, j)
                + dfs(grid, i, j + 1)
                + dfs(grid, i - 1, j)
                + dfs(grid, i, j - 1) + 1;
    }
};

7.4 统计子岛屿

在这里插入图片描述

class Solution {
public:
    int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) {
        int m = grid1.size(), n = grid1[0].size();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid1[i][j] == 0 && grid2[i][j] == 1) {
                    // 这个岛屿肯定不是子岛,淹掉
                    dfs(grid2, i, j);
                }
            }
        }
        // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid2[i][j] == 1) {
                    res++;
                    dfs(grid2, i, j);
                }
            }
        }
        return res;
    }

    // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
    void dfs(vector<vector<int>>& grid, int i, int j) {
        int m = grid.size(), n = grid[0].size();
        if (i < 0 || j < 0 || i >= m || j >= n) {
            // 超出索引边界
            return;
        }
        if (grid[i][j] == 0) {
            // 已经是海水了
            return;
        }
        // 将 (i, j) 变成海水
        grid[i][j] = 0;
        // 淹没上下左右的陆地
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

7.5 不同的岛屿数量

在这里插入图片描述
岛屿序列化的结果,只要每次使用 dfs 遍历岛屿的时候生成这串数字进行比较,就可以计算到底有多少个不同的岛屿了


 // 稍微改造 dfs 函数,添加一些函数参数以便记录遍历顺序
void dfs(int[][] grid, int i, int j, string sb, int dir) {
    int m = grid.length, n = grid[0].length;
    if (i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0) {
        return;
    }
    // 前序遍历位置:进入 (i, j)
    grid[i][j] = 0;
    sb.append(dir).append(',');
    
    dfs(grid, i - 1, j, sb, 1); // 上
    dfs(grid, i + 1, j, sb, 2); // 下
    dfs(grid, i, j - 1, sb, 3); // 左
    dfs(grid, i, j + 1, sb, 4); // 右
    
    // 后序遍历位置:离开 (i, j)
    sb.append(-dir).append(',');
}

 // dir 记录方向,dfs 函数递归结束后,sb 记录着整个遍历顺序。有了这个 dfs 函数就好办了,我们可以直接写出最后的解法代码:

int numDistinctIslands(int[][] grid) {
int numDistinctIslands(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    // 记录所有岛屿的序列化结果
    set<string> st;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 1) {
                // 淹掉这个岛屿,同时存储岛屿的序列化结果
                string sb;
                // 初始的方向可以随便写,不影响正确性
                dfs(grid, i, j, sb, 666);
                st.insert(sb);
            }
        }
    }
    // 不相同的岛屿数量
    return st.size();
}

8 括号生成

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

输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]

class Solution {
public:
    vector<string> generateParenthesis(int n) {
        if (n == 0) return {};
        // 记录所有合法的括号组合
        vector<string> res;
        // 回溯过程中的路径
        string track;
        // 可用的左括号和右括号数量初始化为 n
        backtrack(n, n, track, res);
        return res;
    }

    // 可用的左括号数量为 left 个,可用的右括号数量为 rgiht 个
    void backtrack(int left, int right, 
                string& track, vector<string>& res) {
        // 若左括号剩下的多,说明不合法
        if (right < left) return;
        // 数量小于 0 肯定是不合法的
        if (left < 0 || right < 0) return;
        // 当所有括号都恰好用完时,得到一个合法的括号组合
        if (left == 0 && right == 0) {
            res.push_back(track);
            return;
        }
        
        // 尝试放一个左括号
        track.push_back('('); // 选择
        backtrack(left - 1, right, track, res);
        track.pop_back(); // 撤消选择

        // 尝试放一个右括号
        track.push_back(')'); // 选择
        backtrack(left, right - 1, track, res);
        track.pop_back(); // 撤消选择
    }
};

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
宫格数独问题是一个经典的回溯算法问题。该问题的目标是在一个宫格填入数字,使得每一行、每一列和每一个宫格内的数字都不重复。 回溯算法是一种递归算法,它尝试解决问题的所有可能的解,并返回最优解。在宫格数独问题,我们可以从左上角开始,逐行或逐列地填充数字。每当我们填充一个数字,我们需要检查该数字是否已经出现在该行、该列或该宫格。如果是,则需要回溯并尝试其他数字。如果所有数字都尝试过了,但没有找到解决方案,则需要回溯到上一个节点并尝试其他数字。 在实现宫格数独问题的回溯算法,我们可以使用递归函数来遍历所有可能的解决方案。我们可以使用一个二维数组来表示宫格,其0表示需要填充数字的位置。在递归函数,我们可以遍历每一个需要填充数字的位置,并尝试填充数字。如果填充的数字符合要求,则递归进入下一个需要填充数字的位置。如果填充的数字不符合要求,则需要回溯并尝试其他数字。 当递归函数遍历完所有需要填充数字的位置宫格数独问题的解决方案就被找到了。如果在遍历过程没有找到解决方案,则说明该问题无解。 总的来说,宫格数独问题是一个经典的回溯算法问题。通过使用递归函数和回溯技术,我们可以遍历所有可能的解决方案,并找到最优解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ray Song

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值