[Leetcode] Backtracking回溯法(又称DFS,递归)全解

回溯全集

回溯是啥

用爬山来比喻回溯,好比从山脚下找一条爬上山顶的路,起初有好几条道可走,当选择一条道走到某处时,又有几条岔道可供选择,只能选择其中一条道往前走,若能这样子顺利爬上山顶则罢了,否则走到一条绝路上时或者这条路上有一坨屎,我们只好返回到最近的一个路口,重新选择另一条没走过的道往前走。如果该路口的所有路都走不通,只得从该路口继续回返。照此规则走下去,要么找到一条到达山顶的路,要么最终试过所有可能的道,无法到达山顶。
回溯本质上是一种穷举。

还有一些爱混淆的概念:递归,回溯,DFS。这些都是一个事儿的不同方面。以下以回溯统称,因为这个词听上去很文雅。

识别回溯

判断回溯很简单,拿到一个问题,你感觉如果不穷举一下就没法知道答案,那就可以开始回溯了。
一般回溯的问题有三种:

  1. Find a path to success 有没有解
  2. Find all paths to success 求所有解

    • 求所有解的个数
    • 求所有解的具体信息
  3. Find the best path to success 求最优解

理解回溯:给一堆选择, 必须从里面选一个. 选完之后我又有了新的一组选择. This procedure is repeated over and over until you reach a final state. If you made a good sequence of choices, your final state is a goal state; if you didn't, it isn't.

回溯可以抽象为一棵树,我们的目标可以是找这个树有没有good leaf,也可以是问有多少个good leaf,也可以是找这些good leaf都在哪,也可以问哪个good leaf最好,分别对应上面所说回溯的问题分类。
good leaf都在leaf上。good leaf是我们的goal state,leaf node是final state,是解空间的边界。

对于第一类问题(问有没有解),基本都是长着个样子的,理解了它,其他类别迎刃而解:

boolean solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, return true
        else return false
    } else {
        for each child c of n {
            if solve(c) succeeds, return true
        }
        return false
    }
}

请读以下这段话以加深理解:
Notice that the algorithm is expressed as a boolean function. This is essential to understanding the algorithm. If solve(n) is true, that means node n is part of a solution--that is, node n is one of the nodes on a path from the root to some goal node. We say that n is solvable. If solve(n) is false, then there is no path that includes n to any goal node.

还不懂的话请通读全文吧:Backtracking - David Matuszek

关于回溯的三种问题,模板略有不同,
第一种,返回值是true/false。
第二种,求个数,设全局counter,返回值是void;求所有解信息,设result,返回值void。
第三种,设个全局变量best,返回值是void。

第一种:

boolean solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, return true
        else return false
    } else {
        for each child c of n {
            if solve(c) succeeds, return true
        }
        return false
    }
}

第二种:

void solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, count++, return;
        else return
    } else {
        for each child c of n {
            solve(c)
        }
    }
}

第三种:

void solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, update best result, return;
        else return
    } else {
        for each child c of n {
            solve(c)
        }
    }
}

题目

八皇后 N-Queens

问题

1.给个n,问有没有解;
2.给个n,有几种解;(Leetcode N-Queens II)
3.给个n,给出所有解;(Leetcode N-Queens I)

解答
1.有没有解

怎么做:一行一行的放queen,每行尝试n个可能,有一个可达,返回true;都不可达,返回false.

边界条件leaf:放完第n行 或者 该放第n+1行(出界,返回)

目标条件goal:n行放满且isValid,即目标一定在leaf上

helper函数:
boolean solve(int i, int[][] matrix)
在进来的一瞬间,满足property:第i行还没有被放置,前i-1行放置完毕且valid
solve要在给定的matrix上试图给第i行每个位置放queen。

public static boolean solve1(int i, List<Integer> matrix, int n) {
    if (i == n) {
        if (isValid(matrix))
            return true;
        return false;
    } else {
        for (int j = 0; j < n; j++) {
            matrix.add(j);
            if (isValid(matrix)) {    //剪枝
                if (solve1(i + 1, matrix, n)) 
                    return true;
            }
            matrix.remove(matrix.size() - 1);
        }
        return false;
    }
}
2.求解的个数

怎么做:一行一行的放queen,每行尝试n个可能。这回因为要找所有,返回值就没有了意义,用void即可。在搜索时,如果有一个可达,仍要继续尝试;每个子选项都试完了,返回.

边界条件leaf:放完第n行 或者 该放第n+1行(出界,返回)

目标条件goal:n行放满且isValid,即目标一定在leaf上

