标签:
承接上文,这次以递推的思维,介绍组合学当中一个很经典的问题。
这个问题最开始由瑞士数学家欧拉提出,原始的问题被叫做“装信封问题”,问题的大意就是:有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)
读题可以看到,题目需要输出概率,分子显然是【全错位排列数】,而分母是所有可能情况,即是n的阶乘。
再看这个题.(Problem source : 2049)
可以看到这两个题目都是在全错位排列的基础上稍做了一些改动,这个题目需要我们输出所有情况数目。 显然为了完成这件事,先要找出那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)
这是一道很标准很基础的错排题目,只是在编写的时候,对于数据类型——到底用__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]); } }
标签:
原文地址:http://www.cnblogs.com/rhythmic/p/5503509.html