回溯算法是一種遞歸模式,它是一種暴力求解方法(brute force method),用於求出所有可能的解,回溯算法通常會構建一個狀態空間樹(state space tree),
將可能的組和從根到葉節點進行展開,然后以深度優先的方式搜索遍歷狀態樹,遍歷過程中遇到不符合解的節點立馬返回進行新的遍歷,而不是繼續遍歷,
狀態空間樹的結構可用下圖進行描述:
回溯算法不是用來求解最優解,而是用來求解可行解的方法,回溯算法的代碼結構:
Backtrack(x)
if x is not a solution
return false // return directly
if x is a new solution
add to list of solutions
backtrack(expand x)
根據以上結構,用具體的示例進行分析,例如:
1、排隊問題,假設有2個男孩和1個女孩,女孩不能站在男孩的中間,則可以用一個樹的結構進行描述:
樹是構建好了,根據構建樹構建代碼:
arrange-boy-girl(p,index,result,mem)://index 用於記錄孩子的位置,result是最終的排列結果,mem用於記錄孩子是否已經排在隊中了
if index == p.length:
print(result);//輸出結果
// expand p
for i =0 to p.length:
if(index == 2 && p[i]=='girl' || mem[i] ==1)://位置2不能是女孩,且該小孩沒有在隊列中,繼續進行循環
continue;
result[index]=p[i];//將小孩排到隊中
mem[i]=1;//記錄一下,下次這個小孩不能再排了,因為已經在隊伍中了
index++;//排下一個位置
arrange-boy-girl(p,index,result,mem);//遞歸調用,排下一個位置
index--;//注意這里,index恢復原值,表示原來的index位置還可以安排下一個小孩,比如,位置0可以是boy1,也可以是boy2
mem[i]=0;//這里也是,index恢復原值后,mem也要恢復原值
以上是一個全排列問題,但是它有一些限制,就是女孩不能排到男孩兒中間。
2、數的組合排列問題,給定一個數組 1 2 3,可以拼成多少種不同的兩位數,每個數都可以重復利用,先構建組合樹:
以上構建的是兩位數,根據同樣的方式還可以繼續構建,可以是3位數,4位數,根據構建樹編寫程序:
combine-num(arr,depth,result):
//遞歸的結束條件
if depth == 2:
print(result);
// expand array
for int i =0 to arr.length:
result.add(arr[i]);//添加元素
depth++;//走到下一個位置
combine-num(arr,depth,result);
depth--;//回溯到上一個位置
result.removeLastNum();//將當前位置元素刪除,相當於回溯到上一個位置以存放for循環中的第i+1個元素
本示例中,遞歸的結束條件是depth=2,如果需要是3位數,則depth=3,注意這里的“回溯”是在哪里體現的,就是“depth--”那個位置,表示返回
到上一個位置,將該位置放置第(i+1)個元素。
3、子集和的問題,給定一個集合{1,2,3,4,5,6,7,8,9,10}和一個值val=15,判斷該集合中是否存在元素和為val的子集,如果有則輸出該子集;
這個和上面的是一樣的,也是一個組合問題,但是為了避免重復解的出現,比如 7+8=15,8+7=15;可以讓每個元素只和它后面的元素進行組合;構造程序
如下:
def subset-sum(arr,fromIndex,sum,val,result):
if sum==val:
print(result);
for i=fromIndex to arr.length:
sum+=arr[i];//加上當前元素
result.add(arr[i]);//將當前元素放到子集中
subset-sum(arr,i+1,sum,val,result);// 注意這里,fromIndex變為了i+1,也就是當前元素只能和它后面的元素進行組合
sum-=arr[i];//減去當前元素,回溯到上一個子集和的狀態
result.removeLast();//刪除當前元素,回溯到上一個子集狀態
4、八皇后問題,在8*8的空格列表中,每個空格可以放一個皇后,皇后可以攻擊與它同行或同列的其他皇后,在該列表中擺放8個皇后,使其不能
互相攻擊,求一共有多少種擺放方法;首先構造樹結構,這里有一個4*4的列表結構:
從這棵樹的構造過程,可以大體得出代碼的構造過程,根節點到葉子節點的距離就是該葉子節點的深度,其實也就是行數rows;每行中空格的個數也就是
表格的列數cols,可以創建一個arr[rows][cols]的二維數組,然后構建代碼:
N-Queens(arr,rows,cols,row):
if row == rows://當前樹的深度為表格的行數,也就是所有行都遍歷了,則打印出結果
print(arr);
else:
for col=0 to cols://遍歷第depth行中的每個空格
if isValid(arr,row,col,rows,cols):
arr[row][col]=1;
row+=1;//移動到下一行
N-Queen(arr,rows,cols,row);//遍歷下一行
row-=1;//回溯到上一行,對上一行的下一個元素繼續遞歸運算
arr[row][col]=0;//將原來位置重新恢復原值0,注意這里代碼是有先后順序的,row-=1在前,恢復原值在后
else:
continue;//繼續遍歷該行的下一個元素
這里有一個isValid方法,用於判斷row行和col列中沒有皇后存在:
isValid(arr,row,col,rows,cols):
for i=0 to rows and i!=row
if arr[i][col]==1 return false; //存在皇后,該列非法
for i=0 to cols and i!=col
if arr[row][i]==1 return false; //存在皇后,該行非法
return true;//該空格所在的行和列都不存在皇后,是合法位置
總結:
回溯算法是和遞歸相關的,首先需要構建一個狀態空間樹,在代碼層面是通過遍歷和遞歸的形式構建一顆邏輯性的狀態空間樹,而非真的創建一個
樹型的數據結構,在遍歷和遞歸的過程中,這些執行路線構成了一個狀態空間樹,回溯算法的代碼接口可如下表示:
backtrace(arr,result,xx):
if xx滿足條件:
print(result)
else
for el in arr: //expand arr
result.add(el);//做選擇
update xx;//更新條件
backtrace(arr,result,xx);
cancel xx;//撤銷條件
result.delete(el);//撤銷選擇
回溯的難點就是構造樹的過程,這是一個邏輯形式上的構建,通過遍歷和遞歸加以實現,同時需要對執行過程對狀態更新和撤銷。