《程序设计中的组合数学》——全错位排列

   承接上文,这次以递推的思维,介绍组合学当中一个很经典的问题。

   这个问题最开始由瑞士数学家欧拉提出,原始的问题被叫做“装信封问题”,问题的大意就是:有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)

  

Problem Description
HDU 2006'10 ACM contest的颁奖晚会隆重开始了! 为了活跃气氛,组织者举行了一个别开生面、奖品丰厚的抽奖活动,这个活动的具体要求是这样的:
首先,所有参加晚会的人员都将一张写有自己名字的字条放入抽奖箱中; 然后,待所有字条加入完毕,每人从箱中取一个字条; 最后,如果取得的字条上写的就是自己的名字,那么“恭喜你,中奖了!”
大家可以想象一下当时的气氛之热烈,毕竟中奖者的奖品是大家梦寐以求的Twins签名照呀!不过,正如所有试图设计的喜剧往往以悲剧结尾,这次抽奖活动最后竟然没有一个人中奖!
我的神、上帝以及老天爷呀,怎么会这样呢?
不过,先不要激动,现在问题来了,你能计算一下发生这种情况的概率吗?
不会算?难道你也想以悲剧结尾?!

  读题可以看到,题目需要输出概率,分子显然是【全错位排列数】,而分母是所有可能情况,即是n的阶乘。



  再看这个题.(Problem source : 2049)

 

Problem Description
国庆期间,省城HZ刚刚举行了一场盛大的集体婚礼,为了使婚礼进行的丰富一些,司仪临时想出了有一个有意思的节目,叫做"考新郎",具体的操作是这样的:
首先,给每位新娘打扮得几乎一模一样,并盖上大大的红盖头随机坐成一排; 然后,让各位新郎寻找自己的新娘.每人只准找一个,并且不允许多人找一个. 最后,揭开盖头,如果找错了对象就要当众跪搓衣板...
看来做新郎也不是容易的事情...
假设一共有N对新婚夫妇,其中有M个新郎找错了新娘,求发生这种情况一共有多少种可能.

  可以看到这两个题目都是在全错位排列的基础上稍做了一些改动,这个题目需要我们输出所有情况数目。   显然为了完成这件事,先要找出那M个找错的悲催新郎,得到一个组合数,然后再乘以(这里涉及【组合学】里一个简单的分步乘法原理)那M个人的【全错排列数】即可得到答案。而至于得到那个组合数,设计两个循环分别得到分子和分母再相除即可。

 

 

    再看一个应用稍微灵活的有关错排的题目。  (Problem source : hdu 2068)

 

Problem Description
今年暑假杭电ACM集训队第一次组成女生队,其中有一队叫RPG,但做为集训队成员之一的野骆驼竟然不知道RPG三个人具体是谁谁。RPG给他机会让他猜猜,第一次猜:R是公主,P是草儿,G是月野兔;第二次猜:R是草儿,P是月野兔,G是公主;第三次猜:R是草儿,P是公主,G是月野兔;......可怜的野骆驼第六次终于把RPG分清楚了。由于RPG的带动,做ACM的女生越来越多,我们的野骆驼想都知道她们,可现在有N多人,他要猜的次数可就多了,为了不为难野骆驼,女生们只要求他答对一半或以上就算过关,请问有多少组答案能使他顺利过关。

  数理分析:这道题乍一看好像和错排没什么关系,因为题目中提到只要答对一半或以上即可,好像和错排沾不上边。但是仔细分析一下会发现联系。我们从答对一半(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)

 

Problem Description
大家常常感慨,要做好一件事情真的不容易,确实,失败比成功容易多了! 做好“一件”事情尚且不易,若想永远成功而总从不失败,那更是难上加难了,就像花钱总是比挣钱容易的道理一样。 话虽这样说,我还是要告诉大家,要想失败到一定程度也是不容易的。比如,我高中的时候,就有一个神奇的女生,在英语考试的时候,竟然把40个单项选择题全部做错了!大家都学过概率论,应该知道出现这种情况的概率,所以至今我都觉得这是一件神奇的事情。如果套用一句经典的评语,我们可以这样总结:一个人做错一道选择题并不难,难的是全部做错,一个不对。
不幸的是,这种小概率事件又发生了,而且就在我们身边: 事情是这样的——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)

Problem Description
  吉哥还是那个吉哥   那个江湖人称“叽叽哥”的基哥      每当节日来临,女友众多的叽叽哥总是能从全国各地的女友那里收到各种礼物。   有礼物收到当然值得高兴,但回礼确是件麻烦的事!   无论多麻烦,总不好意思收礼而不回礼,那也不是叽叽哥的风格。      现在,即爱面子又抠门的叽叽哥想出了一个绝妙的好办法:他准备将各个女友送来的礼物合理分配,再回送不同女友,这样就不用再花钱买礼物了!      假设叽叽哥的n个女友每人送他一个礼物(每个人送的礼物都不相同),现在他需要合理安排,再回送每个女友一份礼物,重点是,回送的礼物不能是这个女友之前送他的那个礼物,不然,叽叽哥可就摊上事了,摊上大事了......      现在,叽叽哥想知道总共有多少种满足条件的回送礼物方案呢?

  这道题目在错排的基础上,加入的求余处理。我们可以想象,根据错排的递推式,打表循环几次就可以把__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]);
         }
  }

 

  

posted on 2016-05-17 22:40  在苏州的城边  阅读(1847)  评论(0编辑  收藏  举报

导航