Leetcode101 回溯法

160 阅读4分钟

回溯法(backtracking)是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。 顾名思义,回溯法的核心是回溯。在搜索到某一节点的时候,如果我们发现目前的节点(及其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态还原。这样的好处是我们可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存状态。在具体的写法上,它与普通的深度优先搜索一样,都有 [修改当前节点状态]→[递归子节点] 的步骤,只是多了回溯的步骤,变成了 [修改当前节点状态]→[递归子节点]→[回改当前节点状态]。

(1)46.Permutations (Medium)

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

题目描述

给定一个无重复数字的整数数组,求其所有的排列方式。

个人思路

太久没做回溯想不起来应该怎么套用了,本来打算用循环暴力破解,想了一下好像不对,需要进行n!次循环,需要递归。不过递归后就能达到回溯的效果,当然在解题的时候需要注意修改状态和回溯状态,以及注意结束条件。这个回溯挺经典的,还是要牢记。

代码展示

class Solution {
public:

    void dfs(vector<vector<int>>& ans,vector<bool>& used,vector<int>& current,vector<int>& nums){
        if(current.size()==nums.size()){
            ans.push_back(current);
            return;
        }
        for(int i=0;i<nums.size();i++){
            if(used[i])
                continue;
            current.push_back(nums[i]);
            used[i]=true;
            dfs(ans,used,current,nums);
            used[i]=false;
            current.pop_back();
        }
    }

    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> ans;
        vector<bool> used(nums.size(),false);
        vector<int> current;
        dfs(ans,used,current,nums);
        return ans;
    }
};

(2)77. Combinations (Medium)

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

题目描述

给定一个整数 n 和一个整数 k,求在 1 到 n 中选取 k 个数字的所有组合方法。

个人思路

有了前一题的经验,这道题目就非常好做了。唯一的区别在于前一题是长度为n,此题是长度为给定的k,因此稍作修改就行。还要注意的一点是不要重复,所以设置一个start作为起始递归值避免重复。

代码展示

class Solution {
public:
    void dfs(vector<vector<int>>& ans,vector<bool>& used,vector<int>& current,vector<int>& nums,int k,int start){
        if(current.size()==k){
            ans.push_back(current);
            return;
        }
        for(int i=start;i<nums.size();i++){
            if(used[i])
                continue;
            current.push_back(nums[i]);
            used[i]=true;
            dfs(ans,used,current,nums,k,i);
            used[i]=false;
            current.pop_back();
        }
    }

    vector<vector<int>> combine(int n, int k) {
        vector<int> nums(n);
        for(int i=0;i<n;i++){
            nums[i]=i+1;
        }
        vector<vector<int>> ans;
        vector<bool> used(nums.size(),false);
        vector<int> current;
        dfs(ans,used,current,nums,k,0);
        return ans;
    }
};

(3)79. Word Search (Medium)

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

题目描述

给定一个字母矩阵,所有的字母都与上下左右四个方向上的字母相连。给定一个字符串,求字符串能不能在字母矩阵中寻找到。

个人思路

和前两题同样的思路,不过不同的是这里变成了二维数组,因此需要注意判断和循环的方式。以及字符串的搜寻不是遍历矩阵了,而是在上下左右四个方向搜寻,和做深搜的时候一样,加个四方数组就行。

不过如果只是简单地循环,可能会出现超时,因为特殊情况下可能每次都要遍历到底。因此这里采用剪枝的方式对代码进行优化。

代码展示

class Solution {
public:
    vector<int> direc = {-1, 0, 1, 0, -1};

    bool dfs(vector<vector<char>>& board, const string& word, vector<vector<bool>>& visited, int current, int a, int b) {
        if (current == word.size()) {
            return true;
        }

        int x, y;
        for (int i = 0; i < 4; i++) {
            x = a + direc[i];
            y = b + direc[i+1];
            if (x >= 0 && x < board.size() && y >= 0 && y < board[0].size()) {
                if (board[x][y] == word[current] && !visited[x][y]) {
                    visited[x][y] = true;
                    if (dfs(board, word, visited, current+1, x, y)) {
                        return true; // 找到了匹配的路径,提前结束
                    }
                    visited[x][y] = false;
                }
            }
        }
        return false; // 当前路径未找到匹配的路径
    }

    bool exist(vector<vector<char>>& board, string word) {
        vector<vector<bool>> visited(board.size(), vector<bool>(board[0].size(), false));
        for (int i = 0; i < board.size(); i++) {
            for (int j = 0; j < board[0].size(); j++) {
                if (board[i][j] == word[0]) {
                    visited[i][j] = true;
                    if (dfs(board, word, visited, 1, i, j)) {
                        return true; // 找到了匹配的路径,提前结束
                    }
                    visited[i][j] = false;
                }
            }
        }
        return false;
    }
};

(4)51.N皇后问题(Hard)

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

题目描述

给定一个大小为 n 的正方形国际象棋棋盘,求有多少种方式可以放置 n 个皇后并使得她们互不攻击,即每一行、列、左斜、右斜最多只有一个皇后。

个人思路

看到题目的时候我内心一喜:这不是考试考过的题目么。根据回溯的思路快速编了一遍代码,但是很遗憾没过,仔细又看了一遍题目:哦要求也不能是一条斜边,大脑直接宕机了。看了一下题解的代码也不是很简洁,还是老师写的比较简单易懂。将老师写的伪代码重新写了一遍,啪的一下,很快啊,就过了。

代码展示

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> solutions;
        vector<int> board(n, -1); // board[i] 表示第 i 行的皇后放在了第几列

        solveNQueensHelper(solutions, board, 0, n);
        return solutions;
    }

    bool isSafe(const vector<int>& board, int row, int col) {
        for (int i = 0; i < row; ++i) {
            if (board[i] == col || abs(row - i) == abs(col - board[i])) {
                return false;
            }
        }
        return true;
    }

    void solveNQueensHelper(vector<vector<string>>& solutions, vector<int>& board, int row, int n) {
        if (row == n) {
            vector<string> solution(n, string(n, '.'));
            for (int i = 0; i < n; ++i) {
                solution[i][board[i]] = 'Q';
            }
            solutions.push_back(solution);
            return;
        }

        for (int col = 0; col < n; ++col) {
            if (isSafe(board, row, col)) {
                board[row] = col;
                solveNQueensHelper(solutions, board, row + 1, n);
                board[row] = -1; // 回溯,尝试其他位置
            }
        }
    }
};

总结

总体来说,今天的回溯还是比较简单的,有了一个样板后做后面的题目都比较顺畅(除了八皇后属实打击了一下自己)。在做题的时候注意几点:首先是记得修改状态和恢复状态,其次是注意结束时间。另外要适时剪枝。