··前言:

DFS作为蓝桥杯等大赛的必备算法,是必须要掌握和学会灵活应用的

··问题形象化(图片来自《啊哈!算法》):

假如有编号为1、2、3的3张扑克牌和编号为1、2、3的3个盒子,现在我们想把这3张扑克牌分别放进3个盒子里面,并且每个盒子有且只能放一张扑克牌,那么一共有多少中不同的方法?

首先,我们手拿三张扑克牌,走到了1号盒子前,我们心想:放1号扑克牌,还是2号、3号呢?现在我们想生成全排列,所以肯定要将三种情况全部尝试一次。那么现在我们约定一个顺序,每次到一个盒子面前,都先放1号,再放2号,最后放3号,所以我们现在把1号扑克牌放进第一个盒子中
放好之后,我们走到第二个盒子面前,现在手里剩下2、3号,按照以前的约定顺序,我们将2号扑克牌放进2号盒子中,接着我们来到3号盒子面前
现在我们手上只剩下3号扑克牌了,于是只能将手中的3号放入其中,放好后,我们再往后走一步,走到了第4号盒子(实际上没有),这时我们手中已经没有扑克牌了,现在,观察前三个盒子,成功的生成了一组全排列,即“1 2 3”.
但是我们不可能由此结束,我们还要回到3号盒子面前,收回3号扑克牌,但是因为我们手中除了3号已经没有其他扑克牌了,所以我们还要往前走一个箱子,来到了2号前。
我们收回2号扑克牌,现在,我们手中有2、3号扑克牌了,按照约定的顺序,我们把3号扑克牌放入其中,放完后我们再次向后走一步,来到3号箱子前。
此时我们手中只剩下2号扑克牌了,我们将2号扑克牌放进3号箱子中,放完后向后走一部,来到第4号盒子前(实际上没有)
此时,我们再次成功的生成了一组排列,即“1 3 2”.
我们如果多次重复上面的步骤,先返回到3号,取回2号卡片,再返回到2号,取回3号,再返回到1号,取回1号,再前进放入,再返回。。。。如此往复,我们可以生成所有的排列可能。
由此我们可以写出如下代码

#include<stdio.h>
int box[20 + 5], book[20 + 5],n;//box是箱子,book用于储存是否卡牌被使用
void dfs(int step);
int main() {
	//实现1~n的全排列
	scanf("%d", &n);
	dfs(1);
	return 0;
}
void dfs(int step) {
	if (step == n + 1) {//临界条件,此时我们已经走到了第n+1个盒子前
		int i;
		printf("%d", box[1]);
		for (i = 2; i <= n; i++) {
			printf(" %d", box[i]);
		}
		printf("\n");
		return;
	}
	//现在站在第step个箱子前
	int i;
	for (i = 1; i <= n; i++) {//逐个遍历每张卡片
		if (book[i] == 0) {//如果这张卡片没有被使用过
			box[step] = i;
			book[i] = 1;
			dfs(step + 1);
			book[i] = 0;//递归放完卡片之后,要将卡片恢复为未使用状态,以便后续操作
		}
	}
	return;
}

下面我们分析一道变式题目:
【递归入门】组合的输出
[命题人 : 外部导入]
时间限制 : 1.000 sec 内存限制 : 128 MB

解决: 728
提交: 728
统计
题目描述
排列与组合是常用的数学方法,其中组合就是从n个元素中抽出r个元素(不分顺序且r < = n),我们可以简单地将n个元素理解为自然数1,2,…,n,从中任取r个数。
现要求你不用递归的方法输出所有组合。
例如n = 5 ,r = 3 ,所有组合为:
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5
输入
一行两个自然数n、r ( 1 < n < 21,1 < = r < = n )。
输出
所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,所有的组合也按字典顺序。

问题分析:这道题目和上面例题的不同之处在于,该题要求从n个数中取出r个数,而不是n个数的全排列,并且还有一个要点,在于输出的元素按由小到大排列,所有的组合还要按照字典顺序

#include<iostream>
using namespace std;
int a[21+5], b[21+5] = { 0 }, n, r;//a是箱子,b是信号量判断是否已经被使用
void dfs(int step) {
    int i, j;
    if (step == r + 1) {//只允许放r个盒子,step>r就结束放盒子,开始输出r个数据
        for (i = 1; i <= r; i++) {
            printf("%d ", a[i]);
        }
        printf("\n");
        return;
    }
    for (i = a[step - 1]; i <= n; i++) {//i每次都以前一个盒子里数为开端开始遍历,这样避免从1开始遍历,导致后面盒子的数比前面盒子的数小
        if (b[i] == 0) {
            b[i] = 1;
            a[step] = i;
            dfs(step + 1);
            b[i] = 0;
        }
    }
    return;
}
int main() {
    scanf("%d%d", &n, &r);
    a[0] = 1;//配合上面的a[step-1],初次要从1号卡牌开始遍历
    dfs(1);
    return 0;
}

两个关键点阐释:

1.对于i=a[step-1]的理解
我的观点如下:
假设有5个数,要取出3个数组成全排列,第一次进入循环,肯定是按照1、2、3的顺序放进盒子,待返回时,取出3,这时比3大的有4、5,将4放进去,取出来,放入5,再返回到2号前,放入3.。。。对于这一趟循环,肯定时满足从小到大的。但是如果我们走到1号盒子前,把1号卡牌取出,紧接着把2号放进去,下一步走到2号盒子前,假设i还是从1开始的,我们会把1号卡牌放进去,这样就违背了从小到大的原则了,所以我们应该是从3开始,保证箱子里的数字从左到右都是从小到大排列的。
2. 如何保证是只输出r个,组成一次排列
这里我们只安排r个盒子就可以了,r个盒子一旦装满,即step=r+1,我们就可以输出当前序列了。因此正确做法是修改临界条件