概念

「回溯」就是 深度优先遍历 状态空间的过程中发现的特有的现象,程序会回到以前访问过的结点。而程序在回到以前访问过的结点的时候,就需要将状态变量恢复成为第一次来到该结点的值。

问「一个问题 所有的 解」一般考虑使用回溯算法。因此回溯算法也叫「暴力搜索」,但不同于最粗暴的多个 for 循环,回溯算法是有方向的遍历。

实现细节

解释递归后面状态重置是怎么回事

当回到上一级的时候,所有的状态变量需要重置为第一次来到该结点的状态,这样继续尝试新的选择才有意义;
在代码层面上,需要在递归结束以后,添加递归之前的操作的逆向操作;

基本类型变量和对象类型变量的不同处理

基本类型变量每一次向下传递的时候的行为是复制,所以无需重置;
对象类型变量在遍历的全程只有一份,因此再回退的时候需要重置;
类比于 Java 中的 方法参数 的传递机制:
基本类型变量在方法传递的过程中的行为是复制,每一次传递复制了参数的值;
对象类型变量在方法传递的过程中复制的是对象地址,对象全程在内存中共享地址。

字符串问题的特殊性

如果使用 + 拼接字符串,每一次拼接产生新的字符串,因此无需重置;
如果使用 StringBuilder 拼接字符串,整个搜索的过程 StringBuilder 对象只有一份,需要状态重置。

为什么不是广度优先遍历

广度优先遍历每一层需要保存所有的「状态」,如果状态空间很大,需要占用很大的内存空间;
深度优先遍历只要有路径可以走,就继续尝试走新的路径,不同状态的差距只有一个操作,而广度优先遍历在不同的层之前,状态差异很大,就不能像深度优先遍历一样,可以使用一份状态变量去遍历所有的状态空间,在合适的时候记录状态的值就能得到一个问题的所有的解。

例题

完成「力扣」第 46 题:全排列(中等);
完成「力扣」第 37 题:数独(困难);
下面是字符串的搜索问题,完成这些问题可以帮助理解回溯算法的实现细节。

完成「力扣」第 22 题:括号生成(中等);
完成「力扣」第 17 题:电话号码的字母组合(中等);
完成「力扣」第 784 题:字母大小写全排列(中等)。

力扣46 全排列

46. 全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3] 输出:

[

[1,2,3],

[1,3,2],

[2,1,3],

[2,3,1],

[3,1,2],

[3,2,1]

]

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/permutations

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  1. class Solution {
  2. public:
  3. vector<vector<int>> permute(vector<int>& nums) {
  4. vector<vector<int>>ret;
  5. if(nums.size() == 0)
  6. return ret;
  7. if(nums.size() == 1)
  8. return vector<vector<int>>{{nums[0]}};
  9. for(int i = 0; i < nums.size(); i++)
  10. {
  11. vector<int>part(nums.begin(), nums.begin() + i);
  12. part.insert(part.end(), nums.begin() + i + 1, nums.end());
  13. vector<vector<int>>tmp_ret = permute(part);
  14. for(auto &vec : tmp_ret)
  15. {
  16. vec.insert(vec.begin(), nums[i]);
  17. ret.push_back(vec);
  18. }
  19. }
  20. return ret;
  21. }
  22. };

力扣351 安卓系统手势解锁

351. 安卓系统手势解锁

我们都知道安卓有个手势解锁的界面,是一个 3 x 3 的点所绘制出来的网格。

给你两个整数,分别为 m 和 n,其中 1 ≤ m ≤ n ≤ 9,那么请你统计一下有多少种解锁手势,是至少需要经过 m 个点,但是最多经过不超过 n 个点的。

先来了解下什么是一个有效的安卓解锁手势:

每一个解锁手势必须至少经过 m 个点、最多经过 n 个点。 解锁手势里不能设置经过重复的点。 假如手势中有两个点是顺序经过的,那么这两个点的手势轨迹之间是绝对不能跨过任何未被经过的点。

经过点的顺序不同则表示为不同的解锁手势。

解释:

| 1 | 2 | 3 |

| 4 | 5 | 6 |

| 7 | 8 | 9 |

无效手势:4 - 1 - 3 - 6

连接点 1 和点 3 时经过了未被连接过的 2 号点。

无效手势:4 - 1 - 9 - 2

连接点 1 和点 9 时经过了未被连接过的 5 号点。

有效手势:2 - 4 - 1 - 3 - 6

连接点 1 和点 3 是有效的,因为虽然它经过了点 2 ,但是点 2 在该手势中之前已经被连过了。

有效手势:6 - 5 - 4 - 1 - 9 - 2

