Leetcode——回溯法常考算法整理

  • Preface

Definition

First, let’s see the definition of backtracking given by Wikipedia:

Backtrack is a general algorithm for finding all (or some) solutions to some computational problems, notably constraint satisfaction problems, that incrementally builds candidates to the solutions, and abandons a candidate (“backtracks”) as soon as it determines that the candidate cannot possibly be completed to a valid solution.

The classic textbook example of the use of backtracking is the eight queens puzzle, that asks for all arrangements of eight chess queens on a standard chessboard so that no queen attacks any other. In the common backtracking approach, the partial candidates are arrangements of k queens in the first k rows of the board, all in different rows and columns. Any partial solution that contains two mutually attacking queens can be abandoned.

So, in short, backtracking is notably useful to deal with ‘constraint satisfaction problems’– we must find solutions that satisfy certain constraints–

We can know from the definition that, the name of ‘backtrack’ comes from the way it searches for solutions: ‘it incrementally builds candidates to the solutions, and abandons a candidate (“backtracks”) as soon as it determines that the candidate cannot possibly be completed to a valid solution.’

So how does the algorithm ‘determine whether a candidate cannot possibly be completed to a valid solution’? Clearly, this is when the constraint will play a role, i.e. the algorithm would judge whether the current candidate satisfies the constraint.

Another problem is: how to abandon a candidate (‘backtrack’)? Let’s move on!

Why & When to Use Backtrakcing

Backtrack is a good way to search for solutions that satisfy certain constraints, which makes it fit for corresponding problems.

In wiki, it also says:

Backtracking can be applied only for problems which admit the concept of a “partial candidate solution” and a relatively quick test of whether it can possibly be completed to a valid solution. It is useless, for example, for locating a given value in an unordered table. When it is applicable, however, backtracking is often much faster than brute force enumeration of all complete candidates, since it can eliminate a large number of candidates with a single test.

So according to the wiki, the problem that backtracking can solve must satisfy two conditions:

- partial candidate solution

- quick way to test whether a partial candidate can be completed to a valid solution

  • What is partial candidate solution?

In my understanding, partial candidate solution refers to solution that is ‘complicate’, not just a simple number or character, but a combination/group of them that work together as the solution to the problem.

For example, in classical problem 8-Queens, we’re required to place 8 queens on an 8×8 chessboard such that no two queens attack each other, and we’d like to know how many possible solutions there are. Each solution consists of the position information of 8 queens. So we say position information of 8 queens together forms a possible solution to the problem, and when there are only i (i<8) queens on the chessboard, we say it’s a partial solution candidate.

Therefore, when we face with a problem, we can first decide whether we can solve it in using backtracking by analyzing if its solution could be broken apart into partial solutions. (We will better understand the concept of partial candidate solution through practicing on some Leetcode problems)

Besides, we can know that for problem the backtracking is applicable, it’s much more efficient than brute force enumeration, since it can eliminate a large number of candidates with a single test, and that is the key difference which make backtracking much more advantageous than brute force enumeration for the problem.

How to Use Backtracking

According to wiki’s description of the method:

The backtracking algorithm enumerates a set of partial candidates that, in principle, could be completed in various ways to give all the possible solutions to the given problem. The completion is done incrementally, by a sequence of candidate extension steps.

Conceptually, the partial candidates are represented as the nodes of a tree structure, the potential search tree. Each partial candidate is the parent of the candidates that differ from it by a single extension step; the leaves of the tree are the partial candidates that cannot be extended any further.

The backtracking algorithm traverses this search tree recursively, from the root down, in depth-first order. At each node c, the algorithm checks whether c can be completed to a valid solution. If it cannot, the whole sub-tree rooted at c is skipped (pruned). Otherwise, the algorithm (1) checks whether c itself is a valid solution, and if so reports it to the user; and (2) recursively enumerates all sub-trees of c. The two tests and the children of each node are defined by user-given procedures.

Therefore, the actual search tree that is traversed by the algorithm is only a part of the potential tree. The total cost of the algorithm is the number of nodes of the actual tree times the cost of obtaining and processing each node. This fact should be considered when choosing the potential search tree and implementing the pruning test.

