输出1~n的全排列,其中n由键盘输入。
1>解向量的分析
要求所有排列结果,那么任何一个排列方案,就是问题的一个解,如何表示这个解呢?很显然,我们可以用一个数组int X[MAX]; 这就是解向量。
2>解空间树与回溯函数
每一个解元素的范围都在[1,n]范围,其解空间树如图左侧:
回溯函数如图右侧,
该函数围绕着X[1]~ X[n]的求解,从解空间树的根开始深度优先遍历,其中f(k+1)是对一棵子树进行遍历,每到达一个叶子节点【即if(k-1==n)成立时】,则输出从根到该叶子路径上存储的X[1]~X[n]【每一个非叶子节点处所选择的分支,相当于迷宫的一个岔路口的选择,用对应X[]记录了该岔路口的选择】。从代码中可以看出,并没有创建这棵树,该树是有神而无形的“隐形”树。
运行结果图2。
3>剪枝函数
图1程序中,对X[k]取值i时没有任何限制,所以输出结果中数字允许重复出现,而全排列问题,应该让X[k]与X[1]~ X[k-1]不重复,那么这就是剪枝逻辑,在没有状态信息时,剪枝函数xianjie(k,i)中通过循环扫描x[1]~x[k-1]看是否已经出现过i,从而使得其对应解空间树的遍历逻辑如图3。
4>状态量的设计
前面剪枝函数中,在判断X[k]能否取i时,用了一个循环,依次检查X[1]~X[k-1]中是否已经出现i。显然可以加入一个状态数组int used[ ],其初值均为0,用used[i]记住在x[1]~x[k-1]中i已经被使用次数,这样程序可以改进为:
int X[100],used[100],n, cnt=0; //键盘输入的n小于100
int xianjie(int k, int i)//判断X[k]能否取i
{
if(used[i] >0) return 0; //used[i]>0时表示数字i已经被使用过
//if(cnt>=20) return 0; //此剪枝可选用:只输出前20个排列结果
return 1;
}
void f(int k)
{ int i;
if(k-1==n) { 输出++cnt: 输出X[1]~X[n]; }
else for(i=1;i<=n;i++)
if(xianjie(k,i))
{
X[k]=i;
used[i]++; //数字i已经被使用的次数+1,从而在递归f(k+1)里面used[i]都是增加1之后的值
f(k+1); //遍历 x[k]取i 处的一颗子树
used[i]--;//f(k+1)结束后,准备遍历旁边一棵子树(即x[k]取i+1 处的子树),x[1]~x[k-1]没有变化,
//仅X[k]换成 i+1了,所以在旁边这棵子树中数字i的使用次数应-1
}
}
void main(void)
{
cout<<"Enter n:";
cin>>n;
f(1);
}
归纳:
(1)这是一个“麻雀虽小却五脏俱全”的例子。学会解向量的分析、围绕解向量求解的回溯函数框架、进一步控制解元素取值范围的剪枝函数(即问题提出的各种约束条件)。
(2)弄懂基本原理后,以后基本上可以一步到位的写含有状态量的回溯算法代码,而且只要发现剪枝函数中有循环,基本上都可以考虑设计状态量,从而减轻剪枝函数的运算量。另外状态量应该在 f(k+1)前、后做对称修改。
(3)回溯函数、剪枝函数、输出函数、外加其它辅助函数,区分开来写,而不要将全部的逻辑都裹在一起,这样有利于保持自己思路的清晰,出错时好对症下药。
(4)剪枝函数的注意事项,只有最后一个语句是 return 1; 前面全部是return 0的逻辑语句。其形式基本上如下
if(不满足约束的条件1) return 0;
if(不满足约束的条件2) return 0;
…
if(不满足约束的条件n) return 0;
return 1; //不需要 “else”