第8章 递归和回溯法
回溯法是解决很多算法问题的常见思想,甚至可以说是传统人工智能的基础方法。其本质依然是使用递归的方法在树形空间中寻找解。在这一章,我们来具体看一下将递归这种技术使用在非二叉树的结构中,从而认识回溯这一基础算法思想。…
树形问题实质是递归
程序调试方法
目录
-
8-1 树形问题 Letter Combinations of a Phone Number
-
8-2 什么是回溯
-
8-3 排列问题 Permutations
-
8-4 组合问题 Combinations
-
8-5 回溯法解决组合问题的优化
-
8-6 二维平面上的回溯法 Word Search
-
8-7 floodfill算法,一类经典问题 Number of Islands-
-
8-8 回溯法是经典人工智能的基础 N Queens
8-1 树形问题 Letter Combinations of a Phone Number
题目: LeetCode 17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:“23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
import java.util.List;
import java.util.ArrayList;
/// 17. Letter Combinations of a Phone Number
/// https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/
/// 时间复杂度: O(2^len(s))
/// 空间复杂度: O(len(s))
class Solution {
private String letterMap[] = {
" ", //0
"", //1
"abc", //2
"def", //3
"ghi", //4
"jkl", //5
"mno", //6
"pqrs", //7
"tuv", //8
"wxyz" //9
};
private ArrayList<String> res;
public List<String> letterCombinations(String digits) {
res = new ArrayList<String>();
if(digits.equals(""))
return res;
findCombination(digits, 0, "");
return res;
}
// s中保存了此时从digits[0...index-1]翻译得到的一个字母字符串
// 寻找和digits[index]匹配的字母, 获得digits[0...index]翻译得到的解
private void findCombination(String digits, int index, String s){
System.out.println(index + " : " + s);
if(index == digits.length()){
res.add(s);
System.out.println("get " + s + " , return");
return;
}
Character c = digits.charAt(index);
assert c.compareTo('0') >= 0 &&
c.compareTo('9') <= 0 &&
c.compareTo('1') != 0;
String letters = letterMap[c - '0'];
for(int i = 0 ; i < letters.length() ; i ++){
System.out.println("digits[" + index + "] = " + c +
" , use " + letters.charAt(i));
findCombination(digits, index+1, s + letters.charAt(i));
}
System.out.println("digits[" + index + "] = " + c + " complete, return");
return;
}
private static void printList(List<String> list){
for(String s: list)
System.out.println(s);
}
public static void main(String[] args) {
printList((new Solution()).letterCombinations("234"));
}
}
8-2 什么是回溯
课后作业: LeetCode 93、131
8-3 排列问题 Permutations
题目: LeetCode 46. 全排列
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
public class Solution {
private ArrayList<List<Integer>> res;
private boolean[] used;
public List<List<Integer>> permute(int[] nums) {
res = new ArrayList<List<Integer>>();
if(nums == null || nums.length == 0)
return res;
used = new boolean[nums.length];
LinkedList<Integer> p = new LinkedList<Integer>();
generatePermutation(nums, 0, p);
return res;
}
// p中保存了一个有index-1个元素的排列。
// 向这个排列的末尾添加第index个元素, 获得一个有index个元素的排列
private void generatePermutation(int[] nums, int index, LinkedList<Integer> p){
if(index == nums.length){
res.add((LinkedList<Integer>)p.clone());
return;
}
for(int i = 0 ; i < nums.length ; i ++)
if(!used[i]){
used[i] = true;
p.addLast(nums[i]);
generatePermutation(nums, index + 1, p );
p.removeLast();
used[i] = false;
}
return;
}
private static void printList(List<Integer> list){
for(Integer e: list)
System.out.print(e + " ");
System.out.println();
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
List<List<Integer>> res = (new Solution()).permute(nums);
for(List<Integer> list: res)
printList(list);
}
}
课后作业: LeetCode 47
8-4 组合问题 Combinations
题目: LeetCode 77. 组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
/// 77. Combinations
/// https://leetcode.com/problems/combinations/description/
/// 时间复杂度: O(n^k)
/// 空间复杂度: O(k)
public class Solution {
private ArrayList<List<Integer>> res;
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList<List<Integer>>();
if(n <= 0 || k <= 0 || k > n)
return res;
LinkedList<Integer> c = new LinkedList<Integer>();
generateCombinations(n, k, 1, c);
return res;
}
// 求解C(n,k), 当前已经找到的组合存储在c中, 需要从start开始搜索新的元素
private void generateCombinations(int n, int k, int start, LinkedList<Integer> c){
if(c.size() == k){
res.add((List<Integer>)c.clone());
return;
}
for(int i = start ; i <= n ; i ++){
c.addLast(i);
generateCombinations(n, k, i + 1, c);
c.removeLast();
}
return;
}
private static void printList(List<Integer> list){
for(Integer e: list)
System.out.print(e + " ");
System.out.println();
}
public static void main(String[] args) {
List<List<Integer>> res = (new Solution()).combine(4, 2);
for(List<Integer> list: res)
printList(list);
}
}
8-5 回溯法解决组合问题的优化
题目: LeetCode 77. 组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
/// 77. Combinations
/// https://leetcode.com/problems/combinations/description/
/// 时间复杂度: O(n^k)
/// 空间复杂度: O(k)
public class Solution {
private ArrayList<List<Integer>> res;
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList<List<Integer>>();
if(n <= 0 || k <= 0 || k > n)
return res;
LinkedList<Integer> c = new LinkedList<Integer>();
generateCombinations(n, k, 1, c);
return res;
}
// 求解C(n,k), 当前已经找到的组合存储在c中, 需要从start开始搜索新的元素
private void generateCombinations(int n, int k, int start, LinkedList<Integer> c){
if(c.size() == k){
res.add((List<Integer>)c.clone());
return;
}
// 还有k - c.size()个空位, 所以, [i...n] 中至少要有 k - c.size() 个元素
// i最多为 n - (k - c.size()) + 1
for(int i = start ; i <= n - (k - c.size()) + 1 ; i ++){
c.addLast(i);
generateCombinations(n, k, i + 1, c);
c.removeLast();
}
return;
}
private static void printList(List<Integer> list){
for(Integer e: list)
System.out.print(e + " ");
System.out.println();
}
public static void main(String[] args) {
List<List<Integer>> res = (new Solution()).combine(4, 2);
for(List<Integer> list: res)
printList(list);
}
}
课后作业: LeetCode 39、40、216、78、90、401
8-6 二维平面上的回溯法 Word Search
题目: LeetCode 79. 单词搜索
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[
[‘A’,‘B’,‘C’,‘E’],
[‘S’,‘F’,‘C’,‘S’],
[‘A’,‘D’,‘E’,‘E’]
]
给定 word = “ABCCED”, 返回 true.
给定 word = “SEE”, 返回 true.
给定 word = “ABCB”, 返回 false.
/// 79. Word Search
/// Source : https://leetcode.com/problems/word-search/description/
///
/// 回溯法
/// 时间复杂度: O(m*n*m*n)
/// 空间复杂度: O(m*n)
public class Solution {
private int d[][] = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
private int m, n;
private boolean[][] visited;
public boolean exist(char[][] board, String word) {
if(board == null || word == null)
throw new IllegalArgumentException("board or word can not be null!");
m = board.length;
if(m == 0)
throw new IllegalArgumentException("board can not be empty.");
n = board[0].length;
if(n == 0)
throw new IllegalArgumentException("board can not be empty.");
visited = new boolean[m][n];
for(int i = 0 ; i < m ; i ++)
for(int j = 0 ; j < n ; j ++)
if(searchWord(board, word, 0, i, j))
return true;
return false;
}
private boolean inArea( int x , int y ){
return x >= 0 && x < m && y >= 0 && y < n;
}
// 从board[startx][starty]开始, 寻找word[index...word.size())
private boolean searchWord(char[][] board, String word, int index,
int startx, int starty){
//assert(inArea(startx,starty));
if(index == word.length() - 1)
return board[startx][starty] == word.charAt(index);
if(board[startx][starty] == word.charAt(index)){
visited[startx][starty] = true;
// 从startx, starty出发,向四个方向寻
for(int i = 0 ; i < 4 ; i ++){
int newx = startx + d[i][0];
int newy = starty + d[i][1];
if(inArea(newx, newy) && !visited[newx][newy] &&
searchWord(board, word, index + 1, newx, newy))
return true;
}
visited[startx][starty] = false;
}
return false;
}
public static void main(String args[]){
char[][] b1 = { {'A','B','C','E'},
{'S','F','C','S'},
{'A','D','E','E'}};
String words[] = {"ABCCED", "SEE", "ABCB" };
for(int i = 0 ; i < words.length ; i ++)
if((new Solution()).exist(b1, words[i]))
System.out.println("found " + words[i]);
else
System.out.println("can not found " + words[i]);
// ---
char[][] b2 = {{'A'}};
if((new Solution()).exist(b2,"AB"))
System.out.println("found AB");
else
System.out.println("can not found AB");
}
}
8-7 floodfill算法,一类经典问题 Number of Islands-
题目: LeetCode 200. 岛屿的个数
给定一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
/// 200. Number of Islands
/// https://leetcode.com/problems/number-of-islands/description/
/// 时间复杂度: O(n*m)
/// 空间复杂度: O(n*m)
class Solution {
private int d[][] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
private int m, n;
private boolean visited[][];
public int numIslands(char[][] grid) {
if(grid == null || grid.length == 0 || grid[0].length == 0)
return 0;
m = grid.length;
n = grid[0].length;
visited = new boolean[m][n];
int res = 0;
for(int i = 0 ; i < m ; i ++)
for(int j = 0 ; j < n ; j ++)
if(grid[i][j] == '1' && !visited[i][j]){
dfs(grid, i, j);
res ++;
}
return res;
}
// 从grid[x][y]的位置开始,进行floodfill
// 保证(x,y)合法,且grid[x][y]是没有被访问过的陆地
private void dfs(char[][] grid, int x, int y){
//assert(inArea(x,y));
visited[x][y] = true;
for(int i = 0; i < 4; i ++){
int newx = x + d[i][0];
int newy = y + d[i][1];
if(inArea(newx, newy) && !visited[newx][newy] && grid[newx][newy] == '1')
dfs(grid, newx, newy);
}
return;
}
private boolean inArea(int x, int y){
return x >= 0 && x < m && y >= 0 && y < n;
}
public static void main(String[] args) {
char grid1[][] = {
{'1','1','1','1','0'},
{'1','1','0','1','0'},
{'1','1','0','0','0'},
{'0','0','0','0','0'}
};
System.out.println((new Solution()).numIslands(grid1));
// 1
// ---
char grid2[][] = {
{'1','1','0','0','0'},
{'1','1','0','0','0'},
{'0','0','1','0','0'},
{'0','0','0','1','1'}
};
System.out.println((new Solution()).numIslands(grid2));
// 3
}
}
课后作业: LeetCode 130、417
8-8 回溯法是经典人工智能的基础 N Queens
题目: LeetCode 51. N皇后
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例:
输入: 4
输出: [
[".Q…", // 解法 1
“…Q”,
“Q…”,
“…Q.”],
["…Q.", // 解法 2
“Q…”,
“…Q”,
“.Q…”]
]
解释: 4 皇后问题存在两个不同的解法。
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ArrayList;
/// 51. N-Queens
/// https://leetcode.com/problems/n-queens/description/
/// 时间复杂度: O(n^n)
/// 空间复杂度: O(n)
public class Solution {
private boolean[] col;
private boolean[] dia1;
private boolean[] dia2;
private ArrayList<List<String>> res;
public List<List<String>> solveNQueens(int n) {
res = new ArrayList<List<String>>();
col = new boolean[n];
dia1 = new boolean[2 * n - 1];
dia2 = new boolean[2 * n - 1];
LinkedList<Integer> row = new LinkedList<Integer>();
putQueen(n, 0, row);
return res;
}
// 尝试在一个n皇后问题中, 摆放第index行的皇后位置
private void putQueen(int n, int index, LinkedList<Integer> row){
if(index == n){
res.add(generateBoard(n, row));
return;
}
for(int i = 0 ; i < n ; i ++)
// 尝试将第index行的皇后摆放在第i列
if(!col[i] && !dia1[index + i] && !dia2[index - i + n - 1]){
row.addLast(i);
col[i] = true;
dia1[index + i] = true;
dia2[index - i + n - 1] = true;
putQueen(n, index + 1, row);
col[i] = false;
dia1[index + i] = false;
dia2[index - i + n - 1] = false;
row.removeLast();
}
return;
}
private List<String> generateBoard(int n, LinkedList<Integer> row){
assert row.size() == n;
ArrayList<String> board = new ArrayList<String>();
for(int i = 0 ; i < n ; i ++){
char[] charArray = new char[n];
Arrays.fill(charArray, '.');
charArray[row.get(i)] = 'Q';
board.add(new String(charArray));
}
return board;
}
private static void printBoard(List<String> board){
for(String s: board)
System.out.println(s);
System.out.println();
}
public static void main(String[] args) {
int n = 4;
List<List<String>> res = (new Solution()).solveNQueens(n);
for(List<String> board: res)
printBoard(board);
}
}