To sum up, all possible solutions can be seen as the set of all nodes in a tree structure, and in backtracking, we would find all valid solutions by searching in DFS way.

For any specific node N, we would first determine whether N can be completed to a valid solution (i.e. whether N has broken the constraint). If it cannot, the whole sub-tree rooted at N is skipped (pruned, i.e. backtracking). Otherwise, the algorithm (1) checks whether c itself is a valid solution, and if so reports it to the user; and (2) recursively enumerates all sub-trees of c. The two tests and the children of each node are defined by user-given procedures.

  • Time Cost

According to wiki:

Therefore, the actual search tree that is traversed by the algorithm is only a part of the potential tree. The total cost of the algorithm is the number of nodes of the actual tree times the cost of obtaining and processing each node. This fact should be considered when choosing the potential search tree and implementing the pruning test.

  • Pseudocode

In order to apply backtracking to a specific class of problems, one must provide the data P for the particular instance of the problem that is to be solved, and six procedural parameters, root, reject, accept, first, next, and output. These procedures should take the instance data P as a parameter and should do the following:

  1. root(P): return the partial candidate at the root of the search tree.
  2. reject(P,c): return true only if the partial candidate c is not worth completing.
  3. accept(P,c): return true if c is a solution of P, and false otherwise.
  4. first(P,c): generate the first extension of candidate c.
  5. next(P,s): generate the next alternative extension of a candidate, after the extension s.
  6. output(P,c): use the solution c of P, as appropriate to the application.

The backtracking algorithm reduces the problem to the call bt(root(P)), where bt is the following recursive procedure:

procedure bt(c)
if reject(P,c) then return
if accept(P,c) then output(P,c)
s ← first(P,c)
while s ≠ Λ do
bt(s)
s ← next(P,s)
  • Details for Writing Code

  • You must ensure that the reject() method rejects candidate correctly.

  • In next() method, if there is no possible way to further extend partial candidate, then remember to backtrack the candidate by one step and then return.

Leetcode Problems

N-Queens

The n-queens puzzle is the problem of placing n queens on an n×n chessboard such that no two queens attack each other.

Given an integer n, return all distinct solutions to the n-queens puzzle.

Each solution contains a distinct board configuration of the n-queens’ placement, where ‘Q’ and ‘.’ both indicate a queen and an empty space respectively.

Example:

Input: 4
Output: [
[".Q..", // Solution 1
"...Q",
"Q...",
"..Q."], ["..Q.", // Solution 2
"Q...",
"...Q",
".Q.."]
]
Explanation: There exist two distinct solutions to the 4-queens puzzle as shown above.
  • My Answer
package medium2;

