回溯算法的核心就是迭代,本质上并没有区别。相比较“回溯法”这个名字,我觉得“嗅探法”这个命名对于回溯算法更有描述性,即像一只小动物那样,走一步,嗅探一下,再向“正确”的方向移动,再进行嗅探,直到走到终点。回溯算法并不难掌握,甚至可以总结为一个固定的、适用于许多编程题的套路,至于这个套路是什么,我先不说,大家一步一步看了解会更深一些。
解空间
在展示回溯算法前,有一个概念需要大家了解——解空间。回溯法的解空间是树的形式(不了解什么是树的朋友请自己学习)。对于一个使用回溯算法的题目,如果可以把解空间构造出来,那么这个题就差不多可以解答了。解空间在我们学习的过程中已经有过使用,比如说在学习概率的时候,有许多题都需要用树形结构表示出所有的可能性。比如说摇骰子,摇三次,出现两个4和一个6的概率是多少?根据这个问题我们可以画出一个深度为3(摇三次),每个节点的度为6(骰子六个面)的树,共有6^3种结果,从中选取自己需要的结果。我们从这个例子可以看到使用解空间解决问题有两种作用:列举出所有的情况、选取特殊的结果(剪枝)。这两种操作分别对应了回溯算法的外在和内核。接下来我们用几个例子来了解回溯算法。
红绿灯
有一个红绿灯,上面有三个灯,每个灯有三种颜色,请分别用123表示三种颜色,得到这个红绿灯变色的可能性。
看完这个问题,有些朋友可能已经十指连动,写完了这样的一段代码
void light()
{
for(int i=1;i<=3;i++)
{
for(int j=1;j<=3;j++)
{
for(int k=1;k<=3;k++)
{
printf("%d%d%d",i,j,k);
printf("\n");
}
}
}
}
这段代码没有问题,三层for循环表示树的深度为3,每个for循环都是从1到3,表示每个节点的度为3,构造出了正确的解空间。虽然这样写是正确的,我们在使用回溯算法时不推荐这样写:3个灯是三层for循环,那么10个灯呢?不能真的写十层for循环吧。所以我们对代码进行改写,使用迭代的方式,但不改变代码的意思。
void light(int n,int z[])
{
if(n==3) /*迭代三次后停止,打印此时的数组z*/
{
for(int i=0;i<3;i++)
{
printf("%d",z[i]);
}
printf("\n");
}
for(int i=1;i<=3;i++) /*对z[n]赋值,123分别赋值,然后再次迭代,对下一个位置进行赋值*/
{
z[n]=i;
light(n+1,z);
}
}
通过这个例子,我们了解到使用回溯算法时,应该采用迭代的方式编写代码。下面我们通过一个简单的例子来熟悉回溯算法。
全排列
给定一个数组,里面有不重复的n个正整数,求它们的全排列。
求数组的全排列,直白地说,就是对每个数进行讨论:取,还是不取?这样这个问题可以进一步简化为与上面红绿灯问题相近的问题:有n个灯,每个灯有两种颜色,有几种可能性?这并不难,我们直接看代码。
void track(int z[],int n,int j,int A