回溯是遍历搜索空间所有可能组态的方法。这些组态也许代表对象的所有排列或这是构建对象集合的所有可能的方法(子集)。其他情况包括列举一个图的所有生成树,两个节点的所有路径或是把节点分类成不同颜色的所有不同的方式。
这些问题有一个共同的难点就是我们必须每次产生一个可能的组态。避免重复或遗漏组态的方法就是我们必须定义一个系统性的产生组态的顺序。我们把组合搜索解作为一个向量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