Problem A: 【递归入门】全排列

Description

     排列与组合是常用的数学方法。
先给一个正整数 ( 1 < = n < = 10 )
例如n=3,所有组合,并且按字典序输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1 

Input

输入一个整数n(  1<=n<=10)

Output

输出所有全排列

每个全排列一行,相邻两个数用空格隔开(最后一个数后面没有空格)

Sample Input Copy

3

Sample Output Copy

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

参考链接:问题 A: 【递归入门】全排列

最终AC代码:

#include <cstdio>
#include <cstring>
const int MAX=15;
int n, p[MAX];
bool vis[MAX]={false};
//这应该是一个经典的排列问题了 出现了很多次了!!! 
void generateP(int index){
    if(index == n+1){ //表明已经已经有 n 个数了 
        for(int i=1; i<=n; i++){
            printf("%d", p[i]);
            printf(i<n?" ":"
");
        }
        return ;
    }
    for(int i=1; i<=n; i++){ //这里的逻辑要理解! 
        if(!vis[i]){
            p[index] = i; //这里不是 p[i]=x;!!! 
            vis[i] = true;
            generateP(index + 1); //生成后一个 
            vis[i] = false;
        }
    }
}
int main(){
    while(~scanf("%d", &n)) generateP(1);
    return 0;
}

总结:主要是对于生成的逻辑理解,即一下代码:

for(int i=1; i<=n; i++){ //这里的逻辑要理解! 
    if(!vis[i]){
        p[index] = i; //这里不是 p[i]=x;!!! 
        vis[i] = true;
        generateP(index + 1); //生成后一个 
        vis[i] = false;
    }
}

我的理解(可能有不足)是:标号1~n共n个数的全排列,即要每次找到n个排列的数(每次还不能相同),并且将这些数输出。如何找呢?

第一,一次全排列中,每个数只能出现一次,那么就用标记数组记录在当前排列趟数中,知道有哪些数已经使用了。

第二,两趟排列中,只要任意两个位置数不同,那么就视为两种排列。换句话,就是标号为1~n的数字可以出现在任意位置上。

第三,必须保证本趟排列与之前输出的排列不同。

第二、三条件的限制比较难理解,也是我每次不知如何下手的根结所在。现在仔细想想,是不是可以这么理解:因为递归的性质,每一趟递归都可能在递归函数后面有很多未处理的逻辑,在本趟递归没完成前,我只专注地往更深层的递归走下去,直到达到递归边界。于是,可以将从递归开始到达到递归边界记录的排列,在达到递归边界时输出,然后再往上返回。

但是,不是每次返回都从边界层返回到开始层。每回到上一层,别忘了此时可能还有其他未处理的代码,也就是前面递归遇到的未处理的逻辑。于是程序顺序执行,把当前层处理逻辑处理完,此时可能再次往更深层逻辑递归,而不是直接往上层返回(for循环可能多次满足条件,从而多次触发递归)。

有意思的是在本程序代码中,本层深度的递归,其实就对应着p[]数组的第index个数据的生成。前面已经分析,任意一个数字可以出现在范围内的任意位置,换种理解,不就是任意位置可以是数字1~n中的某位(只要不重复出现,而变量i在循环增加,其实就保证了这个条件)。那么,可以在递归从深层返回到本层后,添加这个逻辑。

再仔细想想,每次递归前标记,递归后解除标记,便保证了每一趟排列中不会出现两个一样的数(当然,希望以后如果遇到类似条件,能够想起这点),于是第三个条件也满足了。

嗯,虽然只有几行代码,但是对于初学者来说,这逻辑真的比较难理解。此外,从理解这层逻辑到编写出正确、精简的代码,还是有很长一段路程的。所以,好好刷题,熟能生巧嘛~加油!

原文地址:https://www.cnblogs.com/heyour/p/12639045.html