Chinaunix首页 | 论坛 | 博客
  • 博客访问: 36823
  • 博文数量: 3
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 94
  • 用 户 组: 普通用户
  • 注册时间: 2013-11-24 13:07
个人简介

玩中学。。

文章分类
文章存档

2014年(2)

2013年(1)

我的朋友

分类: C/C++

2014-04-16 14:29:40

       最近在看具体数学,再次碰见了我们的老朋友 --- 约瑟夫环问题,通过对其的较为深度的研究,发现了许多有意思的东西,所以写出来分享一下。
       问题描述:
            约瑟夫环问题(Josephus)       
            约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。我们要找出最后一个出列的人的编号。

        看起来百无聊赖的问题,却发现了几个不同的解法,有通用解法,有特殊解法,还有令我感觉十分舒爽的奇思妙想解法(虽然有些局限,但是个人还是比较喜欢)。废话不多说,下面小菜把几种解法一一分享出来。 

        方法一:构建一个可循环的数据结构,例如循环数组,循环链表。从头部遍历,计数到第m个数时,踢出该元素,继续循环,直至剩下最后一个元素,输出最后一个元素的编号。
        下面是我用循环链表实现的代码,对于循环数组,比较简单,就不做解释了。 
        

点击(此处)折叠或打开

  1. /*
  2. *     约瑟夫环问题(Josephus)
  3. *     Code by Boy Yue 2014.4.15
  4. *     http://blog.chinaunix.net/uid/29341537.html
  5. */
  6. #include <stdio.h>
  7. #include <malloc.h>
  8. #include <stdlib.h>

  9. //链表节点
  10. typedef struct Linked
  11. {
  12.     int position;
  13.     struct Linked *next;
  14. }LinkNode, *LinkList;

  15. //创建节点并初始化
  16. LinkNode *creatNode(int const number);
  17. //创建约瑟夫环链表
  18. LinkList creatList(int const count);
  19. //循环运算
  20. LinkNode *dealJosephus(LinkList list, int const key);
  21. //当前节点删除
  22. void deleteNode(LinkNode *node);
  23. //输入规模及key值
  24. void input(int *count, int *key);
  25. //显示胜出者
  26. void showWinner(LinkNode *node);

  27. LinkNode *creatNode(int const number)
  28. {
  29.     LinkNode *node = NULL;

  30.     node = (LinkNode *)malloc(sizeof (LinkNode));
  31.     //初始化
  32.     node -> position = number;
  33.     node -> next = NULL;

  34.     return node;
  35. }

  36. LinkList creatList(int const count)
  37. {
  38.     int num = count;
  39.     LinkList jose_list = NULL;
  40.     LinkNode *head = NULL, *node = NULL;
  41.     
  42.     head = jose_list = creatNode(count - num + 1);

  43.     // 创建循环链表
  44.     while (--num)    {
  45.         node = creatNode(count - num + 1);
  46.         head -> next = node;
  47.         head = node;
  48.     }
  49.     
  50.     //构成环形
  51.     head -> next = jose_list;

  52.     return jose_list;
  53. }

  54. void deleteNode(LinkNode *node)
  55. {
  56.     LinkNode *node_next = node -> next;

  57.     *node = *node_next;
  58.     //将后一个节点的数据复制到当前节点并删除后一个节点,使时间复杂度为o(1)
  59.     free(node_next);
  60. }

  61. LinkNode *dealJosephus(LinkList list, int const key)
  62. {
  63.     int num = key;
  64.     LinkNode *node = list;

  65.     //当循环链表只有一个节点时返回当前节点
  66.     if (node -> next == node)
  67.         return node;

  68.     while (--num)
  69.         node = node -> next;

  70.     //showWinner(node);

  71.     deleteNode(node);

  72.     //递归调用
  73.     return dealJosephus(node, key);
  74. }

  75. void input(int *count, int *key)
  76. {
  77.     printf ("Person count = ");
  78.     scanf ("%d", count);
  79.     printf ("Key number = ");
  80.     scanf ("%d", key);

  81.     //判断键入的值是否合法
  82.     if (*count <= 0 || *key <= 0)    {
  83.         printf ("Input Error!\n");
  84.         system("pause");
  85.         exit(0);
  86.     }
  87. }

  88. //显示当前节点信息
  89. void showWinner(LinkNode *node)
  90. {
  91.     printf ("The Winner is number %d\n", node -> position);
  92. }

  93. int main()
  94. {
  95.     int count, key;

  96.     input(&count, &key);
  97.     showWinner(dealJosephus(creatList(count), key));

  98.     return 0;
  99. }
            
             方法二:有点动态规划的感觉,不过是数学的递推关系式。想要解决数据规模为n的时候的问题,先解决数据规模为n-1的问题...(小菜认为这应该就是此方法的解题思想吧)
            仔细想一下,假如当前数据规模为n,并且我们已经得到了在此数据规模下第一个出列的人的编号(m%n-1)。那么当第一个人出列后。剩下的n-1个人又组成了一个新的约瑟夫环,且编号是从m%n开始。
            即(m%n),(m%n+1), ... n,... n-2, n-1, 1, 2, 3,..., m%n-2
                        0                     1           n-m%n  m%n-2 ...............................     n-2 
            胜利者Win = (Win+m%n)%n

            得到递推式:    a[1] = 0
                                     a[k] = (a[k-1] + m) % n
            通过递推式,我们很容易写出代码:

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <stdlib.h>

  3. void input(int *count, int *key);
  4. void dealJosephus(int const count, int const key);

  5. void input(int *count, int *key)
  6. {
  7.     printf ("Person count = ");
  8.     scanf ("%d", count);
  9.     printf ("Key number = ");
  10.     scanf ("%d", key);

  11.     //判断键入的值是否合法
  12.     if (*count <= 0 || *key <= 0) {
  13.         printf ("Input Error!\n");
  14.         system("pause");
  15.         exit(0);
  16.     }
  17. }

  18. void dealJosephus(int const count, int const key)
  19. {
  20.     int winner = 0;
  21.     int num;

  22.     for (num = 2; num <= count; num++)
  23.         winner = (winner + key) % num;

  24.     printf ("The Winner is %d\n", winner+1);
  25. }

  26. int main()
  27. {
  28.     int count, key;
  29.     
  30.     input (&count, &key);
  31.     dealJosephus(count, key);

  32.     return 0;
  33. }
            显然,上面这段代码的时间复杂度要比第一种方法低的多。第一种方法不仅写起来麻烦,而且时间复杂度为O(nm),而这种方法的时间复杂度仅仅为O(n)。效率上有很大的提升。

            方法三:此方法比较特殊,首先我们先针对m = 2时的情况进行讨论
            例如n=10为起始。消去的顺序为2,4,6,8,10,3,7,1,9,于是5为胜利者。
            问题:确定幸存者的号码J(n)。
            假设开始有2n个人。经过第一轮后剩下1, 3, 5, 7, ... , 2n-3, 2n-1。
            而3号就是下一个要离开的人,除了每个人的号码加倍并减去1之外,这刚好跟n个人开始时的情景类似。也就是说我们可以找到递推式J(2n) = 2J(n) - 1, n >= 1。
            现在可以过渡到大的n。例如,已知J(10) = 5,所以J(20) = 2J(10) - 1 = 9
            类似的有J(40) = 17,并且我们可以推出J(5 * 2m) = 2m+1 + 1
            同样的对于奇数时的情形,我们可以得到递推式 J(2n+1) = 2J(n) + 1, n >= 1
            把方程组合起来得到递推式    J(1) = 1
                                                             J(2n) = 2J(n) - 1 ,n >= 1
                                                             J(2n+1) = 2J(n) + 1, n >= 1
                这种方法相比于方法二的由J(n-1)推出J(n)
