回溯是遍歷搜索空間所有可能組態的方法。這些組態也許代表對象的所有排列或這是構建對象集合的所有可能的方法(子集)。其他情況包括列舉一個圖的所有生成樹,兩個節點的所有路徑或是把節點分類成不同顏色的所有不同的方式。
這些問題有一個共同的難點就是我們必須每次產生一個可能的組態。避免重復或遺漏組態的方法就是我們必須定義一個系統性的產生組態的順序。我們把組合搜索解作為一個向量a=(a1,a2,...,an),向量元素ai來自有限子集S。這樣的向量也許表示一個序列,元素ai是排列的第i個元素。或者,向量表示的是一個子集S,ai代表空間中的第i個元素是否在子集S中。
在回溯算法的每一步,我們都試圖通過在末尾添加其他元素來擴展一個給定的部分解a=(a1,a2,...,ak)。在擴展之后,我們必須檢驗目前的部分解是不是符合條件的完整解:如果是,我們應該輸出或將解的數量加一(計數)。如果不是完整解,那么我們應該驗證這個部分解有沒有可能擴擴展成為完整解。
回溯算法構造了一顆部分解的樹,樹中每一個節點代表了部分解。如果節點y是從節點x構造而來,那么x與y之間就存在一條邊。這棵樹為思考回溯算法提供了另一個角度,構造解的過程實際上就是在這棵樹上進行深度優先搜索遍歷。回溯算法的結構如下所示:
1 bool finished = FALSE; /* 是否獲得全部解? */ 2 backtrack(int a[], int k, data input) 3 { 4 int c[MAXCANDIDATES]; /*這次搜索的候選 */ 5 int ncandidates; /* 候選數目 */ 6 int i; /* counter */ 7 if (is_a_solution(a,k,input)) 8 process_solution(a,k,input); 9 else { 10 k = k+1; 11 construct_candidates(a,k,input,c,&ncandidates); 12 for (i=0; i<ncandidates; i++) { 13 a[k] = c[i]; 14 make_move(a,k,input); 15 backtrack(a,k,input); 16 unmake_move(a,k,input); 17 if (finished) return; /* 如果符合終止條件就提前退出 */ 18 } 19 } 20 }
這個算法的應用程序部分包括5個子程序:
1 is a solution(a,k,input) -這個布爾函數驗證向量a的前k個元素能否構成一個給定問題的完整解。最后一個參數,input,允許我們像函數中傳遞其他必要的信息。我們可以用它指定n——目標解的大小。
2 construct candidates(a,k,input,c,ncandidates) 根據目前狀態,構造這一步可能的選擇,存入c[]數組,其長度存入ncandidates
3 process_solution(a,k,input) 對於符合條件的解進行處理,通常是輸出、計數等
4 make_move(a,k,input)和unmake_move(a,k,input) 前者將采取的選擇更新到原始數據結構上,后者把這一行為撤銷。
求一個集合的所有子集
一個關鍵問題是,當指定表示組合對象的狀態空間時,這個空間需要表示多少對象。比如,對於序列{1,...,n}這樣一個n元素集合,有多少個子集存在呢?當n=1時,只有兩個子集存在,即{}和{1}。當n=2時,有4個子集存在,當n=3時有8個子集。所以對於n,有2n個子集存在。
每一個子集可以用元素是否在其中來表示。為了構造所有2n個集合,我們建立一個n元素的集合,其中ai的值表示第i個元素是否在給定的子集中。在一般的回溯算法機制中,Sk=(true, false)並且只要k=n時a就是一個解。現在我們可以通過簡單的實現is_a_solution(), construct_candidates()以及process_solution().
由於每次for循環中a[k]=c[i],這是唯一的改動,並且在下次循環時會被覆蓋,不需要專門編寫make_move()和make_unmove()。
#include<stdio.h> #include<stdlib.h> #include<string.h> #define MAXCANDIDATES 100 bool is_a_solution(int *a, int k, int n) { return k == n; } void process_solution(int* a, int k, int n) { int i; printf("{"); for(i = 1; i <= k; ++i) { if (a[i] == true) printf("%d", i); } printf("}\n"); } void construct_candidate(int a[], int k, int n, int c[], int *ncandidates) { c[0] = false; c[1] = true; *ncandidates = 2; } void backtrack(int* a, int k, int n) { int c[MAXCANDIDATES]; int ncandidates; int i; if (is_a_solution(a, k, n)) { process_solution(a, k ,n); } else { k = k + 1; construct_candidate(a, k, n, c, &ncandidates); for (i = 0; i < ncandidates; i++) { a[k] = c[i]; backtrack(a, k, n); } } } void generate_subsets(int n) { int *a = (int*)malloc(sizeof(int)*n); memset(a, 0, sizeof(int) * n); for(int i = 0; i < n; i++) a[i] = i + 1; backtrack(a, 0, n); } int main() { generate_subsets(3); return 0; }
參考資料:
1. 《算法設計手冊》
2. http://www.cnblogs.com/wuyuegb2312/p/3273337.html#intro