回溯算法
解决一个回溯问题,实际上就是决策树的遍历问题。 下图就是一个决策树。因为在每一个节点上你都要做决策,向左或者向右。接下来解释几个名词。【2】是【路径】,【1,3】就是【选择列表】,表示当前可以做出的选择,【结束条件】就是遍历到树的底层。
1.回溯算法框架
result=[]
public void backtrace(路径,选择列表){
if (满足结束条件){
result.add(路径);
return;
}
for 选择 in 选择列表{
做选择;
backtrace(路径,选择列表)
撤销选择;
}
}
2.经典例题
1.八皇后问题
问题描述:
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
代码
import java.util.ArrayList;
import java.util.List;
public class n皇后问题 {
public static void main(String[] args) {
List<List<String>> lists = solveNQueens(4);
System.out.println(lists.toString());
}
public static List<List<String>> solveNQueens(int n) {
char[][] chars = new char[n][n];
List<List<String>> lists = new ArrayList<>();
//初始化
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
chars[i][j]='.';
}
}
res(lists,chars,0);
return lists;
}
private static void res(List<List<String>> lists, char[][] chars, int row) {
if (row==chars.length) {
lists.add(constract(chars));
return;
}
for (int col = 0; col <chars[0].length ; col++) {
if (vaild(chars,row,col)) {
chars[row][col] = 'Q';
res(lists,chars,row+1);
chars[row][col]='.';
}
}
}
//将字符二维数组转化为List类型
private static List<String> constract(char[][] chars) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < chars.length; i++) {
list.add(new String(chars[i]));
}
return list;
}
private static boolean vaild(char[][] chars, int row, int col) {
//只用判断当前即可
//判断列是否满足条件
for (int i = 0; i< row; i++) {
if (chars[i][col]=='Q')
return false;
}
//判断右上方是否满足条件
for(int i=row-1,j=col+1; i>=0 && j<chars.length;i--,j++){
if (chars[i][j]=='Q')
return false;
}
//判断左上方是否满足条件
for (int i=row-1,j=col-1;i>=0&&j>=0;i--,j--){
if (chars[i][j]=='Q')
return false;
}
return true;
}
}
2.活字印刷(子集问题)
题目描述 : 找出所有的组合。
输入:"AAB"
输出:8
解释:可能的序列为 "A", "B", "AA", "AB", "BA", "AAB", "ABA", "BAA"。
代码
public int numTilePossibilities(String tiles) {
boolean[] booleans = new boolean[tiles.length()];
Set<String> set = new HashSet<>();
dfs(booleans,set,tiles,new StringBuilder());
return set.size()-1;
}
private void dfs(boolean[] booleans, Set<String> set, String tiles, StringBuilder builder) {
set.add(builder.toString());
for (int i = 0; i < tiles.length(); i++) {
if (!booleans[i]){
booleans[i]=true;
builder.append(tiles.charAt(i));
dfs(booleans,set,tiles,builder);
builder.deleteCharAt(builder.length()-1);
booleans[i]=false;
}
}
}
3.子集
题目描述: 输入一个不包含重复数字的数组,输出这些数字的所有子集。
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
代码
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
if (nums==null){
return lists;
}
List<Integer> list = new ArrayList<>();
backtrace(nums,list,0);
return lists;
}
private void backtrace(int[] nums,List<Integer> list, int i) {
lists.add(new ArrayList<>(list));
for (int j = i; j <nums.length; j++) {
list.add(nums[j]);
backtrace(nums,list,j+1);
list.remove(list.size()-1);
}
}
- 每个数字使用完之后,就不会再被使用。所以下次回溯就从 j+1 开始。
4.组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
- 这是最常见的回溯问题,k限制了树的高度,n限制了树的宽度。
代码
public List<List<Integer>> combine(int n, int k){
List<List<Integer>> lists = new ArrayList<>();
List<Integer> list = new ArrayList<>();
backtrace(lists,list,k,1,n);
return lists;
}
private static void backtrace(List<List<Integer>> lists, List<Integer> list, int k, int i,int n) {
if (list.size()==k){
lists.add(new ArrayList<>(list));
}
for (int j = i; j <=n; j++) {
list.add(j);
//j+1 排除已选择的数
backtrace(lists,list,k,j+1,n);
list.remove(list.size()-1);
}
}
5.排列
如果数组中包含重复元素,一般使用 boolean数组 来判断数字是否使用过。
boolean[] m = new boolean[len];
if (i>0 && nums[i]==nums[i-1] && !m[i-1])
continue;
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
代码
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
ArrayList<Integer> list = new ArrayList<>();
bfs(nums,list);
return lists;
}
private void bfs(int[] nums,ArrayList<Integer> list) {
if (list.size()==nums.length) {
lists.add(new ArrayList<>(list));
return;
}
for (int i = 0; i < nums.length; i++) {
if (list.contains(nums[i]))
continue;
list.add(nums[i]);
bfs(nums,list);
//撤销选择 (移除最后一个元素)
list.remove(list.size()-1);
}
}
6.二维数组路径问题
在一个二维数组中从任一位置开始,可以往他的上下左右4个方向走,然后返回走过的路线中值最大的,0其实就相当于障碍物,不能往位置为0的地方走。
输入:grid = [[0,6,0],[5,8,7],[0,9,0]]
输出:24
解释:
[[0,6,0],
[5,8,7],
[0,9,0]]
一种收集最多黄金的路线是:9 -> 8 -> 7。
代码
public int getMaximumGold(int[][] grid) {
int sum=0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j]==0)
continue;
sum=Math.max(sum,bfs(grid,i,j));
}
}
return sum;
}
private int bfs(int[][] grid, int i, int j) {
//边界条件
if (i<0 || i>=grid.length || j<0 || j>=grid[0].length || grid[i][j]==0)
return 0;
int temp=grid[i][j];
grid[i][j]=0;
int up = bfs(grid, i - 1, j); //向上
int down=bfs(grid,i+1,j); //向下
int left=bfs(grid,i,j-1); //向左
int right=bfs(grid,i,j+1); //向右
//找到最大值
int max=Math.max(Math.max(up,down),Math.max(left,right));
grid[i][j]=temp;
return grid[i][j]+max;
}