import java.util.ArrayList;
import java.util.List; /**
* @author Tom Qian
* @email tomqianmaple@outlook.com
* @github https://github.com/bluemapleman
* @date 2018年6月22日
*/
public class NQueens
{
static List<List<String>> ans;
public static void main(String[] args)
{
List<List<String>> ans=solveNQueens(8);
System.out.println("size:"+ans.size());
// for(List<String> list:ans) {
// for(String row:list)
// System.out.println(row);
// System.out.println();
// } } public static List<List<String>> solveNQueens(int n) {
ans=new ArrayList<List<String>>(); String[][] c=new String[n][n];
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
c[i][j]=new String("."); backtrack(c); return ans;
} public static void backtrack(String[][] c){
if(reject(c)) return;
if(accept(c)) {
List<String> list=new ArrayList<>();
for(int i=0;i<c.length;i++) {
String temp="";
for(int j=0;j<c.length;j++) {
temp+=c[i][j];
}
list.add(temp);
}
ans.add(list);
}
c=first(c);
while(c!=null) {
// output(c);
// System.out.println("---------");
backtrack(c);
c=next(c);
} return;
}
//generate the next alternative extension of a candidate, after the extension s.
public static String[][] next(String[][] c){
int lastRow=-1;
for(int i=0;i<c.length;i++) {
boolean hasQueen=false;
for(int j=0;j<c.length;j++) {
if(c[i][j].equals("Q"))
hasQueen=true;
}
if(!hasQueen) {
lastRow=i-1;
break;
}
} if(lastRow==-1)
lastRow=c.length-1; for(int j=0;j<c.length-1;j++) {
if(c[lastRow][j].equals("Q")) {
c[lastRow][j]=".";
c[lastRow][j+1]="Q";
return c;
}
}
c[lastRow][c.length-1]=".";
return null;
}
//generate the first extension of candidate c.
public static String[][] first(String[][] c){
for(int i=0;i<c.length;i++) {
boolean hasQueen=false;
for(int j=0;j<c.length;j++) {
if(c[i][j].equals("Q"))
hasQueen=true;
}
if(!hasQueen) {
c[i][0]="Q";
return c;
}
}
return null;
} public static boolean accept(String[][] c) {
int count=0;
int n=c.length;
for(int i=0;i<n;i++) {
for(int j=0;j<n;j++) {
if(c[i][j].equals("Q"))
count++;
}
}
if(count==c.length)
return true;
return false;
} public static boolean reject(String[][] c) {
int n=c.length;
// check by row
for(int i=0;i<n;i++) {
boolean hasQueen=false;
for(int j=0;j<n;j++) {
if(c[i][j].equals("Q")) {
if(hasQueen)
return true;
else
hasQueen=true; }
}
}
// check by column
for(int j=0;j<n;j++) {
boolean hasQueen=false;
for(int i=0;i<n;i++) {
if(c[i][j].equals("Q")) {
if(hasQueen)
return true;
else
hasQueen=true; }
}
}
// check by diagram
// left bottom to right top
for(int i=0;i<n;i++) {
boolean hasQueen=false;
for(int x=0;x<=i;x++) {
if(c[i-x][x].equals("Q")) {
if(hasQueen)
return true;
else
hasQueen=true; }
}
}
for(int j=n-1;j>0;j--) {
boolean hasQueen=false;
for(int x=0;x<n-j;x++) {
if(c[n-1-x][j+x].equals("Q")) {
if(hasQueen)
return true;
else
hasQueen=true;
}
}
}
// left top to right bottom
for(int i=n-1;i>=0;i--) {
boolean hasQueen=false;
for(int x=0;x<n-i;x++) {
if(c[i+x][x].equals("Q")) {
if(hasQueen)
return true;
else
hasQueen=true; }
}
}
for(int j=n-1;j>=0;j--) {
boolean hasQueen=false;
for(int x=0;x<n-j;x++) {
if(c[x][j+x].equals("Q")) {
if(hasQueen)
return true;
else
hasQueen=true;
}
}
} return false;
} public static void output(String[][] c) {
for(int i=0;i<c.length;i++) {
for(int j=0;j<c.length;j++)
System.out.print(c[i][j]+"-");
System.out.println();
}
}
}

PS: Clearly, I write the code according to pseudocode representing typical backtracking thought, and I think it’s a very convenient and efficient way to finish writing backtracking algorithmwe.

Permutations II

LC47

Given a collection of numbers that might contain duplicates, return all possible unique permutations.

Example:

Input: [1,1,2]
Output:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
  • My Answer
package medium2;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; /**
* @author Tom Qian
* @email tomqianmaple@outlook.com
* @github https://github.com/bluemapleman
* @date 2018年7月3日
*/
public class PermutationsII
{
// First, review the typical pseudocode of backtracking algorithm again:
/*
backtrack(c){
if(reject(c)) return;
if(accept(c)) add c to solution set;
// first extension of c.
s=first(c);
while(s!=null) {
backtrack(s);
// switch to next possible extension of c
s=next(s);
}
}
*/ // The solution is the all possible permutations of given numbers.
// So to use backtracking, we must first define what is partial solution candidate:
// the list of part of numbers from given integer array
List<List<Integer>> ans;
public List<List<Integer>> permuteUnique(int[] nums) {
ans=new ArrayList<List<Integer>>();
List<Integer> numList=new ArrayList<>();
Arrays.sort(nums);
for(Integer i:nums)
numList.add(i); backtrack(new ArrayList<>(), numList);
return ans;
} public void backtrack(List<Integer> c,List<Integer> numList) {
if(reject(c,numList)) return;
if(accept(c, numList)) {
List<Integer> list=new ArrayList<>();
for(int i=0;i<c.size();i++) {
list.add(c.get(i));
}
ans.add(list);
}
List<Integer> s=first(c, numList);
while(s!=null) {
// System.out.println(s);
// System.out.println(numList);
// System.out.println();
backtrack(s, numList);
s=next(s, numList);
}
} public boolean reject(List<Integer> c,List<Integer> numList) {
return false;
} public boolean accept(List<Integer> c,List<Integer> numList) {
if(numList.isEmpty()) {
return true;
}else
return false; } public List<Integer> first(List<Integer> c,List<Integer> numList){
if(!numList.isEmpty()){
c.add(numList.remove(0));
return c;
}
else
return null;
} public List<Integer> next(List<Integer> c,List<Integer> numList){
boolean nextFlag=false;
for(int i=0;i<numList.size();i++) {
int ele=numList.get(i);
int lastEle=c.get(c.size()-1);
if(ele>lastEle) {
numList.remove(i);
numList.add(i,lastEle);
c.remove(c.size()-1);
c.add(ele);
nextFlag=true;
break;
}
}
if(nextFlag) {
return c;
}else {
numList.add(c.remove(c.size()-1));
Collections.sort(numList);
return null;
}
}
}