明显要“高效”的多, 因为每次调用它的时候,它都是缩减2倍。因此相比于方法二的O(n)的时间复杂度,方法三的O(log(n))貌似更加快速。
                    但是有没有更加简单的方法了呢?
            上面我们得到了递推式。但是我们仍然需要一个封闭的形式。因为封闭的形式计算起来更加方便快捷。
            如果我们将n写成n = 2k + l的形式,其中k是不超过n的2的最大幂的指数,而l是指剩下的数。
            那么递归式的解看起来是 J(2m + l) = 2l + 1, m >= 0, 0  <= l <= 2m
                
我们需要给出这一解析式的证明。很简单,数学归纳法。这里小菜就不再细说了。

            我们在求解的过程中发现2的幂起到了重要的作用,因此我们来看看能不能从2的幂找到突破口。
                假设n的二进制的展开式是:
                                n = (bm bm-1 ··· b1 b0)2
        
也就是说
                                n = bm2+ bm-12m-1 + ··· + b0
         
其中每个 b为0或者1,而首位数字bm是1。所以我们有:
                                n = (1 bm-1 ··· bb0)2
                                           
l = (0 bm-1 ··· bb0)2
                               2 l = (bm-1 ··· bb0 0)2
                                        J(n) = 2 l + 1  = 
 (bm-1 ··· bb0 1)2
       我们惊奇的发现n的二进制向左循环移动一位就得到了J(n)

     
        同样如果是m = 3时,只要转换成三进制就行呢!依次类推、。。
      
 实在是不可思议!
        例如如果计算 n = 100 = (1100100)2,那么J(n) = (1001001)2= 73.
       不过这种方式有些缺陷。比如当n = 13 时,J(13) = J((1101)) = (111)2 = 7并不是正确结果。
     当0为首位时它会消失,导致结果错误。对于其详细的解释以及这种方法的改进推广延伸,有兴趣的话,小伙伴们可以自己研究研究,具体数学中也有较为详细的解释。
        
                
            

                参考书籍:具体数学 ----计算机科学基础(第二版)




阅读(3487) | 评论(0) | 转发(0) |
1

上一篇:java的main函数为什么没有返回值

下一篇:没有了

给主人留下些什么吧!~~