回溯算法

leetcode上回溯算法题目+套路总结

Posted by tianhaoo on March 29, 2020 本文阅读量

概念

回溯算法是一种类似枚举的搜索算法,按照一定的条件搜索解,如果发现不满足就退回一步,重新选择其他分支进行搜索,许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

通常做法是:在包含问题的所有解的解空间树(回溯树)中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。

若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。

而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

特点及与其他算法的对比

  • 回溯算法一般情况下用在求所有的解的情况(或很多解,但也有特殊情况,比如数独问题,要想求出来所有的解时间复杂度太高,所以找到一个解之后就要停止递归)

  • 动态规划一般用在求一个最值的情况

  • dfs一般用在不知道几层循环(dfs并不是一类独立的算法,只是一种搜索的方法,回溯法可以被认为是一个有过剪枝的DFS过程)

用回溯算法解题的步骤

首先画出回溯算法的决策树,把选择标在边上,节点旁边画上当前状态的待选列表和路径,例如

比如要对[1, 2, 3]全排列,画出回溯树
              o
            / |  \
          1/  |2  \3
          /   |    \
         o    o     o
        / \       
      2/   \3    ...
      /     \   
     o       o  
     |       |
    3|       |2
     |       |
     o       o

画好回溯树之后注意观察树的高度、宽度和如何剪枝等

然后套算法模板


result = []
def backtrack(路径, 待选列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 待选列表:
        做选择
        backtrack(路径, 待选列表)
        撤销选择

几个经典题目

求子集

实际上这题也可以用分治法解决,但时间复杂度较高


def func1(lst):
    res = []
    def backtrack(routes, start):  # routes记录路径(即子集),start记录开始位置(间接记录剩下的选择)

        res.append(routes.copy())
        for i in range(start, len(lst)):
            routes.append(lst[i])
            backtrack(routes, i+1)
            routes.pop()
    backtrack([], 0)
    return res

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> route = new ArrayList<>();
        backtrack(res, route, nums, 0);
        return res;
    }

    public void backtrack(List<List<Integer>> res, List<Integer> route, int[] nums, int start){
        List<Integer> routeCopy = new ArrayList<>(route);
        res.add(routeCopy);

        // 常用方法,
        for(int i=start; i<nums.length; i++){  // 所有可能的选择
            // 做选择
            route.add(nums[i]);

            backtrack(res, route, nums, i+1); // 这里传i+1

            // 撤销选择
            route.remove(route.size()-1);
        }
    }
}

求排列 计算A(n, m)

# A(n, m) = n!/(n-m)! = n(n-1)(n-2)...(n-m+1)

def func3(n, m):
    res = []
    def backtrack(routes, choices):
        if len(routes) == m:
            res.append(routes.copy())
            return
        for choice in choices:
            if choice in routes:  # 排除不合法的选择

                continue
            routes.append(choice)
            backtrack(routes, choices)
            routes.pop()
    backtrack([], [i for i in range(1, n+1)])
    return res
class Solution {
    public List<List<Integer>> permute(int[] nums) {
        boolean[] selected = new boolean[nums.length];
        for(int i=0; i<nums.length; i++){
            selected[i] = false;
        }
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> routes = new ArrayList<>();
        backtrack(res, routes, nums, selected);
        return res;
    }

    public static void backtrack(List<List<Integer>> res, List<Integer> routes, int[] choices, boolean[] selected){
        if(routes.size() == choices.length){
            // System.out.println(routes);
            List<Integer> temp = new ArrayList<>();
            // Collections.copy(temp, routes);
            temp.addAll(routes);
            res.add(temp);
        }
        // 不能重复选择元素
        for(int i=0; i<choices.length; i++){ // 所有可能的选择
            if (! selected[i]){
                int current_choice = choices[i];
                // 做选择
                selected[i] = true;
                routes.add(current_choice);
                // 递归调用
                backtrack(res, routes, choices, selected);
                // 撤销选择
                selected[i] = false;
                routes.remove(routes.size()-1);
            }
        }
    }
}

求组合 计算C(n, m)

# C(n, m) = A(n, m)/m!

def func2(n, m):
    res = []
    def backtrack(routes, start):  # start从1开始

        if len(routes) == m:
            res.append(routes.copy())
            return
        for choice in range(start, n+1):
            routes.append(choice)
            backtrack(routes, choice+1)
            routes.pop()
    backtrack([], 1)
    return res
