全排列算法是DFS(深度优先搜索)的一个简单入门应用。
假设我们现在给过一个数n, 要求我们输出从1到 n的n个数的所有排列方式。
详细要求可参考codevs中 1294全排列问题。
首先给出程序的主体及各个变量的作用:
#include<stdio.h>
#include<stdlib.h>
int main(void){
int n;
scanf("%d", &n);
int *flag = (int *)malloc(sizeof(int) * (n + 1));
int *result = (int *)malloc(sizeof(int) * (n + 1));
int i;
for(i = 1;i <= n;i++){
flag[i] = 0;
}
int index = 1;
DFS(result, flag, index, n);
return 0;
}
①flag数组标志着第i位数字是否使用,当第i位数字没有使用的时候,flag[i] = 0, 当第i位数字已经被使用的时候, flag[i] = 1。
②result数组包含了要输出的结果。
③index变量代表着当前的第几层(也就是第几位数字)。
④flag数组以及result数组分配n + 1个空间是为了更直观,从数组[1]开始存放值。
接下来给出DFS函数:
void DFS(int *result, int *flag, int index, int n){
int i;
if(index == n + 1){
for(i = 1;i <= n;i++){
printf("%d ", result[i]);
}
printf("\n");
}
else{
for(i = 1;i <= n;i++){
if(flag[i] == 0){
result[index] = i;
flag[i] = 1;
DFS(result, flag, index + 1, n);
flag[i] = 0;
}
}
}
}
①首先该函数终止的条件是index == n + 1, 因为当index == n时最后一位数字还没有放进result中,所以真正结束的条件是n + 1。
②因为index是逐层加一,所以不会出现index > n + 1的情况,即是说,index < n + 1时证明还没有构造出一个可以输出的结果。
③当我们第一次递归至可以输出结果时,因为每一个数字依次递增都是可用的,所以第一个输出的数字肯定是1 2 ….. n,而此时flag数组上所有的值都为1代表所有数字都已经使用。
④当输出这一结果后,我们再进行回溯,flag[n] = 0意味着当前数字n不使用,然后回到上一层(index == n - 1层),循环刚好到达到第n - 1层的DFS函数处, 继续数字n - 1不使用,而此时上面的循环(i <= n)迭代加一(此时数字n没有使用所以可以flag[n] == 0 为true),所以在result数组的n - 1位置上插入了n,在下一个DFS函数中第n个位置只能插入n - 1。
举个简单的例子:假设n = 5,则第一个数是12345, 而第二个是12354.
⑤每一次循环中都会优先找到比较小的数在高位所以排序从小到大。(可以通过改变1到n的顺序来改变)
⑥其实第一次循环时当不看调用DFS函数调用时可以看为是决定第一位上的数字,当决定好第一位时再从剩下的数字中决定第二位,依次递推。
接着我们看POJ 1564 Sum it up问题:
该问题要求首先输入总和total, 然后输入n个数,并打印出n个数中其中任意个数加起来等于total的加法表达式, 其中n个数中可以有重复。
该题目的思路是在全排列的基础上还要去重。
程序主体及变量解释:
#include<stdio.h>
#include<stdlib.h>
#define MAX 12
int total, n;
int flag;
int result[MAX];
int container[MAX];
int main(void){
int i;
scanf("%d%d", &total, &n);
int currentSum, index, step;
while(n != 0){
currentSum = 0;
index = 0;
step = 0;
flag = 0;
for(i = 0;i < n;i++){
scanf("%d", &container[i]);
}
printf("Sums of %d:\n", total);
DFS(index, step, currentSum);
if(flag == 0){
printf("NONE\n");
}
scanf("%d%d", &total, &n);
}
}
①flag哨兵标志着是否有解,无解则输出NONE。
②result数组存放输出结果,container数组存放n个数,这里为什么不需要全排列中的flag数组呢?因为n个数是按递减的顺序输入,求和的时候只需要考虑向后小的数,不会再加之前的数(之前的数要么已经跳过或已经加上)。
③index, step可参考全排列例子,currentSum 则代表当前DFS函数之前已经增加的数的和。
接下来给出DFS函数:
void DFS(int index, int step, int currentSum){
int i;
if(currentSum == total){
flag = 1;
printf("%d", result[0]);
for(i = 1;i < step;i++){
printf("+%d", result[i]);
}
printf("\n");
}
else{
if(currentSum < total && index < n){
for(i = index;i < n;i++){
if(i == index || container[i] != container[i - 1]){
result[step] = container[i];
DFS(i + 1, step + 1, currentSum + container[i]);
}
}
}
}
}
①首先当前DFS判断结束的条件是curretSum = total, 其次currentSum < total && index < n代表当前函数还没有结束,只要其中一个条件不满足则代表无解(即currentSum > total 或者 index >= n)。
②接着老规矩step代表当前result数组上的第几位要存放什么数,但是在循环条件中i == index为什么呢?这跟不用flag数组的原因是一样的,之前的数不用考虑,只需要跳转到第i层就可以了(也就是把第i层的数放在step的位置上),所以下一个DFS函数跳转的层数也是i + 1,而并非index + 1。
③我们还要求去重,举个例子4 6 4 3 2 2 1 1,result上0位放3,1位放1,但是1有两个就重复了。所以每到新的一层时,第一个元素肯定是可以放入step位置的,但接下来的每一个元素都要进行判断是否跟前一个元素相等,若相等,则放在同一个step上会有重复。
这是其中解决问题的一种思路,我们接下来给出另一种思路。
假设这n个数中通过currentSum中记录当前的和,那么对于每一个数我们有两种选择,加或者不加。
我们看核心的函数DFS:
void DFS(int currentSum, int index, int skip){
int i;
if(index == n + 1){
return ;
}
if(currentSum == total){
flag = 1;
for(i = 0;i < n;i++){
if(status[i]){
printf("%d", container[i]);
i++;
break;
}
}
for(;i < n;i++){
if(status[i]){
printf("+%d", container[i]);
}
}
printf("\n");
}
else if(currentSum < total){
if(skip != container[index]){
status[index] = 1;
DFS(currentSum + container[index], index + 1, -1);
}
status[index] = 0;
DFS(currentSum, index + 1, container[index]);
}
}
①index == n + 1因为最终判定结果在n + 1层,第n层判定的是上一个结果。
②skip作为标志意味着之前的数是否有跳过,当有跳过的时候保存上一个数,检查这一个位置是否放入相同的数。