回溯法有“通用解題法”之稱。用它可以系統地搜索問題的所有解。回溯法是一個既帶有系統性又帶有跳躍性的搜索算法。
在包含問題的所有解的解空間樹中,按照深度優先搜索的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,如果包含,就從該結點出發繼續探索下去,如果該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜索算法)。若用回溯法求問題的所有解時,要回溯到根,且根結點的所有可行的子樹都要已被搜索遍才結束。 而若使用回溯法求任一個解時,只要搜索到問題的一個解就可以結束。
1.回溯法的解題步驟
(1)針對所給問題,定義問題的解空間;
(2)確定易於搜索的解空間結構;
(3)以深度優先方式搜索解空間,並在搜索過程中用剪枝函數避免無效搜索。
2.子集樹與排列樹
下面的兩棵解空間樹是回溯法解題時常遇到的兩類典型的解空間樹。
(1)當所給問題是從n個元素的集合S中找出S滿足某種性質的子集時,相應的解空間樹稱為子集樹。例如從n個物品的0-1背包問題(如下圖)所相應的解空間樹是一棵子集樹,這類子集樹通常有2^n個葉結點,其結點總個數為2^(n+1)-1。遍歷子集樹的算法需Ω(2^n)計算時間。
(2)當所給問題是確定n個元素滿足某種性質的排列時,相應的解空間樹稱為排列樹。例如旅行售貨員問題(如下圖)的解空間樹是一棵排列樹,這類排列樹通常有n!個葉結點。遍歷子集樹的算法需Ω(n!)計算時間。
用回溯法搜索子集樹的一般算法可描述為:
-
/**
-
* output(x) 記錄或輸出得到的可行解x
-
* constraint(t) 當前結點的約束函數
-
* bount(t) 當前結點的限界函數
-
* @param t t為當前解空間的層數
-
*/
-
void backtrack(int t){
-
if(t >= n)
-
output(x);
-
else
-
for (
int i =
0; i <=
1; i++) {
-
x[t] = i;
-
if(constraint(t) && bount(t))
-
backtrack(t+
1);
-
}
-
}
用回溯法搜索排列樹的一般算法可描述為:
-
/**
-
* output(x) 記錄或輸出得到的可行解x
-
* constraint(t) 當前結點的約束函數
-
* bount(t) 當前結點的限界函數
-
* @param t t為當前解空間的層數
-
*/
-
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) && bount(t))
-
backtrack(t+
1);
-
swap(x[t], x[i]);
-
}
-
}
3.回溯法的應用例子
(a)子集樹
(為了便於描述算法,下列方法使用了較多的全局變量)
I.輸出集合S中所有的子集,即limit為all;
II.輸出集合S中限定元素數量的子集,即limit為num;
III.輸出集合S中元素奇偶性相同的子集,即limit為sp。
-
public
class Subset {
-
-
private
static
int[] s = {
1,
2,
3,
4,
5,
6,
7,
8};
-
private
static
int n = s.length;
-
private
static
int[] x =
new
int[n];
-
-
/**
-
* 輸出集合的子集
-
* @param limit 決定選出特定條件的子集
-
* 注:all為所有子集,num為限定元素數量的子集,
-
* sp為限定元素奇偶性相同,且和小於8。
-
*/
-
public static void all_subset(String limit){
-
switch(limit){
-
case
"all":backtrack(
0);
break;
-
case
"num":backtrack1(
0);
break;
-
case
"sp":backtrack2(
0);
break;
-
}
-
}
-
-
-
/**
-
* 回溯法求集合的所有子集,依次遞歸
-
* 注:是否回溯的條件為精髓
-
* @param t
-
*/
-
private static void backtrack(int t){
-
if(t >= n)
-
output(x);
-
else
-
for (
int i =
0; i <=
1; i++) {
-
x[t] = i;
-
backtrack(t+
1);
-
}
-
-
}
-
-
/**
-
* 回溯法求集合的所有(元素個數小於4)的子集,依次遞歸
-
* @param t
-
*/
-
private static void backtrack1(int t){
-
if(t >= n)
-
output(x);
-
else
-
for (
int i =
0; i <=
1; i++) {
-
x[t] = i;
-
if(count(x, t) <
4)
-
backtrack1(t+
1);
-
}
-
-
}
-
-
/**
-
* (剪枝)
-
* 限制條件:子集元素小於4,判斷0~t之間已被選中的元素個數,
-
* 因為此時t之后的元素還未被遞歸,即決定之后的元素
-
* 是否應該被遞歸調用
-
* @param x
-
* @param t
-
* @return
-
*/
-
private static int count(int[] x, int t) {
-
int num =
0;
-
for (
int i =
0; i <= t; i++) {
-
if(x[i] ==
1){
-
num++;
-
}
-
}
-
return num;
-
}
-
-
/**
-
* 回溯法求集合中元素奇偶性相同,且和小於8的子集,依次遞歸
-
* @param t
-
*/
-
private static void backtrack2(int t){
-
if(t >= n)
-
output(x);
-
else
-
for (
int i =
0; i <=
1; i++) {
-
x[t] = i;
-
if(legal(x, t))
-
backtrack2(t+
1);
-
}
-
-
}
-
-
/**
-
* 對子集中元素奇偶性進行判斷,還需元素的數組和小於8
-
* @param x
-
* @param t
-
* @return
-
*/
-
private static boolean legal(int[] x, int t) {
-
boolean bRet =
true;
//判斷是否需要剪枝
-
int part =
0;
//奇偶性判斷的基准
-
-
for (
int i =
0; i <= t; i++) {
//選擇第一個元素作為奇偶性判斷的基准
-
if(x[i] ==
1){
-
part = i;
-
break;
-
}
-
}
-
-
for (
int i =
0; i <= t; i++) {
-
if(x[i] ==
1){
-
bRet &= ((s[part] - s[i]) %
2 ==
0);
-
}
-
-
}
-
-
int sum =
0;
-
for(
int i =
0; i <= t; i++){
-
if(x[i] ==
1)
-
sum += s[i];
-
}
-
bRet &= (sum <
8);
-
-
return bRet;
-
}
-
-
-
/**
-
* 子集輸出函數
-
* @param x
-
*/
-
private static void output(int[] x) {
-
for (
int i =
0; i < x.length; i++) {
-
if(x[i] ==
1){
-
System.out.print(s[i]);
-
}
-
}
-
System.out.println();
-
}
-
-
}
(b) 排列樹
(為了便於描述算法,下列方法使用了較多的全局變量)
I.輸出集合S中所有的排列,即limit為all;
II.輸出集合S中元素奇偶性相間的排列,即limit為sp。
-
public
class Permutation {
-
-
private
static
int[] s = {
1,
2,
3,
4,
5,
6,
7,
8};
-
private
static
int n = s.length;
-
private
static
int[] x =
new
int[n];
-
-
/**
-
* 輸出集合的排列
-
* @param limit 決定選出特定條件的子集
-
* 注:all為所有排列,sp為限定元素奇偶性相間。
-
*/
-
public static void all_permutation(String limit){
-
switch(limit){
-
case
"all":backtrack(
0);
break;
-
case
"sp":backtrack1(
0);
break;
-
}
-
}
-
-
-
/**
-
* 回溯法求集合的所有排列,依次遞歸
-
* 注:是否回溯的條件為精髓
-
* @param t
-
*/
-
private static void backtrack(int t){
-
if(t >= n)
-
output(s);
-
else
-
for (
int i = t; i < n; i++) {
-
swap(i, t, s);
-
backtrack(t+
1);
-
swap(i, t, s);
-
}
-
-
}
-
-
/**
-
* 回溯法求集合中元素奇偶性相間的排列,依次遞歸
-
* @param t
-
*/
-
private static void backtrack1(int t){
-
if(t >= n)
-
output(s);
-
else
-
for (
int i = t; i < n; i++) {
-
swap(i, t, s);
-
if(legal(x, t))
-
backtrack1(t+
1);
-
swap(i, t, s);
-
}
-
-
}
-
-
/**
-
* 對子集中元素奇偶性進行判斷
-
* @param x
-
* @param t
-
* @return
-
*/
-
private static boolean legal(int[] x, int t) {
-
boolean bRet =
true;
//判斷是否需要剪枝
-
-
//奇偶相間,即每隔一個數判斷奇偶相同
-
for (
int i =
0; i < t -
2; i++) {
-
bRet &= ((s[i+
2] - s[i]) %
2 ==
0);
-
}
-
-
return bRet;
-
}
-
-
-
/**
-
* 元素交換
-
* @param i
-
* @param j
-
*/
-
private static void swap(int i, int j,int[] s) {
-
int tmp = s[i];
-
s[i] = s[j];
-
s[j] = tmp;
-
}
-
-
/**
-
* 子集輸出函數
-
* @param x
-
*/
-
private static void output(int[] s) {
-
for (
int i =
0; i < s.length; i++) {
-
System.out.print(s[i]);
-
}
-
System.out.println();
-
}
-
}
參考文獻:
1. 《算法設計與分析》