Combinations

Given two integers n and k, return all possible combinations of k numbers out of 1 … n.

Example:

Input: n = 4, k = 2
Output:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
  • My Answer
package medium2;

import java.util.ArrayList;
import java.util.List; /**
* @author Tom Qian
* @email tomqianmaple@outlook.com
* @github https://github.com/bluemapleman
* @date 2018年7月5日
*/
public class Combinations
{
List<List<Integer>> ans;
int n,k;
public List<List<Integer>> combine(int n, int k) {
ans=new ArrayList<>();
this.n=n;
this.k=k;
List<Integer> numList=new ArrayList<>();
for(int i=1;i<=n;i++) {
numList.add(i);
}
backtrack(new ArrayList<>(), numList);
return ans;
} public void backtrack(List<Integer> c,List<Integer> numList) {
if(reject(c)) return;
if(accept(c)) {
List<Integer> list=new ArrayList<>();
for(Integer ele:c)
list.add(ele);
ans.add(list);
}
List<Integer> s=first(c,numList);
while(s!=null) {
backtrack(s, numList);
s=next(s,numList);
}
} public boolean reject(List<Integer> c) {
for(int i=0;i<c.size()-1;i++) {
if(c.get(i)>c.get(i+1))
return true;
}
return false;
} public boolean accept(List<Integer> c) {
if(c.size()==k)
return true;
else
return false;
} public List<Integer> first(List<Integer> c,List<Integer> numList){
if(c.size()<k) {
if(c.isEmpty()) {
c.add(numList.remove(0));
return c;
}
else {
int lastEle=c.get(c.size()-1);
if(n>lastEle) {
c.add(lastEle+1);
return c;
}
else
return null;
}
}else
return null;
} public List<Integer> next(List<Integer> c,List<Integer> numList){
int lastEle=c.get(c.size()-1);
if(n>lastEle) {
c.remove(c.size()-1);
c.add(lastEle+1);
return c;
}else {
c.remove(c.size()-1);
return null;
}
}
}

Sudoku Solver

LC37

Write a program to solve a Sudoku puzzle by filling the empty cells.

A sudoku solution must satisfy all of the following rules:

  • Each of the digits 1-9 must occur exactly once in each row.
  • Each of the digits 1-9 must occur exactly once in each column.
  • Each of the the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid.

Empty cells are indicated by the character ‘.’.

A sudoku puzzle…

…and its solution numbers marked in red.

Note:

  • The given board contain only digits 1-9 and the character ‘.’.
  • You may assume that the given Sudoku puzzle will have a single unique solution.
  • The given board size is always 9x9.

  • My Answer

