回溯法是所有搜索算法中最为基本的一种算法,又称为试探法,基本做法是深度优先搜索。
回溯算法框架如下所示,其核心就是 for 循环里面的递归,在递归调⽤之前做出选择,在递归调⽤之后撤销该选择。
result = []
function backtrack(路径, 选择列表) {
if (满足条件) { // 结束条件
result.add(路径);
return;
}
for (选择 in 选择列表) {
做出选择
backtrack(路径,选择列表)
撤销选择
}
}
链接
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
这里用了一个begin防止搜索重复的值
class Solution {
public List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
if (n <= 0 || k <= 0 || n < k)
return list;
backTrack(n, k, 1, new Stack<>());
return list;
}
private void backTrack(int n, int k, int begin, Stack<Integer> track) {
//结束条件
if (track.size() == k) {
list.add(new ArrayList<>(track));
return;
}
for(int i = begin; i <= n; i++){
track.push(i); //做出选择
backTrack(n, k, i + 1, track);
track.pop(); //撤销选择
}
}
}
设想一下从[1,2,3,4,5]中取出3个数,for循环到3的时候,还刚好剩下3个数,但循环到4时,以及不够3个数了,这些分支是没必要执行的。
class Solution {
public List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
if (n <= 0 || k <= 0 || n < k)
return list;
backTrack(n, k, 1, new Stack<>());
return list;
}
private void backTrack(int n, int k, int begin, Stack<Integer> track) {
//结束条件
if (track.size() == k) {
list.add(new ArrayList<>(track));
return;
}
/*
//剪枝:剩下的个数不足时,停止搜索
// i 的极限值满足: n - i + 1 = (k - track.size())。
// n - i + 1 是 [i,n] 的长度。
// k - track.size() 是剩下还要寻找的数的个数。
*/
for(int i = begin; i <= n - (k - track.size()) + 1; i++){
track.push(i); //做出选择
backTrack(n, k, i + 1, track);
track.pop(); //撤销选择
}
}
}
链接
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
class Solution {
public List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
backTrack(nums, 0, new Stack<Integer>());
return list;
}
private void backTrack(int[] nums, int begin, Stack<Integer> track) {
list.add(new ArrayList<>(track));
for (int i = begin; i < nums.length; i++) {
track.push(nums[i]);
backTrack(nums, i + 1, track);
track.pop();
}
}
}
逐个枚举,空集的幂集只有空集,每增加一个元素,让之前幂集中的每个集合,追加这个元素,就是新增的子集。
比如说:1,2,3的子集
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList();
res.add(new ArrayList<Integer>());//添加空白元素
for (Integer num : nums) {
int size = res.size();
for (int i = 0; i < size; i++) {
List<Integer> newSub = new ArrayList<>(res.get(i));
newSub.add(num);
res.add(newSub);
}
}
return res;
}
}
链接
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
class Solution {
public List<List<Integer>> list = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
backTrack(nums, new LinkedList<>());
return list;
}
private void backTrack(int[] nums, LinkedList<Integer> track) {
//结束条件
if (track.size() == nums.length) {
list.add(new LinkedList(track));
return;
}
//选择列表
for (int i = 0; i < nums.length; i++) {
//排除不合理的选择
if (track.contains(nums[i]))
continue;
//做出选择
track.add(nums[i]);
//进入下一层决策树
backTrack(nums, track);
//撤销选择
track.removeLast();
}
}
}
class Solution {
public List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
backTrack(nums, 0);
return list;
}
private void backTrack(int[] nums, int start) {
//结束条件
if (start == nums.length) {
List<Integer> curr = Arrays.stream(nums).boxed().collect(Collectors.toList());//将数组转换为List
list.add(curr);
}
//选择列表
for (int i = start; i < nums.length; i++) {
//交换
int temp = nums[i];
nums[i] = nums[start];
nums[start] = temp;
//进入下一层决策树
backTrack(nums, start + 1);
//交换回来(撤销之前的交换)
temp = nums[i];
nums[i] = nums[start];
nums[start] = temp;
}
}
}
添加链接描述链接
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
在上一题的方法二的基础上,添加了一个HashSet去重。
class Solution {
public List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
backTrack(nums, 0);
return list;
}
private void backTrack(int[] nums, int start) {
//结束条件
if (start == nums.length) {
List<Integer> curr = Arrays.stream(nums).boxed().collect(Collectors.toList());//将数组转换为List
list.add(curr);
}
//选择列表
HashSet<Integer> set = new HashSet<>();
for (int i = start; i < nums.length; i++) {
//去重
if(set.contains(nums[i]))
continue;
set.add(nums[i]);
//交换
int temp = nums[i];
nums[i] = nums[start];
nums[start] = temp;
//进入下一层决策树
backTrack(nums, start + 1);
//交换回来(撤销之前的交换)
temp = nums[i];
nums[i] = nums[start];
nums[start] = temp;
}
}
}
链接
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
class Solution {
private Map<Character, String> phone = new HashMap<>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
private List<String> res = new ArrayList<String>();
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0){
return res;
}
backTrack(new StringBuilder(), 0, digits);
return res;
}
private void backTrack(StringBuilder combination, int index, String digits) {
//结束条件
if (index == digits.length()) {
res.add(combination.toString());
return;
}
//选择列表
String currNumChar = phone.get(digits.charAt(index));//index位置的数字所代表的字符序列
for (int i = 0; i < currNumChar.length(); i++) {
//做出选择
combination.append(currNumChar.charAt(i));
//进入下一层决策树
backTrack(combination, index + 1, digits);
//撤销选择
combination.deleteCharAt(combination.length() - 1);
}
}
}
利用队列先进先出的特点,每次从队列中取出一个元素与新数字所代表的的每一个字符进行拼接并入队。
比如说输入为“23”:
class Solution {
private Map<Character, String> phone = new HashMap<>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<String>();
if (digits == null || digits.length() == 0){
return res;
}
res.add("");
for (int i = 0; i < digits.length(); i++) {
String currNumChar = phone.get(digits.charAt(i));//获取当前数字代表的字符序列
int size = res.size();
for (int j = 0; j < size; j++) {//取出队列中每一个元素,与当前数字代表的每一个字符拼接
String letter = res.remove(0);//取出第一个字符串
//并分别和当前数字所代表的的字符串中的每一个字符进行拼接
for (int k = 0 ; k < currNumChar.length(); k++) {
res.add(letter + currNumChar.charAt(k));
}
}
}
return res;
}
}
链接
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
输出:
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。
提示:
皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一到七步,可进可退。(引用自 百度百科 - 皇后 )
LeetCode提交时间为8ms
class Solution {
private List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
if (n < 1)
return res;
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(n, 0, board);
return res;
}
private void backTrack(int n, int row, List<String> board) {
//结束条件
if (row >= n) {
res.add(new ArrayList<>(board));
return;
}
//选择列表:当前层的每一列
for (int col = 0; col < n; col++) {
//排除不合法的选择
if (!isValid(board, row, col, n)) {
continue;
}
//做选择
StringBuilder sb = new StringBuilder(board.get(row));
sb.setCharAt(col, 'Q');
board.set(row, sb.toString());
//进入下一层决策树
backTrack(n, row + 1, board);
//撤销选择
sb = new StringBuilder(board.get(row));
sb.setCharAt(col, '.');
board.set(row, sb.toString());
}
}
private boolean isValid(List<String> board, int row, int col, int n) {
//检查同一列有没有冲突
for (int i = 0; i < row; i++) {
if (board.get(i).charAt(col) == 'Q')
return false;
}
//检查\列有没有冲突
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board.get(i).charAt(j) == 'Q')
return false;
}
//检查/列有没有冲突
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board.get(i).charAt(j) == 'Q')
return false;
}
return true;
}
}
进行代码优化,仅适用2ms:
public class Solution {
private boolean[] visited;
private boolean[] dia1;
private boolean[] dia2;
private List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
visited = new boolean[n];
//2*n-1个斜对角线
dia1 = new boolean[2*n-1];
dia2 = new boolean[2*n-1];
backTrack(new ArrayList<String>(), 0, n);
return res;
}
private void backTrack(List<String> list, int row, int n){
//结束条件
if (row == n){
res.add(new ArrayList<String>(list));
return;
}
for (int i = 0; i < n; i++){
//排除不合法的选择
if (visited[i] || dia1[row+i] || dia2[row-i+n-1])
continue;
//做选择
char[] charArray = new char[n];
Arrays.fill(charArray, '.');
charArray[i] = 'Q';
String rowString = new String(charArray);
list.add(rowString);
visited[i] = true;
dia1[row+i] = true;// "/",row+col用来标识一条“/”
dia2[row-i+n-1] = true;// "\",row-col用来标识一条“\”,+n-1是为了防止数组越界
//进入下一层决策树
backTrack(list, row + 1, n);
//撤销选择
list.remove(list.size() - 1);
charArray[i] = '.';
visited[i] = false;
dia1[row+i] = false;
dia2[row-i+n-1] = false;
}
}
}
链接
给定一个整数 n,返回 n 皇后不同的解决方案的数量。
class Solution {
private int count = 0;
public int totalNQueens(int n) {
if (n < 1)
return count;
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(n, 0, board);
return count;
}
private void backTrack(int n, int row, List<String> board) {
//结束条件
if (row >= n) {
count++;
return;
}
//选择列表:当前层的每一列
for (int col = 0; col < n; col++) {
//排除不合法的选择
if (!isValid(board, row, col, n)) {
continue;
}
//做选择
StringBuilder sb = new StringBuilder(board.get(row));
sb.setCharAt(col, 'Q');
board.set(row, sb.toString());
//进入下一层决策树
backTrack(n, row + 1, board);
//撤销选择
sb = new StringBuilder(board.get(row));
sb.setCharAt(col, '.');
board.set(row, sb.toString());
}
}
private boolean isValid(List<String> board, int row, int col, int n) {
//检查同一列有没有冲突
for (int i = 0; i < row; i++) {
if (board.get(i).charAt(col) == 'Q')
return false;
}
//检查\列有没有冲突
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board.get(i).charAt(j) == 'Q')
return false;
}
//检查/列有没有冲突
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board.get(i).charAt(j) == 'Q')
return false;
}
return true;
}
}