class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        int[] nums = new int[n];
        for(int i=0; i<n; i++){
           nums[i] = i+1;
        }
        // System.out.println(Arrays.toString(nums));
        boolean[] selected = new boolean[n];
        for(int i=0; i<n; i++){
            selected[i] = false;
        }
        backtrack(res, path, nums, selected, 0, k);
        return res;
    }

    public void backtrack(List<List<Integer>> res, List<Integer> path, int[] nums, boolean[] selected, int start, int k){
        if(path.size() == k){
            List<Integer> pathCopy = new ArrayList<>(path);
            res.add(pathCopy);
            // System.out.println(res);
        }
        for(int i=start; i<nums.length; i++){
            path.add(nums[i]);

            backtrack(res, path, nums, selected, i+1, k);

            path.remove(path.size()-1);
        }


    }
}

电话号码的字母组合

leetcode 17

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 示例: 输入:”23” 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]. 说明: 尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

def func(digits):
    if digits == "":
        return []
    lst = ["", "", "abc", 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']

    def backtrack(route, choices):  # 参数为路径和待选列表

        if not choices:   # 如果满足结束条件

            res.append(route)   # 记录或输出

            return

        for choice in choices:   # 遍历所有剩余的选择

            route += choice  # 做出当前选择

            updated_choices = []  # 更新choices,即看看下一轮还剩下什么选择

            if (len(route) < len(digits)):
                for c in lst[int(digits[len(route)])]:
                    updated_choices.append(c)

            backtrack(route, updated_choices)

            route = route[:-1]  # 撤销选择


    res = []
    backtrack("", lst[int(digits[0])])
    return res

class Solution {
    public List<String> letterCombinations(String digits) {
        if(digits.equals("")){
            return new ArrayList<>();
        }

        String[] map = new String[] {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        char[] digitsArray = digits.toCharArray();
        List<String> res = new ArrayList<>();

        backtrack(res, "", digitsArray, 0, map);

        return res;

    }

    public void backtrack(List<String> res, String path, char[] digitsArray, int position, String[] map){
        if(path.length() == digitsArray.length){
            // System.out.println(path);
            res.add(path);
        }
        String choices = "";
        if(position < digitsArray.length){
            choices = map[digitsArray[position] - '0'];
        }

        for(int i=0; i<choices.length(); i++){
            path += choices.charAt(i);

            backtrack(res, path, digitsArray, position+1, map);

            path = path.substring(0, path.length()-1);
        }

    }


}

组合总和

leetcode 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] ]

def func2(candidates, target):
    # 每次考虑过的元素就不作为选择了,这样可以避免重复。(并不会导致有可行解找不到)

    # 每次更新choices,从当前选择的元素往前(包括)

    res = []
    def backtrack(route, choices):
        if sum(route) == target:
            res.append(route.copy())
            return
        if sum(route) > target:
            return

        for c in choices:
            route.append(c)
            temp = []
            flag = False
            for i in choices:  # 由于choices里面没有重复的元素,所以这样可以保证每次从c右边开始选(包括c)

                if i == c:
                    flag = True
                if flag:
                    temp.append(i)
            backtrack(route.copy(), temp)
            route.pop()


    backtrack([], candidates)
    return res


class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();

        backtrack(res, path, 0, candidates, target);

        return res;

    }

    // 类似求所有的子集
    public void backtrack(List<List<Integer>> res, List<Integer> path, int start, int[] candidates, int target){
        // System.out.print("#");
        // System.out.println(path);
        int sum = 0;
        for(int x : path){
            sum += x;
        }
        if(sum>target){
            return;
        }
        // System.out.println(sum);
        if(sum == target){
            // System.out.println(path);
            List<Integer> pathCopy = new ArrayList<>(path);
            res.add(pathCopy);
        }


        for(int i=start; i<candidates.length; i++){
            path.add(candidates[i]);

            backtrack(res, path, i, candidates, target);

            path.remove(path.size()-1);
        }
    }
}

N皇后

