回溯法的实质
回溯法可看作穷举法的一种实现方式
计算过程
每步只构造一个部分节并立即对此部分解进行评估。若此部分解有可能拓展为“所求解”,则继续扩展;反之此部分解不可能扩展为所求解,则继续尝试其他部分解。直到穷尽一切可能。
解空间与解空间树
描述回溯法时,可有两种解空间树选择。一是子集树,一是排列树。
解空间:所有可能的解构成的集合。
解空间树:将解空间组织成树结构。
子集树:每个解(x1,….,xn)的每个分量xi的值取自于一个集合Si,解空间大小为2的n次方;
排列树:每个解(x1,…,xn)都是集合S的全排列。解空间大小为n!
一般情况下,子集树优于排列树,因为n! >> 2的n次方(题目要采取哪种解空间,要具体视情况而定)
解题步骤
1)针对所给问题,定义问题的解空间;
2)确定易于搜索的解空间结构;
3)以深度优先方式搜索解空间树,并在搜索过程中用剪枝函数避免无效搜索。
框架
t:表示递归深度,即当前扩展节点在解空间树中的深度。
output():纪录或输出结果的函数
constaint():约束函数。返回值=true,则满足约束条件;返回值=false,可剪去相应子树。
bound():限界函数。返回值=true,目标函数未越界,可用backtak(t+1)进一步搜索;返回值=false,可剪去相应子树。
一、子集树
1.递归回溯子集树的一般算法
void backtrack(int t){
if(t > n){
output(x);
}else {
for(int i = 0;i <= 1;i++){
x[t] = i;
if(constraint(t) && bound(t)){
backtrack(t+1);
}
}
}
}
2.递归回溯排列树的一般算法
因为是排列树,所以相当于当前的数不断地和后面的数交换,得到新的排列。
void backtrack(int t){
if(t > n){
output(x);
}else {
for(int i = t;i < n;i++){
swap(x[t],x[i]);
if(constraint(t) && bound(t)){
backtrack(t+1);
}
swap(x[t],x[i]);
}
}
}
例子
1、n皇后问题
描述:在nx格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行/同一列/同一斜线上的棋子。n后问题等价于在nxn格的棋盘上放置n个皇后,任何2个皇后不放在同一列/同一行/同一斜线。
解法一:
/**
* 递归版本
* 子集树来做n后问题
* 输出所有的排布方案和方案数量
*/
public class Queen1 {
public int sum;
/**
* 这个用1维数组存,设下标为i,i为行号,arr[i]为列号,表示第i行第j列的位置放了皇后
*/
private int[] arr;
public StringBuilder res;
private int n;
public void run(){
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
res = new StringBuilder();
arr = new int[n];
backtrack(0);
}
/**
* 回溯法法搜索子集树的一般算法
* @param layer layer为层数,即树的第x层,从0开始
*/
public void backtrack(int layer){
// 如果已经遍历到最后一层了,那说明前面的所有条件都满足,说明这是一个合格的序列,故保存
if(layer >= n){
update();
sum++;
return;
}
// 把第layer层给它从0-(n-1)的所有可能值,看看哪个满足
for(int i = 0;i < n;i++) {
arr[layer] = i;
if (bound(layer)) {
// 若满足,则继续填下一个
backtrack(layer + 1);
}
}
}
/**
* 判断是否满足,不满足则不要,这是限界。
* @param layer 目前的层数
* @return
*/
public boolean bound(int layer){
// 只有从0-(layer-1)层填了数,这是遍历arr[layer]的所有可能取值,不满足就false
for(int i = 0;i < layer;i++){
//for(int i = 0;i < n;i++){
// 共三个条件,不能在同一行同一列同一斜线
if(arr[i] == arr[layer]){
return false;
}
// 同一斜线判断用斜率就好
if((arr[i] - arr[layer] == (i - layer))){
return false;
}
if((arr[i] - arr[layer] == (layer - i))){
return false;
}
}
return true;
}
/**
* 纪录结果的函数
*/
public void update(){
for(int i = 0;i < n;i++){
res.append(arr[i] + " ");
}
res.append("\n");
}
}
解法二:
/**
* 对此题来说,用全排列好很多
* 后面的改造成1半也是跟Queen2的constraint函数相同
* 排列树来做n后问题
* 输出所有的排布方案和方案数量
*/
public class Queen3 {
public int sum;
private int[] arr;
public StringBuilder res;
private int n;
public void run(){
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
res = new StringBuilder();
arr = new int[n];
// 排列树来做,初始化要为n的有效排列之一,因为后面用的是swap来进行排列组合,如果刚开始不是一个n的一个排列,那后面也不会满足
for(int i = 0;i < n;i++){
arr[i] = i;
}
backtrack(0);
}
/**
* 回溯法法搜索子集树的一般算法
* @param layer layer为层数,即树的第x层,从0开始
*/
public void backtrack(int layer){
// 如果已经遍历到最后一层了,那说明前面的所有条件都满足,说明这是一个合格的序列,故保存
if(layer >= n){
update();
sum++;
return;
}
// 把layer后面的层一一跟layer层交换,得到全排列。
for(int i = layer;i < n;i++) {
swap(layer,i);
if (bound(layer)) {
// 若满足,则继续填下一个
backtrack(layer + 1);
}
swap(i,layer);
}
}
public void swap(int index1,int index2){
int temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
/**
* 判断是否满足,不满足则不要,这是限界。
* @param layer 目前的层数
* @return
*/
public boolean bound(int layer){
// 只有从0-(layer-1)层填了数,这是遍历arr[layer]的所有可能取值,不满足就false
for(int i = 0;i < layer;i++){
//for(int i = 0;i < n;i++){
// 共三个条件,不能在同一行同一列同一斜线
if(arr[i] == arr[layer]){
return false;
}
// 同一斜线判断用斜率就好
if((arr[i] - arr[layer] == (i - layer))){
return false;
}
if((arr[i] - arr[layer] == (layer - i))){
return false;
}
}
return true;
}
/**
* 纪录结果的函数
*/
public void update(){
for(int i = 0;i < n;i++){
res.append(arr[i] + " ");
}
res.append("\n");
}
}
解法三:
package com.lianup.suanfa.Backtrack;
import java.util.Scanner;
/**
* 迭代版本
* 子集树来做n后问题
* 输出所有的排布方案和方案数量
*/
public class Queen4 {
public int sum;
/**
* 这个用1维数组存,设下标为i,i为行号,arr[i]为列号,表示第i行第j列的位置放了皇后
*/
private int[] arr;
public StringBuilder res;
private int n;
public static void main(String[] args){
new Queen4().run();
}
public void run(){
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
res = new StringBuilder();
arr = new int[n];
backtrack();
System.out.println(sum);
}
/**
* 回溯法法搜索子集树的一般算法
*/
public void backtrack(){
int layer = 0;
arr[layer] = -1;
// 当layer为-1时,说明不存在满足条件的解了,说明遍历完成
while(layer > -1) {
//移到下一列
arr[layer]+=1;
// 如果不满足条件
while (arr[layer] < n && !bound(layer)) {
arr[layer] += 1;
}
// 如果找到了满足条件的值
if(arr[layer] < n){
// 若为此层的最后一个皇后,就纪录结果
if(layer == n - 1){
update();
sum++;
}else {
// 否则,继续遍历下一个皇后
layer++;
// 之前这里设置成了0,是错的,因为要为-1,后面才能满足arr[layer]++=1;
arr[layer] = -1;
}
// 找不到满足条件的值就回溯
}else {
layer--;
}
}
}
/**
* 判断是否满足,不满足则不要,这是限界。
* @param layer 目前的层数
* @return
*/
public boolean bound(int layer){
// 只有从0-(layer-1)层填了数,这是遍历arr[layer]的所有可能取值,不满足就false
for(int i = 0;i < layer;i++){
//for(int i = 0;i < n;i++){
// 共三个条件,不能在同一行同一列同一斜线
if(arr[i] == arr[layer]){
return false;
}
// 同一斜线判断用斜率就好
if((arr[i] - arr[layer] == (i - layer))){
return false;
}
if((arr[i] - arr[layer] == (layer - i))){
return false;
}
}
return true;
}
/**
* 纪录结果的函数
*/
public void update(){
for(int i = 0;i < n;i++){
res.append(arr[i] + " ");
}
res.append("\n");
}
}
2、0-1背包问题
public class Package1 {
int[][] element; // 物品,element[i][0]为重量,1为价值
int n;
int[] choose; //是否选择该物品
int c; // 背包容量
boolean hasRes; // 是否有结果
int bestV; // 最优值(上界)
int curW; //目前的已装入背包的重量
int curV; // 目前装入背包的所有物品总价值
public static void main(String[] args){
new Package1().run();
}
public void run(){
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
c = scanner.nextInt();
choose = new int[n];
element = new int[n][n];
curW = 0;
curV = 0;
hasRes = false;
bestV = 0;
for(int i = 0;i < n;i++){
int w= scanner.nextInt();
int v = scanner.nextInt();
element[i][0] = w;
element[i][1] = v;
}
// 把其按单价从高到低排序
Arrays.sort(element, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
double a = o1[1]/(double)o1[0];
double b = o2[1]/(double)o2[0];
if(b > a){
return 1;
}
if(b == a){
return 0;
}
return -1;
}
});
backtrack(0);
System.out.println("all value:" + bestV);
System.out.println(Arrays.toString(choose));
}
public void backtrack(int t){
if(t >= n){
hasRes = true;
bestV = curV;
return;
}
if(curW + element[t][0] <= c){
// 回溯解空间树的左子树
curW += element[t][0];
curV += element[t][1];
choose[t] = 1;
backtrack(t+1);
curW -= element[t][0];
curV -= element[t][1];
}
// 若其最大可能值大于上一次回溯的最优值,那么就让它继续回溯
// 即,右子树可能包含最优解时,才进入,否则剪枝
if(constraint(t+1) > bestV){
backtrack(t+1);
}
}
public int constraint(int t){
int leftw = c - curW;
int b = curV;
while(t < n && element[t][0] <= leftw){
leftw += element[t][0];
b += element[t][1];
t++;
}
return b;
}
}