class Solution {
// This solution from: https://leetcode.com/problems/sudoku-solver/discuss/15752/Straight-Forward-Java-Solution-Using-Backtracking
// Backtrack int tempX,tempY;
boolean[][] cellsToBeFilled;
public void solveSudoku(char[][] board) {
if(board == null || board.length == 0)
return;
cellsToBeFilled=new boolean[board.length][board[0].length];
for(int i=0;i<board.length;i++) {
for(int j=0;j<board[0].length;j++) {
if(board[i][j]=='.') {
cellsToBeFilled[i][j]=true;
}
}
}
backtrack(board);
} boolean findAnswerFlag=false;
public void backtrack(char[][] board) {
if(reject(board)) return;
if(accept(board)) {
findAnswerFlag=true;
return;
}
char[][] s=first(board);
while(s!=null) {
// outputMatrix(board);
backtrack(s);
if(findAnswerFlag)
return;
s=next(s);
}
} public boolean reject(char[][] board) {
int n=board.length;
for(int i=0;i<n;i++) {
Set<Character> set=new HashSet<>();
for(int j=0;j<n;j++) {
if(board[i][j]!='.')
if(set.contains(board[i][j])) {
return true;
}else
set.add(board[i][j]);
}
} for(int j=0;j<n;j++) {
Set<Character> set=new HashSet<>();
for(int i=0;i<n;i++) {
if(board[i][j]!='.')
if(set.contains(board[i][j])) {
return true;
}else
set.add(board[i][j]);
}
} for(int x=0;x<2;x++) {
for(int y=0;y<2;y++) {
int startRow=x*3,startCol=y*3;
Set<Character> set=new HashSet<>();
for(int i=startRow;i<=startRow+2;i++) {
for(int j=startCol;j<=startCol+2;j++) {
if(board[i][j]!='.')
if(set.contains(board[i][j])) {
return true;
}else
set.add(board[i][j]);
}
}
}
} return false;
} public boolean accept(char[][] board) {
int n=board.length;
for(int i=0;i<n;i++) {
for(int j=0;j<n;j++) {
if(board[i][j]=='.') {
return false;
}
}
}
return true;
} public char[][] first(char[][] board) {
int n=board.length;
boolean hasEmptyCellFlag=false;
for(int i=0;i<n;i++) {
for(int j=0;j<n;j++) {
if(board[i][j]=='.') {
board[i][j]='1';
tempX=i;tempY=j;
hasEmptyCellFlag=true;
break;
}
}
if(hasEmptyCellFlag)
break;
}
if(hasEmptyCellFlag)
return board;
else {
return null;
}
} public char[][] next(char[][] board) {
int number=Character.digit(board[tempX][tempY], 10);
if(number<9) {
board[tempX][tempY]=Character.forDigit(number+1, 10);
return board;
}
else {
board[tempX][tempY]='.';
boolean findLastToBeFilledCellFlag=false;
for(int j=tempY-1;j>=0;j--) {
if(cellsToBeFilled[tempX][j]==true) {
tempY=j;
findLastToBeFilledCellFlag=true;
break;
}
}
if(!findLastToBeFilledCellFlag) {
boolean breakFlag=false;
for(int i=tempX-1;i>=0;i--) {
for(int j=board.length-1;j>=0;j--) {
if(cellsToBeFilled[i][j]==true) {
tempX=i;
tempY=j;
breakFlag=true;
break;
}
}
if(breakFlag)
break;
}
} return null;
} } public void outputMatrix(char[][] board) {
for(int i=0;i<board.length;i++) {
for(int j=0;j<board.length;j++) {
System.out.print(board[i][j]+" ");
}
System.out.println();
}
System.out.println();
}
}

