解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
List<List<Integer>> res = new ArrayList<>();
public void dfs(路径,选择列表) {
if满足条件:
res.add(路径)
return;
for (选择 in 选择路径) {
做选择
dfs(路径,选择列表)
撤销选择
}
}
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。
我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。
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();
}
}
通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是很高效,因为对链表使用contains
方法需要 O(N) 的时间复杂度。有更好的方法通过交换元素达到目的,
public static void permutation(char[] chars , int index , List<String> res) {
if (index == chars.length - 1) {
String string = new String(chars);
res.add(string);
}else {
for (int i = index; i < chars.length; i++) {
//去重
if (isSwap(chars, index, i)) {
swap(chars, index, i);
permutation(chars, index+1, res);
//回溯,恢复到初始状态
swap(chars, index, i);
}
}
}
}
public static boolean isSwap(char[] chars, int begin, int end) {
for (int i = begin; i < end; i++) {
if (chars[i] == chars[end]) {
return false;
}
}
return true;
}
public static void swap(char[] chars, int i, int j) {
char temp = chars[j];
chars[j] = chars[i];
chars[i] = temp;
}
public static void solveSudoku(char[][] board) {
//用于记录行上的某个数是否存在
boolean[][] row = new boolean[9][9];
//用于记录列上的某个数是否存在
boolean[][] col = new boolean[9][9];
//用于记录九宫格上的某个数是否存在
boolean[][] block = new boolean[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
int num = board[i][j] - '1';
row[i][num] = true;
col[j][num] = true;
//第几个九宫格 // blockIndex = i / 3 * 3 + j / 3,取整
int blockindex = i/3*3 + j/3;
block[blockindex][num] = true;
}
}
}
dfs(board,row, col, block, 0, 0);
}
//路径:已经填过的位置
//选择列表:三个boolean[][]
//结束条件:遍历一遍
public static boolean dfs(char[][] board,boolean[][] row, boolean[][] col, boolean[][] block, int i,int j) {
//判断,是否是空格 && i,j,是否越界
while (board[i][j] != '.') {
if (++j >= 9) {
//要换到下一行
i++;
j = 0;
}
//结束
if (i >= 9) {
return true;
}
}
for (int num = 0; num < 9; num++) {
//判断该数字是否已经存在
if (!row[i][num] && !col[j][num] && !block[i/3*3+j/3][num]) {
board[i][j] = (char) (num + '1');
row[i][num] = true;
col[j][num] = true;
block[i/3*3+j/3][num] = true;
if (dfs(board, row, col, block, i, j)) {
return true;
}
//回溯
board[i][j] = '.';
row[i][num] = false;
col[j][num] = false;
block[i/3*3+j/3][num] = false;
}
}
return false;
}
static List<List<String>> res = new LinkedList<>();
/**
* 在同一行上,同一列上,同一条斜线上不能有两个
*/
public static List<List<String>> solveNQueens(int n) {
//初始化一个棋盘
char[][] chars = new char[n][n];
for (int i = 0; i < chars.length; i++) {
for (int j = 0; j < chars.length; j++) {
chars[i][j] = '.';
}
}
dfs(chars, 0);
return res;
}
//路径:填过的棋盘
//选择列表:剩余的空位置棋盘
//结束条件:遍历一遍棋盘
public static void dfs(char[][] chars, int row) {
if (row == chars.length) {
List<String> list = print(chars);
res.add(new ArrayList<>(list));
return;
}
int len = chars.length;
for (int i = 0; i < len; i++) {
//检查是否重复
if (isValid(chars, row, i)) {
chars[row][i] = 'Q';
dfs(chars, row+1);
chars[row][i] = '.';
}
}
}
/**
* 注意:这里只判断了上部分的。
* @param chars
* @param row
* @param col
* @return
*/
public static boolean isValid(char[][] chars, int row, int col) {
//检查这一列上是否重复
int len = chars.length;
for (int j = 0; j < len; j++) {
if (chars[j][col] == 'Q') {
return false;
}
}
//判断左上方
for (int i = row-1,j = col-1; i>=0&&j>=0;i--,j--) {
if (chars[i][j] == 'Q') {
return false;
}
}
//判断右上方
for (int i = row-1,j=col+1; i>=0&&j<len;i--,j++) {
if (chars[i][j] == 'Q') {
return false;
}
}
return true;
}
public static List<String> print(char[][] chars) {
List<String> list = new ArrayList<>();
for (int i = 0; i < chars.length; i++) {
list.add(new String(chars[i]));
}
return list;
}
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
LinkedList<Integer> list = new LinkedList<>();
dfs(0, nums, list);
return res;
}
//路径:
public void dfs(int index, int[] nums, LinkedList<Integer> list) {
res.add(new ArrayList<>(list));
for (int i = index; i < nums.length; i++) {
list.addLast(nums[i]);
dfs(i+1, nums, list);
list.removeLast();
}
}
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
int[] nums = new int[n];
for (int i = 0; i < nums.length; i++) {
nums[i] = i+1;
}
List<Integer> list = new ArrayList<>();
dfs(nums, k,0, list);
return res;
}
public void dfs(int[] nums,int k,int start, List<Integer> list) {
if (list.size() == k) {
res.add(new ArrayList<>(list));
return;
}
for (int i = start; i < nums.length; i++) {
list.add(nums[i]);
dfs(nums, k, start+1, list);
list.remove(list.size()-1);
}
}
**子集问题可以利用数学归纳思想,假设已知一个规模较小的问题的结果,思考如何推导出原问题的结果。也可以用回溯算法,要用 **start
参数排除已选择的数字。
**组合问题利用的是回溯思想,结果可以表示成树结构,我们只要套用回溯算法模板即可,关键点在于要用一个 **start
排除已经选择过的数字。
**排列问题是回溯思想,也可以表示成树结构套用算法模板,不同之处在于使用 **contains
方法排除已经选择的数字,
**我的代码:先全排列,然后用栈判断是否是合法的括号。**麻烦
class Solution {
List<String> res = new ArrayList<>();
public List<String> generateParenthesis(int n) {
char[] chars = new char[n*2];
for (int i = 0; i < chars.length; i++) {
if (i % 2 == 0) {
chars[i] = '(';
}else {
chars[i] = ')';
}
}
//List<Character> list = new ArrayList<>();
dfs(chars, 0);
return res;
}
public void dfs(char[] chars,int index) {
if (chars.length == index) {
Stack<Character> stack = new Stack<>();
String str = "";
for (Character c : chars) {
str += c+"";
if (c == '(') {
stack.push(c);
}else {
if (stack.empty()) {
return;
}else {
stack.pop();
}
}
}
res.add(str);
return;
}
for (int i = index; i < chars.length; i++) {
if (isSwap(chars, index, i)) {
swap(chars, index, i);
dfs(chars, index+1);
swap(chars, index, i);
}
}
}
public boolean isSwap(char[] chars, int start,int end) {
for (int i = start; i <end; i++) {
if (chars[end] == chars[i]) {
return false;
}
}
return true;
}
public void swap(char[] chars, int i, int j) {
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
}
第二种解法:
List<String> res = new ArrayList<>();
public List<String> generateParenthesis(int n) {
Stack<Character> stack = new Stack<>();
dfs(n, n, stack);
return res;
}
public void dfs(int left, int right, Stack<Character> stack) {
if (right < left) {
return;
}
if (left < 0 || right <0) {
return;
}
if (left == 0 && right == 0) {
String str = "";
for (Character character : stack) {
str+=character;
}
res.add(str);
return;
}
stack.push('(');
dfs(left-1, right, stack);
stack.pop();
stack.push(')');
dfs(left, right-1, stack);
stack.pop();
}
先把边界与边界相连的区域,O换成-,然后遍历二维数组,把O->X ,- 换回O;
class Solution {
public void solve(char[][] board) {
//上边界
for (int i = 0; i < board[0].length; i++) {
dfs(board, 0, i);
}
//下边界
for (int i = 0; i < board[0].length; i++) {
dfs(board, board.length-1, i);
}
//左边界
for (int i = 0; i < board.length; i++) {
dfs(board, i, 0);
}
//右边界
for (int i = 0; i < board.length; i++) {
dfs(board, i, board[i].length-1);
}
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
if (board[i][j] == 'O') {
board[i][j] = 'X';
}
if (board[i][j] == '-') {
board[i][j] = 'O';
}
}
}
}
public void dfs(char[][] board, int i, int j) {
if(i>=0&&j>=0&&i<board.length&&j<board[0].length&&board[i][j]=='O')
{
board[i][j]='-';
dfs(board,i+1,j);
dfs(board,i-1,j);
dfs(board,i,j+1);
dfs(board,i,j-1);
}
}
}
- dfs + 回溯;
- 使用二维布尔数组记录之前的位置是否已经被访问过,如果已经被访问过的话,则在 dfs 的过程中,直接 return false 即可。也就是说,此路已经不通了;
- 如果当前遍历到的字符不等于 boardi 位置上的字符,那么说明此路也是不通的,因此返回 false;
- 至于递归结束的条件:如果指针 start 能够来到 word 的最后一个字符,那么说明能够在矩阵 board 中找到一条路径,此时返回 true;
- 在遍历到当前 boardi 的时候,首先应将该位置的 visitedi 设置为 true,表明访问过;
- 然后,递归地去 boardi 的上下左右四个方向去找,剩下的路径;
- 在 dfs 的过程当中,如果某条路已经不通了,那么那么需要回溯,也就是将 visitedi 位置的布尔值重新赋值为 fasle;
- 最后,返回 ans 即可。
class Solution {
public boolean exist(char[][] board, String word) {
if (board == null || board.length == 0 || board[0].length == 0) {
return false;
}
char[] chars = word.toCharArray();
boolean[][] visited = new boolean[board.length][board[0].length];
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
// 从 (0, 0) 点开始进行 dfs 操作,不断地去找,
// 如果以 (0, 0) 点没有对应的路径的话,那么就从 (0, 1) 点开始去找
if (dfs(board, chars, visited, i, j, 0)) {
return true;
}
}
}
return false;
}
private boolean dfs(char[][] board, char[] chars, boolean[][] visited, int i, int j, int start) {
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length
|| chars[start] != board[i][j] || visited[i][j]) {
return false;
}
if (start == chars.length - 1) {
return true;
}
visited[i][j] = true;
boolean ans = false;
ans = dfs(board, chars, visited, i + 1, j, start + 1)
|| dfs(board, chars, visited, i - 1, j, start + 1)
|| dfs(board, chars, visited, i, j + 1, start + 1)
|| dfs(board, chars, visited, i, j - 1, start + 1);
visited[i][j] = false;
return ans;
}
}
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构
Set<Node> visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录扩散的步数
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这里判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj())
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
/* 划重点:更新步数在这里 */
step++;
}
}
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
//Set<TreeNode> set = new HashSet<>();
queue.offer(root);
//set.add(root);
int step = 1;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left == null && node.right == null) {
return step;
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
step++;
}
return step;
}
deadends:中的数字是不可以进行转动的。
class Solution {
public int openLock(String[] deadends, String target) {
Set<String> deSet = new HashSet<>();
for (String string : deadends) {
deSet.add(string);
}
Set<String> visited = new HashSet<>();
Queue<String> queue = new LinkedList<>();
//记录,排除重复的值
visited.add("0000");
queue.offer("0000");
int step = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
String pollString = queue.poll();
//被锁的,不可以在这个基础上转动
if (deSet.contains(pollString)) {
continue;
}
//出口
if (pollString.equals(target)) {
return step;
}
for (int j = 0; j < 4; j++) {
String s1 = plusOne(pollString, j);
if (!visited.contains(s1)) {
queue.offer(s1);
visited.add(s1);
}
String s2 = minusOne(pollString, j);
if (!visited.contains(s2)) {
queue.offer(s2);
visited.add(s2);
}
}
}
step++;
}
return -1;
}
public String plusOne(String cur, int pos) {
char[] c = cur.toCharArray();
if (c[pos] == '9') {
c[pos] = '0';
}else {
c[pos] +=1;
}
return new String(c);
}
public String minusOne(String cur, int pos) {
char[] c = cur.toCharArray();
if (c[pos] == '0') {
c[pos] = '9';
}else {
c[pos] -=1;
}
return new String(c);
}
}
对q1进行扩散,扩散的结果保存到temp,然后q1 = q2,q2= temp,下一轮就相当于对q2进行扩散,双向。就是轮换着进行扩散。
public int openLock(String[] deadends, String target) {
Set<String> q1 = new HashSet<>();
Set<String> q2 = new HashSet<>();
Set<String> valid = new HashSet<>();
for (String string : deadends) {
valid.add(string);
}
if (valid.contains("0000")) {
return -1;
}
// 初始化
q1.add("0000");
q2.add(target);
int step = 0;
while (!q1.isEmpty() && !q2.isEmpty()) {
// 记录扩散结果
Set<String> temp = new HashSet<>();
//对q1进行扩散,扩散的结果保存到temp,然后q1 = q2,q2= temp,下一轮就相当于对q2进行扩散,双向。
for (String str : q1) {
if (q2.contains(str)) {
return step;
}
valid.add(str);
for (int i = 0; i < 4; i++) {
String butString = setRotateBut(str, i);
if (!valid.contains(butString)) {
temp.add(butString);
}
String topString = setRotateTop(str, i);
if (!valid.contains(topString)) {
temp.add(topString);
}
}
}
step ++;
q1 = q2;
q2 = temp;
}
return -1;
}
//0-1-9-0旋转
public static String setRotateTop(String str, int j) {
char[] chars= str.toCharArray();
if (chars[j] == '9') {
chars[j] = '0';
}else {
chars[j] = (char)(chars[j] +1);
}
return new String(chars);
}
//0-9-8-0
public static String setRotateBut(String str, int j) {
char[] chars= str.toCharArray();
if (chars[j] == '0') {
chars[j] = '9';
}else {
chars[j] = (char)(chars[j] -1);
}
return new String(chars);
}
我的代码,双向BFS,用字符串进行操作。时间复杂度高。
public static int slidingPuzzle(int[][] board) {
Set<String> q1 = new HashSet<>();
Set<String> q2 = new HashSet<>();
Set<String> valid = new HashSet<>();
q1.add(serializationRe(board));
q2.add("123450");
int step = 0;
while (!q1.isEmpty() && !q2.isEmpty()) {
Set<String> temp = new HashSet<>();
for (String str : q1) {
if (q2.contains(str)) {
return step;
}
valid.add(str);
String[] arrStrings = serialization(str);
for (int i = 0; i < arrStrings.length; i++) {
if (arrStrings[i] != null && !valid.contains(arrStrings[i])) {
temp.add(arrStrings[i]);
}
}
}
step++;
q1 = q2;
q2 = temp;
}
return -1;
}
public static String[] serialization(String str){
int[][] arr = new int[2][3];
int n = 0;
int m = 0;
for (int i = 0; i < str.length(); i++) {
if (i<3) {
if (str.charAt(i) == '0') {
m = i;
}
arr[0][i] = str.charAt(i) - '0';
}else {
if (str.charAt(i) == '0') {
n = 1;
m = i-3;
}
arr[1][i-3] = str.charAt(i) - '0';
}
}
String[] strings = new String[3];
int index = 0;
if (m-1>=0) {
swap(arr[n], m-1, m);
strings[index++] = serializationRe(arr);
swap(arr[n], m-1, m);
}
if (m+1<3) {
swap(arr[n], m+1, m);
strings[index++] = serializationRe(arr);
swap(arr[n], m+1, m);
}
if (n == 0) {
int temp = arr[n][m];
arr[n][m] = arr[n+1][m];
arr[n+1][m] = temp;
strings[index] = serializationRe(arr);
}else {
int temp = arr[n][m];
arr[n][m] = arr[n-1][m];
arr[n-1][m] = temp;
strings[index] = serializationRe(arr);
}
return strings;
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static String serializationRe(int[][] board){
String string = "";
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
string+=board[i][j];
}
}
return string;
}
优化:也是转为字符串处理,但是记录一下每个位置为0时可以做交换的位置,省去了去找位置的步骤,用字符串可以交换,避免多次转换。
class Solution {
public:
int slidingPuzzle(vector<vector<int>>& board) {
int m = 2, n = 3;
string start = "";
string target = "123450";
// 将 2x3 的数组转化成字符串
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
start.push_back(board[i][j] + '0');
}
}
// 记录一维字符串的相邻索引
vector<vector<int>> neighbor = {
{ 1, 3 },
{ 0, 4, 2 },
{ 1, 5 },
{ 0, 4 },
{ 3, 1, 5 },
{ 4, 2 }
};
/******* BFS 算法框架开始 *******/
queue<string> q;
unordered_set<string> visited;
q.push(start);
visited.insert(start);
int step = 0;
while (!q.empty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
string cur = q.front(); q.pop();
// 判断是否达到目标局面
if (target == cur) {
return step;
}
// 找到数字 0 的索引
int idx = 0;
for (; cur[idx] != '0'; idx++);
// 将数字 0 和相邻的数字交换位置
for (int adj : neighbor[idx]) {
string new_board = cur;
swap(new_board[adj], new_board[idx]);
// 防止走回头路
if (!visited.count(new_board)) {
q.push(new_board);
visited.insert(new_board);
}
}
}
step++;
}
return -1;
/******* BFS 算法框架结束 *******/
}
};
**答案:**20312088
public static void test() {
boolean[][] bss = new boolean[10000][10000];
bss[3000][3000] = true;
bss[5020][3011] = true;
bss[3011][3014] = true;
bss[5000][5000] = true;
Queue<String> queue = new LinkedList<>();
queue.add("3000,3000");
queue.add("5020,3011");
queue.add("3011,3014");
queue.add("5000,5000");
for (int i = 0; i < 2020; i++) {
int size = queue.size();
for (int j = 0; j < size; j++) {
String str = queue.poll();
String[] arr = str.split(",");
int x = Integer.parseInt(arr[0]);
int y = Integer.parseInt(arr[1]);
if (!bss[x-1][y]) {
bss[x-1][y] = true;
queue.add((x-1)+","+y);
}
if (!bss[x+1][y]) {
bss[x+1][y] = true;
queue.add((x+1)+","+y);
}
if (!bss[x][y-1]) {
bss[x][y-1] = true;
queue.add(x+","+(y-1));
}
if (!bss[x][y+1]) {
bss[x][y+1] = true;
queue.add(x+","+(y+1));
}
}
}
long count = 0;
for (int i = 0; i < bss.length; i++) {
for (int j = 0; j < bss[i].length; j++) {
if (bss[i][j]) {
count++;
}
}
}
System.out.println(count);
}
public static void test() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
bbs[i][j] = true;
dfs(i, j, 1);
bbs[i][j] = false;
}
}
System.out.println(s);
}
static int s = 0;
static boolean[][] bbs = new boolean[4][4];
public static void dfs(int i, int j,int count) {
System.out.println(count);
if (count==16) {
s++;
return;
}
if (i+1<4 && !bbs[i+1][j]) {
bbs[i+1][j] = true;
dfs(i+1, j,count+1);
bbs[i+1][j] = false;
}
if (j+1<4&& !bbs[i][j+1]) {
bbs[i][j+1] = true;
dfs(i, j+1,count+1);
bbs[i][j+1] = false;
}
if (i-1>=0&& !bbs[i-1][j]) {
bbs[i-1][j] = true;
dfs(i-1, j,count+1);
bbs[i-1][j] = false;
}
if (j-1>=0&& !bbs[i][j-1]) {
bbs[i][j-1] = true;
dfs(i, j-1,count+1);
bbs[i][j-1] = false;
}
}