• 16.排列(深度搜索的详细记录)


      关于全排列的代码在网上收集、研究了好几种,包括我自己写的也有循环实现。循环是最容易理解的,按照判定条件进行嵌套,但缺点是,如果有十个八个数据,循环嵌套的层数太深,十分臃肿。很明显,如果一段代码不够简练,我自己也不满意,肯定想尽办法解决它。所以再次记录一下全排列的递归实现。

      还是按照自己的一套方法,以小见大,先不弄太多数据,在最小的范围内观察代码是如何运行的。在全排列的递归实现中,需要三个数组:
    1.源数据,也就是待排的元素,比如,{3,5,7,8....}等等,将之放在一个数组中。
    2.排好的数据。这里的排好数据是指,需要抽取几个,假如从源数据中抽取2个数据排列,这个排列的数组就定为2,因为只存放2个数据。
    3.状态数组,这个数组和源数据数组等长,因为,如果有n个数据,每个数据都可能被选中,所以和源数据数组等长,整个递归过程就依靠状态数组进行工作。
    代码如下:

    #include <iostream>
    using namespace std;
    int a[3];/*存放排列的数组*/
    int flag[3];/*状态数组*/
    int b[4]={0,3,5,8};/*源数据*/
    int n=3;/*源数据个数:3个*/
    void f(int m)
    {
    if(m==3) /*分配完毕*/
    {
        for(int j=1;j<3;j++)/*打印*/
        cout<<a[j];
        cout<<endl;
    }
    else
    for(int i=1;i<=n;i++)
        if(flag[i]==0)
        {
            a[m]=b[i];
            flag[i]=1;
            f(m+1);
            flag[i]=0;
        }
    }
    int main()
    {
    f(1);
    return 0;
    }

    现在详细分析这段代码:(所有下标都从1开始,便于分析
    为什么在代码中存在for循环,是因为,假如有n个数据,根据排列的常识,每个数据都能被选中,假如第一个元素先选中3,那么接下来的5和8具有同样的机会被选中在排列的第一个数。
    第一次,m=1进入,经过循环,i=1,然后调用f(2),这步关键是记住i这个值,它现在(停留)的位置=1.
    第二次,m=2进入,经过循环,i=2,同样,这步的关键依然是i,此时(停留)的位置=2。然后调用f(3),由于已到边界,所以打印数组,得到3,5,返回f(2),接着,设置状态数组中的flag[i]=0,也就是把5换掉,再接着循环,取源数据的第3个数据,再递归又到边界,于是打印数组,得到3,8...

    如果源数据后面还有数据,比如,9,10....等等,那么这种套路会一直持续下去,选中一个,舍弃一个,再去试下一个,如此反反复复,一直穷举到源数据的最后一个数据为止。

    假如穷举完成,又退回到f(1)的时候,则它曾经选中过的所有数据都会置为0,这个容易看出来。也就是退回到f(1)的时候,状态数组就是:1 0 0。
    这就说明,在确定好第一个数据之后,穷举它的下一个数据的所有可能,变成模型就是:假如要选取2个元素,最先被穷举所有可能的就是它的最末位,接着往前推。
    模型1就说明了这个问题,(其实很多奇怪的灵感是在写博客的时候不知不觉捕捉到的,所以写作实在很重要!)就比如这会,经过画图之后,突然就觉得这个递归和for循环极其相像!就按for循环的思路去析:

    /*for循环的套路*/
    for(i=1;i<=n;i++)
    {
        for(j=1;j<=n;j++)
            {
                for(....)
    .....
    }

    也就是先选取第一个元素,内部再依次嵌套内循环,再确定第二个数,第三个数...等等,区别在于,for循环是用内部的判定条件来约束,比如,第二个不等于第一个,第三个不等于第二个和第一个...所以经常有人说,递归和循环是可以相互转化的!
    根据这样一分析,其实就比较明朗了,至少知道这个模型是如何工作的。现在这个例子只取两个数,把代码范围大幅度的减少,看的清楚了,如果数据很多呢,按照这种类型推理是一样的。
    例如:
    源数据={3,5,7,8,9,6},选取三个元素排列,根据分析,它的流程是:
    最先确定第一个,第二个,然后穷举第三个,得到:3,5,7 | 3,5,8|3,5,9|3,5,6|,第三个穷举完毕。
    再回退第二个,由于已经取了5,下一个轮到7,于是,再接着穷举第三位,得到:3,7,5|3,7,8|3,7,9|....第三个穷举完毕。又回到第二个,再穷举第三个....
    等到第二个穷举完全之后,又回到第一个,轮到5为第一位,接着再确定第二位,然后再穷举第三位,反反复复,依此类推,最终完成整个排列过程!


    根据上面的分析,终于看出如何在递归和循环中进行转化,先以for循环嵌套为例:

    for(..)
    {
        for(...)
        {
            for(..)
        ..
    }

    这是一种常见的循环嵌套,一层套一层,如何转化成为递归呢,根据上面的例子,很容易看出来,只要递归函数中有一个for循环就行,这样就把多重循环的嵌套压缩成只有一个for循环的递归形式:

    void f( )
    {
            ..
            for( )
            { }
    ..
    }

    for循环的最尾端判断,在递归函数中被提到最前面。
    从这个图可以看出来,原来递归只是在形式上做了一点“迷惑”,经过展开,原来是等价的!

    那两者的区别是什么呢?进一步挖掘。
    在for循环中,可以根据约束条件进行判定,比如,第二个数不等于第一个数,第三个数不等于第二个数和第一个数。。但是在递归中,可能无法使用这种判定语句,为什么无法使用呢?
    比如说,在第二个for中有它的判定规则,在第三个for中有另一个判定规则,现在递归压缩成一个for,就无法把这些规则融入到一个for循环中,于是,借用了一个状态数组,根据这个数组的状态进行操作。所以,嵌套for循环的判定规则等价于状态数组的使用。

    问题提出:
    有什么办法不借用额外数组就将排列打印出来?

  • 相关阅读:
    QSet使用及Qt自定义类型使用QHash等算法
    QQueue与QStack使用
    QHash和QMultiHash使用
    【洛谷6633】[ZJOI2020] 抽卡(多项式毒瘤题)
    【洛谷5996】[PA2014] Muzeum(模拟费用流)
    【CF1063F】String Journey(后缀自动机+线段树)
    【BZOJ3640】JC的小苹果(高斯消元)
    【洛谷6478】[NOI Online #2 提高组] 游戏(树形DP+二项式反演)
    【洛谷6730】[WC2020] 猜数游戏(数论)
    【洛谷6186】[NOI Online #1 提高组] 冒泡排序(树状数组)
  • 原文地址:https://www.cnblogs.com/tinaluo/p/5338929.html
Copyright © 2020-2023  润新知