Leetcode——回溯法常考算法整理的更多相关文章

  1. Leetcode——二叉树常考算法整理

    二叉树常考算法整理 希望通过写下来自己学习历程的方式帮助自己加深对知识的理解,也帮助其他人更好地学习,少走弯路.也欢迎大家来给我的Github的Leetcode算法项目点star呀~~ 二叉树常考算法 ...

  2. BFS与DFS常考算法整理

    BFS与DFS常考算法整理 Preface BFS(Breath-First Search,广度优先搜索)与DFS(Depth-First Search,深度优先搜索)是两种针对树与图数据结构的遍历或 ...

  3. JS-常考算法题解析

    常考算法题解析 这一章节依托于上一章节的内容,毕竟了解了数据结构我们才能写出更好的算法. 对于大部分公司的面试来说,排序的内容已经足以应付了,由此为了更好的符合大众需求,排序的内容是最多的.当然如果你 ...

  4. (排序算法整理)NEFU 30/32

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/caihongshijie6/article/details/26165093        事实上, ...

  5. C语言入门100题,考算法的居多

    入门题,考算法的居多,共同学习! 1. 编程,统计在所输入的50个实数中有多少个正数.多少个负数.多少个零. 2. 编程,计算并输出方程X2+Y2=1989的所有整数解. 3. 编程,输入一个10进制 ...

  6. LeetCode解题记录(贪心算法)(二)

    1. 前言 由于后面还有很多题型要写,贪心算法目前可能就到此为止了,上一篇博客的地址为 LeetCode解题记录(贪心算法)(一) 下面正式开始我们的刷题之旅 2. 贪心 763. 划分字母区间(中等 ...

  7. 排序算法整理C++(初赛)

    排序算法整理 常见考点 将一个乱掉的字符串排回有序(以交换为基本操作)的最少操作,就是冒泡排序. 排序算法的稳定性 排序算法的时间复杂度 排序算法的稳定性 稳定性是指排序前两个元素a1 = a2,a1 ...

  8. leetcode回溯算法--基础难度

    都是直接dfs,算是巩固一下 电话号码的字母组合 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合. 给出数字到字母的映射如下(与电话按键相同).注意 1 不对应任何字母. 思路 一直 ...

  9. leetcode 算法整理

    一 字符串中的最大回文串(第5题) Given a string s, find the longest palindromic substring in s. You may assume that ...

随机推荐

  1. 前端开发个人小结 · Retrospection的博客

    序 2018年转眼来到了最后一个月,算下来我进入前端之门也有一年了,虽然下半年由于忙于筹备毕业论文的相关事项,前端这一块有所放下,但是想想还是给自己这一年的学习做一个总结. 现代化软件开发确实是一个复 ...

  2. 名企6年Java程序员的工作感悟,送给迷茫的你

    程序员从开始选择到坚持下去,工作了六年对一个程序员意味什么?在职位上:高级开发工程师?架构师?技术经理?or ... ?在能力上:各种编码无压力?核心代码无压力?平台架构无压力? or ... fuc ...

  3. Dart 运行速度测评与比较

    引言 Dart 是一门优秀的跨平台语言,尽管生态方面略有欠缺,但无疑作为一门编程语言来说,Dart 是很优美,很健壮的,同时也引入了一些先进的编程范式,值得去学习. 测试内容 现在,我们就来测评一下D ...

  4. 树莓派3B安装OpenWrt打造超级路由器

    网上有很多树莓派安装OpenWrt的教程,我这里写一下个人安装体验以及踩过的坑

  5. 用nodejs+express搭建前端测试服务端

    平时开发前端应用,如果没有现成的后端接口调试,又要保证前端进度,该怎么办呢,当然办法还是很多的,很多大牛都分享过很多经验,我也来说说我常用的方法. 请求本地数据文件 把本地数据放到程序指定目录,发起h ...

  6. JZOJ 1349. 最大公约数 (Standard IO)

    1349. 最大公约数 (Standard IO) Time Limits: 1000 ms Memory Limits: 65536 KB Description 小菜的妹妹小诗就要读小学了!正所谓 ...

  7. ReentrantLock 源码分析以及 AQS (一)

    前言 JDK1.5 之后发布了JUC(java.util.concurrent),用于解决多线程并发问题.AQS 是一个特别重要的同步框架,很多同步类都借助于 AQS 实现了对线程同步状态的管理. A ...

  8. 简单的猜数字小游戏--Python

    猜数字小游戏: #coding=utf-8 import random   answer =random.randint(1,100) #生成随机数 n=int (input("Please ...

  9. RAC修改VIP地址

    目录 当前环境 1.通过[srvctl config]确认当前VIP地址. 2.关闭dbconsole[对应的em] 3.关闭数据库实例 4.关闭asm实例 5.关闭结点服务 6.修改两个节点的/et ...

  10. ECMAScript进化史(1):​话说Web脚本语言王者JavaScript的加冕历史

    互联网起火-Web时代的来临 在行文之前,反手就安利一下<浏览器史话中chrome霸主地位的奠定与国产浏览器的割据混战>. 浏览器始祖NCSA Mosaic在1993年1月发布(于1992 ...