本篇例子的完整代码地址:
求集合幂集 [email protected]:hglspace/PowerSet.git
四皇后问题的解 [email protected]:hglspace/FourQueens.git
1 在程序设计中,有相当一类是求一组解,或求全部解或求最优解,例如八皇后问题,不是根据某种确定的计算法则,而是利用试探和回溯(Backtracking)的搜索技术求解。回溯法也是设计递归过程的一种重要方法,它的求解过程实际上是一个先序遍历一棵"状态树"的过程,只是这棵树不是遍历前预先建立的,而是隐含在遍历过程中的,如果认识到这一点,很多问题的递归过程设计也就迎刃而解了。
2 看一个简单的例子
求含n个元素的集合的幂集
集合A的幂集是由集合A的所有子集所组成的集合。如:A={1,2,3},则A的幂集
p(A)={{1,2,3},{1,2},{1,3},{1},{2,3},{2},{3},空}
幂集的每个元素是一个集合,它或是空集,或含集合A中一个元素,或含集合A中两个元素,或等于集合A。反之,从集合A的每个元素来看,它只有两种状态:它或属幂集的元素集,或不属幂集的元素集。则求幂集p(A)的元素的过程可看作是依次对集合A中的元素进行"取"或"舍"的过程,并且可以用一棵二叉树来表示过程中幂集元素的状态变化状况,树中的根结点表示幂集元素的初始状态(为空集);叶子结点表示它的终结状态;而第i(i=2,3,....,n-1)层的分支结点,则表示已对集合A中前i-1个元素进行了取/舍处理的当前状态。因此求幂集元素的过程即为先序遍历这棵状态树的过程(意思是:在求幂集的时候会隐蔽生成一棵树,就当这棵树已经存在了,对元素的取舍的过程就像是先序遍历这棵树)。
c语言代码:
#include <stdio.h>
int main(int argc, const char * argv[]) {
char c[3]={'1','2','3'};//集合A
char c1[3]={'\0'};//集合A的幂集的子集
void powerSet(int i,int n,char c[],char c1[]);//求集合的幂集的函数
powerSet(0, 2, c, c1);
return 0;
}
/*
求集合的幂集的函数
*/
void powerSet(int i,int n,char c[],char c1[]){
if(i>n){//I从0开始,当i大于2时,说明所有的元素都已经筛选过了,结果已经出来了
int j;
printf("[\t");
for(j=0;j<=n;j++){
if(c1[j]!='\0'){
printf("%c\t",c1[j]);
}
}
printf("]\n");
}else{
c1[i]=c[i];//取第i个元素
powerSet(i+1, n, c, c1);
c1[i]='\0';//把第i个位置的元素的值置为'\0',相当于舍弃第i个元素,就是回溯
powerSet(i+1, n, c, c1);
}
}
上图中的状态变化是一棵满二叉树,树中每个叶子结点的状态都是求解过程中可能出现的状态(即问题的解)。
3 四皇后问题
很多问题用回溯和试探求解时,描述求解过程的状态树不是一棵满的多叉树。当试探过程中出现的状态和问题所求解产生矛盾时,不再继续试探下去。这时出现的叶子结点不是问题的解的终结状态。这类问题的求解过程可看成是在约束条件下进行先序(根)遍历,并在遍历过程中剪去那些不满足条件的分支。
如求四皇后问题的所有合法布局,下图是求解过程中棋盘状态的变化情况,这是一棵四叉树,树上每个结点表示一个局部布局或一个完整的布局。根结点表示棋盘的初始状态:棋盘上无任何棋子。每个棋子都有4个可选择的位置,但在任何时刻,棋盘的合法布局都必须满足三个条件,即任何两个棋子都不占据棋盘上的同一行或者同一列或者同一对角线。
求所有合法布局的过程即为在上述约束条件下先根遍历下图的状态树的过程。遍历中访问结点的操作为,判别棋盘上是否已得到一个完整的布局(即棋盘上是否已摆上4个棋子),若是,则输出该布局;否则依次先根遍历满足约束条件的各棵子树,即首先判断该子树根的布局是否合法,若合法,则先根遍历该子树,否则剪去该子树的分支。
核心代码:
void Trial(char cb[][4],int n,int i){
//进入Trial时,在这个棋盘前i-1行已经放置了互不攻击的i-1个棋子
//现从第i行起后续棋子选择合适的位置
int s;
int judgeLegal(char cb[][4],int i,int j);
if(i==n){//递归结束条件,4颗棋子已经全部放完了(其实就是找到符合条件的布局了,当i=3结束时,4颗棋子都已经放好了),递归结束
int j,k;
for(j=0;j<n;j++){
for(k=0;k<n;k++){
printf("%c\t",cb[j][k]);
if(k%3==0&&k!=0){
printf("\n");
}
}
}
printf("#################################\n");
}else{
for(s=0;s<n;s++){//在第i行第s列放置一个棋子
cb[i][s]='*';
if(judgeLegal(cb,i,s)){//判断当前布局是否合法
Trial(cb, n, i+1);
}
cb[i][s]='-';//回溯
}
}
}