1 模板
- 请输出所有组合/情况
- 写backtrack函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集
- 可以使用Linkedlist removeLast来实现撤销操作。
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
2 例题
2.1 全排列问题
问题
- 输入一个不包含重复数字的数组 nums,返回这些数字的全部排列
List<List<Integer>> res = new LinkedList<>();
/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
// 触发结束条件
if (track.size() == nums.length) {
res.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();
}
}
2.2 组合问题
详情2
问题:
- 输入两个数字 n, k,算法输出 [1…n] 中 k 个数字的所有组合。
vector<vector<int>>res;
vector<vector<int>> combine(int n, int k) {
if (k <= 0 || n <= 0) return res;
vector<int> track;
backtrack(n, k, 1, track);
return res;
}
void backtrack(int n, int k, int start, vector<int>& track) {
// 到达树的底部
if (k == track.size()) {
res.push_back(track);
return;
}
// 注意 i 从 start 开始递增
for (int i = start; i <= n; i++) {
// 做选择
track.push_back(i);
backtrack(n, k, i + 1, track);
// 撤销选择
track.pop_back();
}
}
2.3 N皇后问题
问题
-
给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。
-
PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
vector<vector<string>> res;
/* 输入棋盘边长 n,返回所有合法的放置 */
vector<vector<string>> solveNQueens(int n) {
// '.' 表示空,'Q' 表示皇后,初始化空棋盘。
vector<string> board(n, string(n, '.'));
backtrack(board, 0);
return res;
}
// 路径:board 中小于 row 的那些行都已经成功放置了皇后
// 选择列表:第 row 行的所有列都是放置皇后的选择
// 结束条件:row 超过 board 的最后一行
void backtrack(vector<string>& board, int row) {
// 触发结束条件
if (row == board.size()) {
res.push_back(board);
return;
}
int n = board[row].size();
for (int col = 0; col < n; col++) {
// 排除不合法选择
if (!isValid(board, row, col))
continue;
// 做选择
board[row][col] = 'Q';
// 进入下一行决策
backtrack(board, row + 1);
// 撤销选择
board[row][col] = '.';
}
}
/* 是否可以在 board[row][col] 放置皇后? */
bool isValid(vector<string>& board, int row, int col) {
int n = board.size();
// 检查列是否有皇后互相冲突
for (int i = 0; i < n; i++) {
if (board[i][col] == 'Q')
return false;
}
// 检查右上方是否有皇后互相冲突
for (int i = row - 1, j = col + 1;
i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q')
return false;
}
// 检查左上方是否有皇后互相冲突
for (int i = row - 1, j = col - 1;
i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q')
return false;
}
return true;
}
package com.zknode.回溯;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ti_51 {
public ArrayList<List<String>> res = new ArrayList<>();
int sizeQueue;
public List<List<String>> solveNQueens(int n) {
this.sizeQueue = n;
//初始化棋牌
char[][] ints = new char[sizeQueue][sizeQueue];
for(int i =0; i<sizeQueue ;i++){
for(int j =0; j<sizeQueue ;j++){
ints[i][j] = '.';
}
}
// 开始回溯
trackback(ints,0);
return res;
}
//回溯,传入地图,第几行,至少传入遍历的图,第几行
public void trackback(char[][] board,int row){
//到底了,return
if(sizeQueue == row){
res.add(convert(board));
return;
}
for(int col =0;col<sizeQueue;col++){
if(!isValid(board,row,col)){
continue;
}
board[row][col] = 'Q';
trackback(board,row+1);
board[row][col] ='.';
}
}
private List<String> convert(char[][] board) {
ArrayList<String> strings = new ArrayList<>();
for(char[] i:board){
strings.add(String.valueOf(i));
}
return strings;
}
//检查上方,右上左上是否有皇后
private boolean isValid(char[][] board, int row, int col) {
// 上方
for(int i = 0;i<row;i++){
if(board[i][col] == 'Q'){
return false;
}
}
// 右上
for(int i = row-1,j=col+1;i>=0&&j<sizeQueue;i--,j++){
if(board[i][j] == 'Q'){
return false;
}
}
// 左上
for(int i = row-1,j=col-1;i>=0&&j>=0;i--,j--){
if(board[i][j] == 'Q'){
return false;
}
}
return true;
}
public static void main(String[] args) {
List<List<String>> lists = new ti_51().solveNQueens(4);
// for(List<String> i:lists){
// for(String j:i){
// System.out.println(j);
// }
// }
System.out.println(lists.toString());
}
}
2.4 括号生成
问题
- LeetCode 第 22 题,请你写一个算法,输入是一个正整数n,输出是n对儿括号的所有合法组合
- 可以理解成: 现在有2n个位置,每个位置可以放置字符(或者),组成的所有括号组合中,有多少个是合法的?
vector<string> generateParenthesis(int n) {
if (n == 0) return {};
// 记录所有合法的括号组合
vector<string> res;
// 回溯过程中的路径
string track;
// 可用的左括号和右括号数量初始化为 n
backtrack(n, n, track, res);
return res;
}
// 可用的左括号数量为 left 个,可用的右括号数量为 rgiht 个
void backtrack(int left, int right,
string& track, vector<string>& res) {
// 若左括号剩下的多,说明不合法
if (right < left) return;
// 数量小于 0 肯定是不合法的
if (left < 0 || right < 0) return;
// 当所有括号都恰好用完时,得到一个合法的括号组合
if (left == 0 && right == 0) {
res.push_back(track);
return;
}
// 尝试放一个左括号
track.push_back('('); // 选择
backtrack(left - 1, right, track, res);
track.pop_back(); // 撤消选择
// 尝试放一个右括号
track.push_back(')'); // 选择
backtrack(left, right - 1, track, res);
track.pop_back(); ;// 撤消选择
}
2.5 不重复的所有子集
问题:
- 问题很简单,输入一个不包含重复数字的数组,要求算法输出这些数字的所有子集。
vector<vector<int>> res;
vector<vector<int>> subsets(vector<int>& nums) {
// 记录走过的路径
vector<int> track;
backtrack(nums, 0, track);
return res;
}
void backtrack(vector<int>& nums, int start, vector<int>& track) {
res.push_back(track);
// 注意 i 从 start 开始递增
for (int i = start; i < nums.size(); i++) {
// 做选择
track.push_back(nums[i]);
// 回溯
backtrack(nums, i + 1, track);
// 撤销选择
track.pop_back();
}
}
2.6 重复的所有子集
问题
-
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
-
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
分析
- 回溯暴搜
- 回溯决策:添加该结点,回溯下一层,(集合均有该节点);删除该节点,进入下一个决策结点。(集合不包含该节点)
- 结果集合添加时机:找到一组就加一组。以为是子集不像排列组合,长度固定。
- 决策层:从当前index开始往后找,因为不考虑顺序性,考虑顺序性就要按照全排列做,从头开始扫描
代码
package com.zknode.回溯;
import java.util.*;
//给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
//
//解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
//
//
//子集不要求顺序,可以排序减少工作量
//不能包括重复子集,可以使用set去重
//所有组合情况,可以考虑使用回溯暴力搜索
//双指针扫描也可以
public class ti_90 {
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
Set<List<Integer>> res = new HashSet<>();
ArrayList<Integer> list = new ArrayList<>();
dfs(nums,0,res,list);
return new ArrayList<>(res);
}
//回溯必要三要素,路径集合,当前位置,当前路径选择,总结果可加可不加,可以作为成员变量
public void dfs(int[] nums, int index, Set<List<Integer>> res,List<Integer> list){
//找到一个加一个
res.add(new ArrayList<>(list));
//index开始的可以不加index
for(int i = index ;i<nums.length ;i++){
//加入当前结点
list.add(nums[i]);
dfs(nums,i+1,res,list);
//撤销选择,不考虑当前结点
list.remove(list.size()-1);
}
}
}
2.7 数独问题
详解
问题
boolean backtrack(char[][] board, int r, int c) {
int m = 9, n = 9;
if (c == n) {
// 穷举到最后一列的话就换到下一行重新开始。
return backtrack(board, r + 1, 0);
}
if (r == m) {
// 找到一个可行解,触发 base case
return true;
}
// 就是对每个位置进行穷举
for (int i = r; i < m; i++) {
for (int j = c; j < n; j++) {
if (board[i][j] != '.') {
// 如果有预设数字,不用我们穷举
return backtrack(board, i, j + 1);
}
for (char ch = '1'; ch <= '9'; ch++) {
// 如果遇到不合法的数字,就跳过
if (!isValid(board, i, j, ch))
continue;
board[i][j] = ch;
// 如果找到一个可行解,立即结束
if (backtrack(board, i, j + 1)) {
return true;
}
board[i][j] = '.';
}
// 穷举完 1~9,依然没有找到可行解,此路不通
return false;
}
}
return false;
}
boolean isValid(char[][] board, int r, int c, char n) {
for (int i = 0; i < 9; i++) {
// 判断行是否存在重复
if (board[r][i] == n) return false;
// 判断列是否存在重复
if (board[i][c] == n) return false;
// 判断 3 x 3 方框是否存在重复
if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n)
return false;
}
return true;
}