(三)回溯算法
一、基本概念
回溯算法,又称穷举算法,实际上是一个枚举的搜索尝试过程,主要在搜索尝试过程中寻求问题的解,当发现满足求解条件时,则将路径存储,否则回溯返回,尝试别的路径。
二、设计思想
回溯思想:穷举搜索
回溯设计:(1)域:设置一个数组arr用于存放可达路径,设置一个数组List<arr>用于存放所有可达路径
(2)问题转化:问题转化为求解可达路径下一步如何走的问题
(3)方法逻辑:给出可达路径的终止条件,满足条件时存储路径,否则将大问题分割成多个规模较小的相同问题,递归求解。
三、问题分析
下面以八皇后问题进行说明,原题表述如下:
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
算法选型:
八皇后问题实质是对可达路径的穷举求解,因而应该选用回溯算法
题目分析:
由于皇后不能处于同一行,因此按行来规定皇后,第一行放第一个皇后,第二行放第二行皇后,直到所有行放完;在上述过程中涉及到两点,首先每行的一个皇后放置在什么位置,这是不确定的需要穷举,但必须满足与前面皇后不能同一列,也不能同一对角线
算法逻辑:
(1)域:array用于存放八皇后的正确摆放方式,storage用于存储所有的摆放方式
(2)问题转化:问题转化为求解第n个皇后放置在什么位置的问题,即:void placeQueue(int n);
(3)终止条件:当摆放第8个皇后时即为终止信号,说明当前路径符合要求即为存储
(4)子问题递归:第n个皇后有多种方法可以放在任意位置,因为需要for穷举,当第n个皇后放置后如果与前面放置的n-1个皇后满足上述限制条件,则继续放置n-1个皇后,与前述完全一致,递归即可
代码示意如下所示:
public class Example { private static ArrayList<ArrayList<Integer>> storage = Lists.newArrayList(); /** * 总皇后数,此时设置为8皇后在8X8棋盘的摆放 **/ private static int max = 8; /** * 存放八皇后的摆放方式,第一个皇后摆在array[0]列,第二个摆在array[1]列,..... **/ private static int[] array = new int[max]; /** * @param n 当前是第几个皇后(从0开始计算) **/ private static void placeQueue(int n) { // 一行摆放完毕 if (n == max) { ArrayList<Integer> arrayList = Lists.newArrayList(); for (int i = 0; i < max; i++) { arrayList.add(array[i]); } storage.add(arrayList); return; } //从第一列开始放值,然后判断是否和本行本列本斜线有冲突,如果OK,就进入下一行的逻辑 for (int i = 0; i < max; i++) { array[n] = i; // 判断是否符合八皇后条件,符合则摆放下一个皇后,否则回退继续尝试摆放 if (judgeLegal(n)) { placeQueue(n + 1); } } } /** * @param n 摆放的当前皇后的编号, array[n]为要当前摆放的皇后的位置,array[0~n-1]为前n-1已经摆放的皇后位置 * @func 判断是否符合八皇后条件 * 分析: 假设一个皇后所在位置为n列,位置为arr[n],坐标为(n, arr[n]), 另一个皇后的坐标为(i, arr[i]), * 如果两个皇后在一条对角线上, 画图根据数学分析可知,则必有Math.abs(n-i) == Math.abs(arr[n], arr[i]); * 如果两个皇后在不同行的同一位置则必有arr[n] == arr[i] **/ private static boolean judgeLegal(int n) { for (int i = 0; i < n; i++) { if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) { return false; } } return true; } public static void main(String[] args) { placeQueue(0); for (int i = 0; i < storage.size(); i++) { System.out.println(storage.get(i)); } } }
四、回溯算法典型类型题
回溯算法虽然复杂但是题型有限,可分为路径长度固定的穷举和路径长度不定的穷举两类,其中,八皇后问题属于路径长度固定的穷举类。
类型一:路径长度不定的穷举
原题表述如下:
输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
代码与分析如下:
import java.util.ArrayList; /** public class TreeNode { int val = 0; TreeNode left = null; TreeNode right = null; public TreeNode(int val) { this.val = val; } } */ public class Solution { // 第一步:定义域用于存储单条可行路径和所有可行路径 private ArrayList<ArrayList<Integer>> listAll = new ArrayList<ArrayList<Integer>>(); private ArrayList<Integer> list = new ArrayList<Integer>(); // 第二步:定义方法用于从某个节点开始查找,目标数字为target public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) { // 第三步:定义无法遍历的条件,节点为null if(root == null){ return listAll; } // 第三步:定义正常路径终止条件,如果是共用数组则需要回退 list.add(root.val); target = target - root.val; if(target == 0 && root.left == null && root.right == null){ // 注意,因为共用list,如果不new一个会对其他路径的遍历有影响 listAll.add(new ArrayList<Integer>(list)); } // 第四步:没有达到要求时应该从左子树和右子树分别找符合需求的节点重复递归即可 FindPath(root.left, target); FindPath(root.right, target); // 回退到上一步继续搜索 list.remove(list.size() - 1); return listAll; } }
类型二:路径长度固定的穷举
原题表述如下: 输入一个字符串,按字典序打印出该字符串中字符的所有排列。
分析:全排列的问题相当于穷举的问题,并且当字符串固定后其输出符合要求的队列长度也固定,因此等同于八皇后问题
步骤:(1)先放置字符串中的第一个字符,再放置字符串中的第二个字符,。。。直到放置完毕说明满足要求
(2)第一个字符可能性取决于字符串的长度,并且放置一个后后面的可能性减少
public Class Example{
// 域:存储路径
private Set<String> set = new TreeSet<>();
// 队列:存储可行性路径(长度固定)
private ArrayList<String> arrayList = new ArrayList<>();
public ArrayList<String> Permutation(String str) {
// 先腾出空间
for (int i = 0; i < str.length(); i++) {
arrayList.add("");
}
getData(str, 0);
return new ArrayList<>(set);
}
/**
* @param str 要排列的字符串
* @param index 当前要放置的字符的位置下标
*/
public void getData(String str, int index) {
// 交换终态
if (index == arrayList.size()) {
String it = "";
for (int i = 0; i < arrayList.size(); i++) {
it = it + arrayList.get(i);
}
set.add(it);
} else {
for (int i = 0; i < str.length(); i++) {
// 放置第一个元素
arrayList.set(index, str.charAt(i) + "");
// 除了已经放置的元素,剩下的都可以放置在剩下的位置
String temp = str.substring(0,i)+str.substring(i+1,str.length());
getData(temp, index + 1);
}
}
}
public static void main(String[] args) {
Example iHello2 = new Example();
System.out.println(iHello2.Permutation("abcd"));
}
}