《程序设计中的组合数学》——全错位排列
承接上文,这次以递推的思维,介绍组合学当中一个很经典的问题。
这个问题最开始由瑞士数学家欧拉提出,原始的问题被叫做“装信封问题”,问题的大意就是:有n封信和n封它们各自对应的信封,如果邮递员想要把每封信都放在不属于这封信的信封,那么请问有多少种排法。(这邮递员真无聊)
想必这个问题在中学阶段数学的【排列组合】都有过接触,但是我记忆非常深刻的是,老师讲到这个模型,自己找了一下n = 5的情况就停止了,然后让大家把前面的数字序列背下来。今日故地重游不禁觉得老师教的好坑爹,搞学习还是要亲历亲为自主探究。
虽然这个问题的排列数有一个很长的通式,但是想用计算机编程实现,必须要把它简化成可以一步一步执行的递推式。
假设b被放到了A当中。
情况1:a在B中。
这种情况下,剩下的(n-2)个球的排列就已经和a、b、A、B没有关系,这种情况的排列数也就是n-2时候的【全错位排列数】
情况2:a不在B中。
这种情况下,就要完成a,c,d,e……与B,C,D,E......的错位排列。而基于a不在B中的条件,可以把a和B看成【对应】信封,这样就相当于是n-1封信进行【全错位排列】。
以上是b在A中的全错排列数,即f(n-1)+f(n-2)。而b可以放在剩除了B以外的任何一个信封当中,因此得到“装信封问题”中,n封信的全错位排列数的递推式是: f(n) = (n-1)*[f(n-1) + f(n-2)](n > 2)
有了【全错位排列】这个模型,就可以很轻松的解决以下问题了。(Problem source : hdu 2048)
首先,所有参加晚会的人员都将一张写有自己名字的字条放入抽奖箱中; 然后,待所有字条加入完毕,每人从箱中取一个字条; 最后,如果取得的字条上写的就是自己的名字,那么“恭喜你,中奖了!”
大家可以想象一下当时的气氛之热烈,毕竟中奖者的奖品是大家梦寐以求的Twins签名照呀!不过,正如所有试图设计的喜剧往往以悲剧结尾,这次抽奖活动最后竟然没有一个人中奖!
我的神、上帝以及老天爷呀,怎么会这样呢?
不过,先不要激动,现在问题来了,你能计算一下发生这种情况的概率吗?
不会算?难道你也想以悲剧结尾?!
读题可以看到,题目需要输出概率,分子显然是【全错位排列数】,而分母是所有可能情况,即是n的阶乘。
再看这个题.(Problem source : 2049)
首先,给每位新娘打扮得几乎一模一样,并盖上大大的红盖头随机坐成一排; 然后,让各位新郎寻找自己的新娘.每人只准找一个,并且不允许多人找一个. 最后,揭开盖头,如果找错了对象就要当众跪搓衣板...
看来做新郎也不是容易的事情...
假设一共有N对新婚夫妇,其中有M个新郎找错了新娘,求发生这种情况一共有多少种可能.
可以看到这两个题目都是在全错位排列的基础上稍做了一些改动,这个题目需要我们输出所有情况数目。 显然为了完成这件事,先要找出那M个找错的悲催新郎,得到一个组合数,然后再乘以(这里涉及【组合学】里一个简单的分步乘法原理)那M个人的【全错排列数】即可得到答案。而至于得到那个组合数,设计两个循环分别得到分子和分母再相除即可。
再看一个应用稍微灵活的有关错排的题目。 (Problem source : hdu 2068)
数理分析:这道题乍一看好像和错排没什么关系,因为题目中提到只要答对一半或以上即可,好像和错排沾不上边。但是仔细分析一下会发现联系。我们从答对一半(m,对于奇偶的分析是后话),我们要从n个里面选出m,然后乘以剩下的n-m个元素的全错排,就是答对m个的所有总数,然后m+1,依次计算,便可以得到最终的结果。
编程实现方面也是比较简单,需要打一个错排的表然后再写有个计算组合数的函数,这里写计算组合数的函数有一个技巧是变量都要用double,否则会出现精度上的错误。另外值得注意的一点是,这里最多有25个元素,错排最多也就是12个元素,所以打错排的表的时候,数组不必开太大。
代码如下。
#include<stdio.h> double a[15]; double Con(int m , int n) { double ret = 1 , i; for(i = 0;i < m;i++) ret *= (n - i)/(m - i); return ret; } void make_list() { int i; a[1] = 0; a[2] = 1; for(i = 3;i <= 15;i++) { a[i] = (i - 1)*(a[i - 1] + a[i - 2]); //printf("%.0lf\n",a[i]); } } int main() { double ans; int n , m ,i; make_list(); while(~scanf("%d",&n) , n) { ans = 0; if(n%2 == 0) m = n / 2; else m = n / 2 + 1; for(i = m;i < n;i++) ans += Con(i , n)*a[n - i]; printf("%.0lf\n",ans + 1); } }
再看一道用到全错位排列公式的简单题目。(Problem source : 1465)
不幸的是,这种小概率事件又发生了,而且就在我们身边: 事情是这样的——HDU有个网名叫做8006的男性同学,结交网友无数,最近该同学玩起了浪漫,同时给n个网友每人写了一封信,这都没什么,要命的是,他竟然把所有的信都装错了信封!注意了,是全部装错哟!
现在的问题是:请大家帮可怜的8006同学计算一下,一共有多少种可能的错误方式呢?
这是一道很标准很基础的错排题目,只是在编写的时候,对于数据类型——到底用__int64还是double,好像对于不同的题目有不同的限制,不过这是oj的问题了。
简单的代码如下。
#include<stdio.h> __int64 a[25]; void make_list() { a[1] = 0 , a[2] = 1; int i; for(i = 3;i <= 20;i++) a[i] = (i - 1)*(a[i - 2] + a[i - 1]); } int main() { int n; make_list(); while(scanf("%d",&n) != EOF) printf("%I64d\n",a[n]); }
再来看一道有关错排的简单题目。(Problem source : hdu 4534)
这道题目在错排的基础上,加入的求余处理。我们可以想象,根据错排的递推式,打表循环几次就可以把__int64给打爆,因此这里题目要求进行求余处理。 在算法实现上,我们可以先求出f[i - 1] + f[i - 2],然后求一步余,然后再乘以(i - 1),再求余,这种分步求余的方式能够进一步的防止数据的溢出。
代码如下:
#include<stdio.h> __int64 a[105]; const int MOD = 1000000007; void make_list() { a[1] = 0; a[2] = 1; int i; for(i = 3;i <= 100;i++) { a[i] = a[i - 1] + a[i - 2]; a[i] %= MOD; a[i] *= i - 1; a[i] %= MOD; } } int main() { make_list(); int n , T; scanf("%d",&T); while(T--) { scanf("%d",&n); printf("%I64d\n",a[n]); } }