连接点 1 和点 9 是有效的,因为虽然它经过了按键 5 ,但是点 5 在该手势中之前已经被连过了。

示例:

输入: m = 1,n = 1

输出: 9

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/android-unlock-patterns

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

概要
自从安卓使用“手势解锁”系统保护手机以免非授权使用之后,一个常被提及的问题是:这个图案的安全性到底有多高?本文将给出这个答案,以呈现以下算法:计算有多少种合法图案。这个算法面向初学者并介绍两个基本概念:回溯和数组。

题解
方法:回溯
算法

算法适用回溯方法模拟所有可能的 kk 个数字 [1…9][1…9] 的组合,其中 m≤k≤n。在构造递归搜索树的时候,算法会剪枝掉所有不满足规则的方案。

为了计算一个合法手势,算法按照如下步骤进行:

选择一个当前仍然未被使用的数字 ii,这一步通过一个访问数组 usedused 实现,保存所有可用数字。
我们需要记录上一个访问的数字 lastlast。算法需要检查是否满足以下任一条件:
从 lastlast 到 ii 之间是国际象棋中马的移动,或者 lastlast 和 ii 是同一行或列的相邻元素。这种情况下,两个数字之和应当为奇数。
连接 lastlast 和 ii 的中间元素 midmid 已经被访问过,比方说 lastlast 和 ii 选择的是对角线上的两点,那么中间点 mid = 5mid=5 应当已经选过。
lastlast 和 ii 是对角线上的相邻元素。
假设上面有一个条件满足,数字 ii 就变成了合法手势的一部分,算法继续枚举下一个数字,知道整个手势图案被生成。最终计数总方案数。

加入没有任一条件符合,算法当前就不会选择数字 ii,回溯然后继续在未使用的数字中搜索可行的数字。

回溯法 - 图1

  1. public class Solution {
  2. private boolean used[] = new boolean[9];
  3. public int numberOfPatterns(int m, int n) {
  4. int res = 0;
  5. for (int len = m; len <= n; len++) {
  6. res += calcPatterns(-1, len);
  7. for (int i = 0; i < 9; i++) {
  8. used[i] = false;
  9. }
  10. }
  11. return res;
  12. }
  13. private boolean isValid(int index, int last) {
  14. if (used[index])
  15. return false;
  16. // first digit of the pattern
  17. if (last == -1)
  18. return true;
  19. // knight moves or adjacent cells (in a row or in a column)
  20. if ((index + last) % 2 == 1)
  21. return true;
  22. // indexes are at both end of the diagonals for example 0,0, and 8,8
  23. int mid = (index + last)/2;
  24. if (mid == 4)
  25. return used[mid];
  26. // adjacent cells on diagonal - for example 0,0 and 1,0 or 2,0 and //1,1
  27. if ((index%3 != last%3) && (index/3 != last/3)) {
  28. return true;
  29. }
  30. // all other cells which are not adjacent
  31. return used[mid];
  32. }
  33. private int calcPatterns(int last, int len) {
  34. if (len == 0)
  35. return 1;
  36. int sum = 0;
  37. for (int i = 0; i < 9; i++) {
  38. if (isValid(i, last)) {
  39. used[i] = true;
  40. sum += calcPatterns(i, len - 1);
  41. used[i] = false;
  42. }
  43. }
  44. return sum;
  45. }
  46. }
  47. 作者:LeetCode
  48. 链接:https://leetcode-cn.com/problems/android-unlock-patterns/solution/an-zhuo-xi-tong-shou-shi-jie-suo-by-leetcode/
  49. 来源:力扣(LeetCode
  50. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

image.png

力扣39 组合总和

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]

]

提示:

1 <= candidates.length <= 30

1 <= candidates[i] <= 200

candidate 中的每个元素都是独一无二的。

1 <= target <= 500

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/combination-sum

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

回溯法需要注意的是,需要找到回溯终止的条件,在这道题中,已经说明了数组中不存在负数,因此可以把target<0作为回溯终止的条件,如果没有不存在负数这个条件,就需要先对数组进行排序,并判断target是否小于排序后的第一个数,如果小于,则终止回溯。详见第二种解法,另外采用emplace_back可以大大提高程序的效率。同时,这道题也说明了,所有的数字都可以重复使用,所以在下面的解法中传入的start是i而非是i+1.