helper函数:
void solve(int i, int[][] matrix)
在进来的一瞬间,满足property:第i行还没有被放置,前i-1行放置完毕且valid
solve要在给定的matrix上试图给第i行每个位置放queen。
这里为了记录解的个数,设置一个全局变量(static)int是比较efficient的做法。

public static void solve2(int i, List<Integer> matrix, int n) {
    if (i == n) {
        if (isValid(matrix))
            count++;
        return;
    } else {
        for (int j = 0; j < n; j++) {
            matrix.add(j);
            if (isValid(matrix)) {    //剪枝
                solve2(i + 1, matrix, n); 
            }
            matrix.remove(matrix.size() - 1);
        }
    }
}
3.求所有解的具体信息

怎么做:一行一行的放queen,每行尝试n个可能。返回值同样用void即可。在搜索时,如果有一个可达,仍要继续尝试;每个子选项都试完了,返回.

边界条件leaf:放完第n行 或者 该放第n+1行(出界,返回)

目标条件goal:n行放满且isValid,即目标一定在leaf上

helper函数:
void solve(int i, int[][] matrix)
在进来的一瞬间,满足property:第i行还没有被放置,前i-1行放置完毕且valid
solve要在给定的matrix上试图给第i行每个位置放queen。
这里为了记录解的具体情况,设置一个全局变量(static)集合是比较efficient的做法。
当然也可以把结果集合作为参数传来传去。

public static void solve3(int i, List<Integer> matrix, int n) {
    if (i == n) {
        if (isValid(matrix))
            result.add(new ArrayList<Integer>(matrix));
        return;
    } else {
        for (int j = 0; j < n; j++) {
            matrix.add(j);
            if (isValid(matrix)) {    //剪枝
                solve3(i + 1, matrix, n); 
            }
            matrix.remove(matrix.size() - 1);
        }
    }
}
优化

上面的例子用了省空间的方法。
由于每行只能放一个,一共n行的话,用一个大小为n的数组,数组的第i个元素表示第i行放在了第几列上。

Utility(给一个list判断他的最后一行是否和前面冲突):

public static boolean isValid(List<Integer> list){
    int row = list.size() - 1;
    int col = list.get(row);
    for (int i = 0; i <= row - 1; i++) {
        int row1 = i;
        int col1 = list.get(i);
        if (col == col1)
            return false;
        if (row1 - row == col1 - col)
            return false;
        if (row1 - row == col - col1)
            return false;
    }
    return true;
    
}


Leetcode题解
刷题心得,记录
361 声望
98 粉丝
0 条评论
推荐阅读
[Leetcode]Top K问题总结
问题:找第 K 大的数在一个数组里在数据流中找最大的 K 个数在一个数组里在数据流中找出现频率最高的 K 个数在一个数组中在数据流中在文件中这种题适合各种follow up核实问题:是否需要精确的结果?数据是离线的(...

liuqi627阅读 3.7k

刷完15道js版dp题,面试再也不怕了
某个男人 动态规划,而我作为一个致力称为厨师界最会写算法的前端,总得刷上一部分题,有那么一点发现吧,现在我们就来聊聊,菜鸡如我,发现了什么。

hellocoder20281阅读 275

力扣之按身高排序
题目描述给你一个字符串数组 names ,和一个由 互不相同 的正整数组成的数组 heights 。两个数组的长度均为 n 。对于每个下标 i,names[i] 和 heights[i] 表示第 i 个人的名字和身高。请按身高 降序 顺序返回对应...

水冗水孚阅读 959

力扣之回文数(双指针中的对撞指针公式模板)
什么双指针没刷算法之前,一听双指针,感觉很厉害的样子。实际上呢?也的确是一个不错的解题思路方式。在LeetCode上的双指针是一大类题目的解决方式,看一下,发现有近200题是双指针类型的,如下图:由此可见,双...

水冗水孚阅读 845

JavaScript刷LeetCode拿offer-双指针技巧(上)
一、前言  一般情况下,遍历数组(或者字符串)操作,都是采用单指针从前往后或者从后往前依次访问数组(或者字符串)中的元素。  而对于以下情况,只采用单指针处理,则会徒增时间复杂度和空间复杂度:例如:找...

hellocoder20281阅读 375

力扣之反转字符串之原地修改输入数组(双指针方式)
题目描述编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。示例 1: {代码...}...

水冗水孚阅读 793

封面图
代码随想录打卡day2
977,有序数组的平方排序 {代码...} 209,长度最小的子序列方法:滑动窗口 {代码...} 59,螺旋矩阵方法:找规律 {代码...}

赵钱什么李阅读 641

361 声望
98 粉丝
宣传栏