算法设计与分析基础系列--生成排列与洗牌算法(一)

 转载文章,原文章来源于算法设计与分析基础系列--生成排列与洗牌算法(一) (欢迎关注微信公众号,会定期更新内容)

============================================================

前言

本文内容基于书籍"算法设计与分析基础"(Introduction to The Design and Analysis of Algorithms,作者Anany Levitin),主要学习和讨论其中的生成排列与洗牌算法。

问题描述

生成排列问题主要是给定从1到n的n个正整数,生成所有的排列结果,一共有n!种序列。而在此基础上,我们接着讨论洗牌算法。洗牌问题的一般描述是给定n张不同的牌,设计一个函数随机均匀地生成其中一种排列,即以概率1/n!生成其中一种排列结果,有时也会以面试题的形式出现。

解答

我们先来讨论生成排列问题。以n=3为例,我们需要生成所有3!=3*2*1=6种排列结果,包括(1,2,3), (1,3,2), (2,1,3), (2,3,1), (3,1,2), (3,2,1)。

我们可以这么来思考。首先,我们需要确定第一个位置放哪个数字,它可能是1,2或者3,有3种选择。当第一个位置确定后,我们需要继续确定第二个位置放哪个数字,它也可能是1,2或者3,但是必须要保证不能和第一个位置放置的数字重复,因此有2种选择,以此类推处理接下来的每个位置(对于n=3,只有最后一个位置了,它有1种选择),直到所有位置都确定了放置的数字。我们会发现,将每个位置(一共3个位置或者说3个步骤)的方案数乘起来,恰好就是3*2*1=6种结果,即覆盖了所有可能的排列。而我们的排列生成算法,就是基于枚举"每个步骤"的所有可能来实现的。

下面我们给出详细的过程。

Result A. 首先确定第一个位置,我们可以放置1,2或者3。假设我们本次决定放置1(步骤1)。接着我们确定第二个位置,它可以放置2或者3,假设我们本次决定放置2(步骤2)。接着我们确定第三个位置,它只能放置3了。此时我们已处理完所有位置,可以输出结果1,2,3。

Result B. 现在我们回看Result A的步骤2,在步骤2的时候,本来我们是可以选择2或者3的,而Result A只是"本次决定放置2"的结果,我们还要尝试"本次决定放置3"这个方案,如果在Result A的步骤2我们选择放置3,那么在第三个位置只能选择2了,此时我们也处理完了所有结果,输出1,3,2。

接着,我们再回看Result A和Result B的步骤1,这两个输出结果中,在步骤1的时候,我们都是"本次决定放置1",但是在步骤1(第一个位置),我们还可以尝试放置2或者3,那么如果我们在步骤1"本次决定放置2"的话会怎样呢?

Result C. 如果步骤1"本次决定放置2",那么在位置2,我们有两种选择1或者3,类似地,在步骤2我们"本次决定放置"1,那么第三个位置就只能放置3了,此时输出结果为2,1,3

Result D. 接着在Result C的步骤2中,我们"本次决定放置"3,那么第三个位置只能放置1,此时的结果为2,3,1

现在我们再次回到Result A和B之后的步骤1那里,在Result A和B中我们"本次决定放置1",在Result C和D中,我们"本次决定放置2",现在我们考虑"本次决定放置3"

Result E. 在步骤1中我们"本次决定放置"3,那么在步骤2中我们可以选择1或者2,假设我们"本次决定放置"1,那么第三个位置只能放置2了,此时结果为3,1,2

Result F. 在步骤1中我们"本次决定放置"3,那么在步骤2中假设此时我们"本次决定放置"2,那么第三个位置只能放置1了,此时结果为3,2,1

我们可以发现,上面的6种结果,恰好对应了所有的排列情况。而得到这些结果的过程可以描述如下。

我们首先确定第一个位置放哪个数字,会发现有多种可能,我们选择其中一种并进入第二个位置的处理步骤中,接着对于第二个位置我们继续从多种可能中选择其中一种,并进入下一个位置的处理中,直到我们处理完所有位置,输出结果。然后,我们"回溯"到最近的那个有多种选择的位置,做出另一个选择,接着再次进入到下一个位置继续处理,直到"每个位置的多种选择都被尝试了一次"停止。

如果大家还记得八皇后问题的话,会发现这个问题和八皇后非常相似,采用的都是"递归+回溯"的思想,可以参考本公众号下的这篇文章,“"剑指offer"中的经典算法面试题--八皇后问题”。

现在我们直接给出代码,代码中我们是从0开始

int a[n]; // 数组a包含数字从0到n-1 即a[i]=i

void solve(int pos, int n){ // pos表示当前处理的位置 n表示数组长度 即数字个数

    if(pos == n){ // 已经处理完所有位置 可以输出答案

        for(int i = 0; i < n; i++){ // 此时数组a装填的就是0到n-1的一种排列

             cout<<a[i]<<" ";

        }

        cout<<endl;

        return;

    }

    for(int i = pos; i < n; i++){ // 当前处理的位置是pos 由于不能和之前已经选择的数字重复 我们只能考虑后面的数字

         int tmp1 = a[pos];

         a[pos] = a[i];

         a[i] = tmp1; // 这3行代码是交换a[pos]和a[i] 表示在pos这个位置 我们"本次决定放置a[i]"

         solve(pos + 1, n); // 递归调用解决下一个位置pos+1

         int tmp2 = a[pos];

         a[pos] = a[i];

         a[i] = tmp2; // 这3行代码是再次交换a[pos]和a[i] 从而将"状态"恢复到"本次决定放置a[i]"之前的样子 在下一次for循环中我们即将"尝试"该位置pos的其他可以放置的数字

    }

}



int main(){

    int n = 5; // 初始化长度n

    for(int i = 0; i < n; i++){

         a[i] = i; // 初始化数组a[i]=i

    }

    solve(0, n); // 调用函数 先决定位置pos=0放置哪个数字

}

我们需要再次强调,在这类"递归+回溯"的算法中,"回溯"部分非常重要(有时也容易遗漏),在一些更复杂的问题中,"回溯"的处理也会更难处理一些。

关于洗牌算法,我们将会在下一篇文章中进行讨论和分析。

总结

1,排列生成算法是很多其他问题的一个基础,比如对于输入规模不大的旅行商问题,我们是可以利用排列生成直接暴力枚举最优解的。

2,递归+回溯,是一个非常重要且经典的思想,往往是很多复杂问题中的关键一步。

欢迎大家多多转载并关注后续更新,如果有其他算法相关的问题也欢迎留言讨论~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值