leetcode 51

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。 每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。 示例: 输入: 4 输出: [ [“.Q..”, // 解法 1 “…Q”, “Q…”, “..Q.”], [”..Q.”, // 解法 2 “Q…”, “…Q”, “.Q..”] ]

def genBoard(n):
    t = []
    for i in range(n):
        t.append("." * n)
    return t

def isValid(n, r, c, board):
    if "Q" in board[r]:
        return False
    for row in board:
        if row[c] == "Q":
            return False

    p, q = r, c
    while 0<=p<n and 0<=q<n:  # ↘

        if board[p][q] == "Q":
            return False
        p+=1
        q+=1
    p, q = r, c
    while 0<=p<n and 0<=q<n:  # ↗

        if board[p][q] == "Q":
            return False
        p+=1
        q-=1
    p, q = r, c
    while 0<=p<n and 0<=q<n:  # ↙

        if board[p][q] == "Q":
            return False
        p-=1
        q+=1
    p, q = r, c
    while 0<=p<n and 0<=q<n:  # ↖

        if board[p][q] == "Q":
            return False
        p-=1
        q-=1
    return True


def func(n):

    def backtrack(board, r):
        # r表示第r行已经被放置皇后

        if r == n:
            # 如果已经放到了最后一行,那么保存结果,结束递归

            res.append(board.copy())
            return
        else:
            for c in range(0, n):  # 新的一行可以选择放第(0, n)列

                # c代表将要放置再第c列

                if isValid(n, r, c, board):
                    # 放在第c列

                    temp = ""
                    for i in range(len(board[r])):
                        if i != c:
                            temp+= board[r][i]
                        else:
                            temp += "Q"
                    board[r] = temp

                    # 递归调用

                    backtrack(board, r+1)

                    # 撤销选择
                    
                    temp = ""
                    for i in range(len(board[r])):
                        if i != c:
                            temp+= board[r][i]
                        else:
                            temp += "."
                    board[r] = temp
    res = []
    board = genBoard(n)
    backtrack(board, 0)
    return res


class Solution {
    public List<List<String>> solveNQueens(int n) {
        List<List<String>> res = new ArrayList<>();
        List<String> board = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        for(int i=0; i<n; i++){
            sb.append(".");
        }
        for (int i=0; i<n; i++){
            board.add(sb.toString());
        }
        backtrack(res, board, n, 0);

        return res;
    }



    public boolean isValid(List<String> board, int n, int r, int c){
        // 是否有同一行的
        String boardR = board.get(r);
        if(boardR.contains("Q")) return false;

        // 是否有同一列的
        StringBuilder sbBoardC = new StringBuilder();
        for(int i=0; i<n; i++){
            sbBoardC.append(board.get(i).charAt(c));
        }
        if(sbBoardC.toString().contains("Q")) return false;

        int tempR;
        int tempC;
        StringBuilder sb;
        // 右下
        tempR = r;
        tempC = c;
        sb = new StringBuilder();
        while(tempR < n && tempC < n){
            sb.append(board.get(tempR).charAt(tempC));
            tempR ++;
            tempC ++;
        }
        if(sb.toString().contains("Q")) return false;

        // 左上
        tempR = r;
        tempC = c;
        sb = new StringBuilder();
        while(tempR >= 0 && tempC >= 0){
            sb.append(board.get(tempR).charAt(tempC));
            tempR --;
            tempC --;
        }
        if(sb.toString().contains("Q")) return false;

        // 右上
        tempR = r;
        tempC = c;
        sb = new StringBuilder();
        while(tempR >= 0 && tempC < n){
            sb.append(board.get(tempR).charAt(tempC));
            tempR --;
            tempC ++;
        }
        if(sb.toString().contains("Q")) return false;

        // 左下
        tempR = r;
        tempC = c;
        sb = new StringBuilder();
        while(tempR < n && tempC >= 0){
            sb.append(board.get(tempR).charAt(tempC));
            tempR ++;
            tempC --;
        }
        if(sb.toString().contains("Q")) return false;

        return true;
    }

    public void backtrack(List<List<String>> res, List<String> board, int n, int r){
        // System.out.println(r);
        if(r >= n){
            // System.out.println(board);
            List<String> boardCopy = new ArrayList<>(board);
            res.add(boardCopy);
            return;
        }
        // 确定行r之后可以选择任意一列
        for(int c=0; c<n; c++){
            // System.out.print("#");
            // System.out.println(c);
            if(isValid(board, n, r, c)){
                // 在(r, c)位置放置一个棋子
                char[] boardR = board.get(r).toCharArray();
                boardR[c] = 'Q';
                board.set(r, String.valueOf(boardR));
                // System.out.print("%");
                // System.out.println(r);
                backtrack(res, board, n, r+1);

                // 在(r, c)位置取消放置
                boardR = board.get(r).toCharArray();
                boardR[c] = '.';
                board.set(r, String.valueOf(boardR));
            }
        }
    }
}

单词搜索

leetcode 79

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

import time

def func(board, word):

    def backtrack(start_x, start_y, pos, visited):
        # print(board[start_x][start_y], word[pos])

        # start_x和start_y是搜索起点的坐标,pos是在word中当前搜索位置

        if pos == len(word) - 1:  # 递归终止的条件
            # 如果pos指向word中的最后一个元素了(这时前面的肯定都已经满足了,要不然走不到这一步)

            # 如果最后一位再满足的话,就是找到了,否则还是不行

            return board[start_x][start_y] == word[pos]

        elif board[start_x][start_y] == word[pos]:
            # 如果还没搜到最后一位,但是当前的一位符合的话,就朝着接下来所有可能的方向搜索一遍

            visited[start_x][start_y] = True  # 做选择

            choices = []
            i, j = start_x, start_y
            if 0 <= j - 1 < n and not visited[i][j-1]:
                choices.append((i, j - 1))
            if 0 <= i - 1 < m and not visited[i - 1][j]:
                choices.append((i - 1, j))
            if 0 <= j + 1 < n and not visited[i][j+1]:
                choices.append((i, j + 1))
            if 0 <= i + 1 < m and not visited[i+1][j]:
                choices.append((i + 1, j))

            for c in choices:
                if backtrack(c[0], c[1], pos+1, visited):
                    return True
            visited[start_x][start_y] = False  # 撤销选择


        else:
            # 如果当前这一位都不一样的话,直接返回False

            return False


    m, n = len(board), len(board[0])
    visited = [[False for _ in range(n)] for _ in range(m)]
    for i in range(m):
        for j in range(n):
            # 对棋盘上的每一个格子都从头开始搜索,只要找到一个合适的,就返回真

            print("before", visited)
            if backtrack(i, j, 0, visited):
                print("after", visited)
                return True
    # 如果对每一个都找一遍都没有,那就返回假

    return False

class Solution {
    public boolean exist(char[][] board, String word) {

        int m = board.length;
        int n = board[0].length;
        if(m==1 && n==1){
            return String.valueOf(board[m-1][n-1]).equals(word);
        }

        boolean[] exist = {false};
        int[][] directions = {
                {0, 1},
                {1, 0},
                {-1, 0},
                {0, -1},
        };
        for(int i=0; i<m; i++){
            for(int j=0; j<n; j++){
                // System.out.println(i + ", " + j + " " + board[i][j]);
                List<Character> path = new ArrayList<>();
                if (!exist[0]){
                    boolean [][] visited = new boolean[m][n];
                    for(int k=0; k<m; k++){
                        for(int v=0; v<n; v++){
                            visited[k][v] = false;
                        }
                    }
                    backtrack(path, board, word, i, j, 0, visited, directions, exist);
                }
            }
        }
        return exist[0];

    }

    public boolean isValid(char[][] board, int m, int n){
        if(m < 0 || n < 0){
            return false;
        }
        if(m >= board.length){
            return false;
        }
        if(n >= board[0].length){
            return false;
        }
        return true;
    }

    public void backtrack(List<Character> path, char[][] board, String word, int startX, int startY, int pos, boolean[][] visited, int[][] directions, boolean[] exist){
        if(exist[0] == true){
            return;
        }
        // System.out.println(startX + "# " + startY);

        if(board[startX][startY] != word.charAt(pos)){
            exist[0] = false;
            return;
        } else if(pos == word.length()-1){
            exist[0] = true;
            return;
        }


        visited[startX][startY] = true;

        for (int[] direction : directions) {
            int m = startX + direction[0];
            int n = startY + direction[1];

            if (isValid(board, m, n) && !visited[m][n]) {
                backtrack(path, board, word, m, n, pos+1, visited, directions, exist);
            }
        }

        visited[startX][startY] = false;
    }
}