class Solution {
public:
    void backtrace(vector<int>& candidates, vector<vector<int>>& ret, vector<int>&tmplist, int target, int start)
    {
        if(target < 0)
            return;

        if(target == 0)
        {
            ret.emplace_back(tmplist);
            return;
        }

        for(int i = start; i < candidates.size(); i++)
        {
            tmplist.emplace_back(candidates[i]);
            backtrace(candidates, ret, tmplist, target-candidates[i], i);
            tmplist.pop_back();
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>>ret;
        vector<int>tmplist;
        backtrace(candidates, ret, tmplist, target, 0);
        return ret;
    }
};

如果没有说明数组中全是正数

class Solution {
public:
    void backtrace(vector<int>& candidates, vector<vector<int>>& ret, vector<int>&tmplist, int target, int start)
    {
        if(target < 0)
            return;

        if(target == 0)
        {
            ret.emplace_back(tmplist);
            return;
        }

        for(int i = start; i < candidates.size(); i++)
        {
            tmplist.emplace_back(candidates[i]);
            backtrace(candidates, ret, tmplist, target-candidates[i], i);
            tmplist.pop_back();
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>>ret;
        vector<int>tmplist;
        backtrace(candidates, ret, tmplist, target, 0);
        return ret;
    }
};

力扣47 全排列II

47. 全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]

输出:

[[1,1,2],

[1,2,1],

[2,1,1]]

示例 2:

输入:nums = [1,2,3]

输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

1 <= nums.length <= 8

-10 <= nums[i] <= 10

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/permutations-ii

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

在这里进行了剪枝

class Solution {
public:
    vector<vector<int>> backtrace(vector<int> nums)
    {
        vector<vector<int>>ret;
        if(nums.size() == 0)
            return ret;

        if(nums.size() == 1)
            return vector<vector<int>>{{nums[0]}};

        for(int i = 0; i < nums.size(); i++)
        {
            if(i > 0 && nums[i] == nums[i-1])
                continue;

            vector<int>part(nums.begin(), nums.begin() + i);
            part.insert(part.end(), nums.begin() + i + 1, nums.end());

            vector<vector<int>>partres = backtrace(part);

            for(auto vec : partres)
            {
                vec.insert(vec.begin(), nums[i]);
                ret.emplace_back(vec);
            }
        }

        return ret;
    }

    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        return backtrace(nums);
    }
};

力扣22 括号生成

22. 括号生成

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

示例 1:

输入:n = 3

输出:[“((()))”,”(()())”,”(())()”,”()(())”,”()()()”]

示例 2:

输入:n = 1

输出:[“()”]

提示:

1 <= n <= 8

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/generate-parentheses

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

class Solution {
public:
    void backtrack(vector<string>& ans, string& cur, int open, int close, int n)
    {
        if(cur.size() == n * 2)
        {
            ans.push_back(cur);
            return;
        }

        if(open < n)
        {
            cur.push_back('(');
            backtrack(ans, cur, open + 1, close, n);
            cur.pop_back();
        }

        if(close < open)
        {
            cur.push_back(')');
            backtrack(ans, cur, open, close + 1, n);
            cur.pop_back();
        }
    }
    vector<string> generateParenthesis(int n) {
        vector<string> result;
        string current;
        backtrack(result, current, 0, 0, n);
        return result;
    }
};

力扣51 N皇后

51. N 皇后

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

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

示例 1:

输入:n = 4 输出:[[“.Q..”,”…Q”,”Q…”,”..Q.”],[“..Q.”,”Q…”,”…Q”,”.Q..”]] 解释:如上图所示,4 皇后问题存在两个不同的解法。 示例 2:

输入:n = 1 输出:[[“Q”]]

提示:

1 <= n <= 9 皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/n-queens 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

class Solution {
public:
    void quenePos(int currow, vector<pair<int, int>>& forbiden, int n)
    {
        if(currow >= n)
        {
            vector<string>tmp;
            for(int i = 0; i < n; ++i)
            {
                string str(n, '.');  
                int col = forbiden[i].second;
                str[col] = 'Q';
                tmp.push_back(str);

            }
            ret.push_back(tmp);
            return;
        }

        for(int col = 0; col < n; col++)
        {
            bool place = true;
            for(int tmp = 0; tmp < forbiden.size(); tmp++)
            {
                int forbidenrow = forbiden[tmp].first;
                int forbidencol = forbiden[tmp].second;
                int gaprow = currow - forbidenrow;
                if((col == forbidencol - gaprow) || (col == forbidencol) || (col == forbidencol + gaprow))
                {
                    place = false;
                    break;
                }

            }

            if(!place)
                continue;

            pair<int, int>position{currow, col};
            forbiden.push_back(position);
            quenePos(currow + 1, forbiden, n);
            forbiden.pop_back();
        }

        return;
    }

    vector<vector<string>> solveNQueens(int n) {
        ret.clear();
        vector<pair<int, int>>forbiden;
        quenePos(0, forbiden, n);
        return ret;

    }
private:
    vector<vector<string>> ret;
};