zoukankan      html  css  js  c++  java
  • 约瑟夫环问题小结

    一 问题描述

    约瑟夫环问题的基本描述如下:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为1的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,要求找到最后一个出列的人或者模拟这个过程。

    二 问题解法

    在解决这个问题之前,首先我们对人物进行虚拟编号,即相当于从0开始把人物重新进行编号,即用0,1,2,3,...n-1来表示人物的编号,最后返回的编号结果加上1,就是原问题的解(为什么这么做呢,下文有解释)。而关于该问题的解通常有两种方法:

    1.利用循环链表或者数组来模拟整个过程。

    具体来讲,整个过程很明显就可以看成是一个循环链表删除节点的问题。当然,我们也可以用数组来代替循环链表来模拟整个计数以及出列的过程。此处只给出利用数组来模拟这个过程的解法,最终结果为最后一个出列的人的编号:

    #include<iostream>
    #include<unordered_map>
    #include<queue>
    #include<cstring>
    #include<cstdlib>
    #include<cmath>
    #include<algorithm>
    #include<sstream>
    #include<set>
    #include<map>
    using namespace std;
    
    
    int main()
    {
      int n,m;
      cin>>n>>m;
      vector<int>rs(n);
      for(int i = 0 ; i < n; i++)
        rs[i] = i + 1;//对人物重新进行编号,从0开始
      int cur_index = 0;//当前圆桌状态下的出列人的编号
      int out_cnt = 0;//用以表示出列的人数
      int cnt = n;//表示当前圆桌的总人数
      while(out_cnt < n - 1)//当out_cnt等于n-1时,循环结束,此时圆桌师生最后一个人,即我们要的结果
      {
            if(cur_index + m > cnt)
            {
                if((cur_index + m) % cnt == 0)//这种情况需要单独考虑,否则cur_index就变成负值了
                    cur_index = cnt - 1;
                else
                    cur_index = (cur_index + m) % cnt - 1;
            }
            else
                cur_index = cur_index + m - 1;
            cnt--;
            out_cnt++;
            cout<<"当前出列的为:"<<*(rs.begin() + cur_index)<<endl;
            rs.erase(rs.begin() + cur_index);//从数组中删去需要出队的人员
      }
      cout<<"最后一个出列的人物为 :"<<rs[0]<<endl;
    }
    

    该方法的时间复杂度为O(nm),空间复杂度为O(n),整个算法的基本流程还是比较清晰的,相当于每次循环更新cur_cnt、cnt和out_cnt这三个变量,当out_cnt == n-1时,此时出队的人数一共有n-1人,圆桌上只剩下一个人了,停止循环。此外,该算法有几点需要注意:

    (1)首先,我们为什么要对用户进行重新编号(从0开始到n-1),在我看来,这是因为在整个循环过程中我们用到了对当前圆桌人数总数cnt进行了取余的操作,而取余的结果包括0到cnt -1,即包括0;如果编号是从1开始的话,在余数为0的时候需要特殊处理,而从0开始编号的话,一方面符合编程习惯(下标从0开始计数);另一方面面对取余操作不需要特殊处理。

    (2)代码中的cur_index指的当前圆桌状态下需要出队的人的编号,即数到m的人的编号(此处的编号指的是重新编号的编号);由于cur_index的值表示的是重新编号后的编号,但它的初值表示的是最开始数数的那个人的编号(初始值的时候就不表示需要出队的人的编号了),由于题目要求的是从编号为1的人开始数数,并且其对应的新编号为0,故cur_index的初值为0。此外,在循环计算cur_index时,我们发现不论是哪种情况,cur_index更新值都有一个减1的操作;这是因为cur_index每次加m得到的值或者加m再取余得到的值,实际上是需要出队人员的原始编号(即从1开始到n结束的那个编号),而cur_index应该表示的是重新编号后的编号,而新编号比旧编号小1,所以需要减去1,这其实可以看成是一个规律。此处可以举个例子,例如:对于n=5,m=3;cur_index的初值为0,cur_index + 3 <5,所以cur_index + m = 3(不用进行取余操作了),如果不减1的话,3表示的是新编号,对应的旧编号就是4,而实际上应该出队的人员的编号是3,对应的新编号是2。

    (3)数组rs的下标相当于新编号,而数组存储的内容相当于旧编号,rs每次删除的元素对应每次出队的人员的编号,在这里我们需要了解erase的原理,rs每删除一个元素时,被删除之后的元素就会前移,相当于新旧编号的对应关系也发生了变化,即下一个开始数数的人占据了之前出队人员的位置,它的新编号发生了变化;而对向量进行erase操作后元素移动的原理实质和圆桌人员移动的情况是一致的啊,然后结合cur_index进行操作(把vector进行erase操作后移动元素的原理看成是圆桌人员移动),所以说能利用vector代替循环链表模拟整个过程。由于rs的内容表示旧编号,所以返回的结果直接是rs[0]的内容,不需要再加1了

    2.利用数学推导得出的公式直接求解。

    方法1中利用了数组直接进行过程模拟,但空间复杂度比较高,下面给出一种更为常见的方法,即直接对整个过程归纳出一个数学公式来。公式的具体推导本文不详细描述,可参考:https://blog.csdn.net/wusuopubupt/article/details/18214999

    上文中给出了公式$f(i)=[f(i-1) + m] \% i$ 其中$f(i)$表示的是当圆桌人数为$i$时,应该出队人员的编号(这里的编号指的是重新编号后的编号);其中当$i = 1$时,$f(i) = 0$ ,所以公式的完整表达式如下:

     $$ f(i) = egin{cases} [f(i-1) + m] \% i ,& ext{i > 1} \ 0 , & ext{i = 1} end{cases} $$

    所以根据这个公式,可以递归实现,也可以用迭代实现,此处只给出迭代的实现:

    #include<iostream>
    #include<unordered_map>
    #include<queue>
    #include<cstring>
    #include<cstdlib>
    #include<cmath>
    #include<algorithm>
    #include<sstream>
    #include<set>
    #include<map>
    using namespace std;
    
    int main()
    {
        int n,m;
        cin>>n>>m;
        int cnt= 0;
        int rs = 0;//f(1) = 0;
        for(int i = 2; i <= n; i++)
        {
            rs = (rs + m) % i;
    
        }
    
        cout<<"最后一个出列的人为:"<<rs + 1<<endl;
    }
    

    该算法的时间复杂度为O(n),空间复杂度为O(1)。需要注意的一点是,rs的初始值为0,表示当总人数为1时,应该出列的人员编号(新编号)。这里与方法1中cur_index的初值表示的意义是完全不同的。整个公式的推导过程与方法1也是完全不同的(见参考链接)。

    三 问题变种

    之前所遇到的问题都是从编号(旧编号)为1的人开始报数,如果从编号为k的人开始报数呢?整个问题就变了,方法2的数学公式就不适用了;如果还想用方法2中的递推数学公式的话,需要根据参考链接中的方法重新推导数学公式。但是方法1还是适用的,只需在cur_index初始化的时候,初始化k-1即可(代码的其他部分不用变),curi_index的初值意义就表示开始数数的人的编号(新编号)。所以遇到哪种类型的问题,要看仔细喽,可以重点掌握下方法1,它的通用型更强,毕竟是直接模拟整个过程啊。

  • 相关阅读:
    viso图插入Word中大片空白解决办法
    面向对象设计模式中的单例模式和工厂模式
    面向对象知识整理
    require和include的区别及自动加载的定义
    面向对象的三大特性及定义
    重写和重载的区别 (部分内容转自竹木人)
    面向对象的基本概念
    PHP json_encode( ) 函数介绍
    js页面跳转常用的几种方式
    js中页面刷新和页面跳转的方法总结 [ 转自欢醉同学 ]
  • 原文地址:https://www.cnblogs.com/wangkundentisy/p/9278693.html
Copyright © 2011-2022 走看看