约瑟夫(Joseph)问题的解决方法

问题描述

最初Joseph问题是这样的:有n个人(分别编号1,2……n)围成一圈,这些人轮流数数(编号为1的人开始,从1开始数),当数到m的人就会被处决。接着,后面的人继续从1开始数数,如此下去直到最后剩下来的那个人才能活命。当然Joseph先生非常聪明,他总是选择能最后剩下的那个位置站位。

用图示来阐述Joseph问题更加直观:如下图,假如有A,B……E共五个人(为了方便描述,这里用字母来对每个人编号),m= 3(即数到3的人出去);则第一个被处决的人是C,接着D报1,E报2,然后A报3,于是A被处决,如此进行下去,E,B又先后被处决,最后活下来的是D,因此最后输出答案就是D(或者输出其下标:3)。

解法一:模拟法

实现:

求解约瑟夫问题最直观(但非最高效)的方法就是直接模拟了。借助循环链表(或者数组模拟的循环链表)来表示约瑟夫环,然后就模拟数数过程,当数到m时就将该元素从循环链表中删除,直到循环链表中只剩一个元素为止!在这里我用数组来模拟循环链表,从而实现约瑟夫环问题的求解,代码如下:

#include"iostream"
#include"assert.h"
using namespacestd;
int Joseph_1(intN,int m)
{
assert(N>0&&m>0); //check theparameter,you can alse use if statement to instead of assert
int ret = 0;
int* Loop = new int[N];
memset(Loop,0,sizeof(int)*N);//Loop[i] =0means the people numbered i are remained
int leave = 0;        //leave record the number of executedpeople
int index = 0;
while(leave != N-1)//while N - 1 people areexecuted,then quit loop
{
int count = 0;
while(true)
{
if(Loop[index] == 0)
{
count ++;//increase thecounter,it means Loop[index] is counting!
if(count == m)
{
Loop[index] =1;//Loop[index]is executed!
break;
}
}
index = (index +1)%N;//this is thekey point of using array to simulate Loop_List
}
leave ++;  //increase the number of executed people
}
//use a loop to find which people isremained,return its subscript not index!
for(int i =0;i<N;i++)
{
if(Loop[i] ==0)
{
ret =i;
}
}
return ret;
}
int main()
{
cout<<Joseph_1(6,5)<<endl;
system("pause");
return 0;
}  

分析:

从上面的实现可以看出来,该方法的空间复杂度是O(N),而时间复杂度至少是O(m*N):因为有两重循环,外层循环N-1次是很明显的,内层循环至少m次!因此最小时间复杂度应该是O(N*m)!

如果不用数组来做,而是用循环链表呢?其复杂度理论上要好点,因为每execute一个人之后链表的长度就会减1,所以,内层循环的次数会逐渐减少,即使这样,总体复杂度仍然逃不脱O(N*N)。

解法二:数学公式法

实现:

上面介绍的是基于直接模拟的方法的时空复杂度都不够理想,这在m或者n非常大时效率尤其低。为此这里介绍一个数学公式法,假设N个人的约瑟夫环的问题的解是f(N),这里特别要理解f(N)的含义:它只N个人的约瑟夫环中最终存活的人的下标,同理可得,f(N-1)是N-1个人的约瑟夫环中的最终存活的人的下标,理解f的含义对理解后面的推导有重要作用,我最初将f(N)理解成N个人的约瑟夫环中第一个被execute的人的下标了,导致我怎么也无法理解后面的推导。现在我们就来推导f(N)的函数式:

首先我们来看看从原始的约瑟夫环到第一个被execute的过程:这时,环中有N个人,从下标为0的人开始从0数数,数到m-1的人就被execute,此人的下标是(m % N)-1(不是m-1因为m很可能比N大)。

当有一个人被execute之后,剩下来的N-1个人继续进行数数(从刚被execute那个人之后的人开始重新从0开始数数),这又是一个规模较小的约瑟夫环问题。但是,要注意,这N-1个人的下标不连续(m % N已经被execute了)并且是从m % n开始数数的。为此我们对剩下的N-1个人的下标做如下映射使得(m% N)-1的后面一个人的下标(假设为K = m % N )映射到0:

K——>0

K+1——>1

……

N-1——>(N-1)-K=(N-k)-1

0——>(N-1)-K+1 =N-k

……

K-2——>N-2

做了如上的映射之后,剩下的N-1的下标即连续又是从下标为0的人开始数数了,根据定义其解是:x’ =  f(N-1)。特别要注意的是x’是经过上面映射之后的下标,需要反变换回去才是该人的最初下标。那么如何反变换回去呢?假设x’对应的最初下标是x,则容易得到:x=( x’+k)%N;由此可得:f(N) = (f(N-1)+m%N)%N =(f(N-1)+m)%N。由此要求f(N)只需求f(N-1),求f(N-1)只需求f(N-2),如此下去,直到求得f(1),然而我们直到f(1)恒等于0。因此得到该问题的一般公式:

f(i) = 0;  (i=1)

f(i) = (f(i-1)+m)%i(i>1)

根据这个公式可以写出如下两个函数:其中一个是递归,另外一个是迭代:

#include"iostream"
#include"assert.h"
using namespace std;
int Joseph_2_1(int N,int m)
{
assert(N>0&&m>0);
int ret = 0;
for(int i = 2;i<=N;i++)
{
ret = (ret+m)%i;
}
return ret;
}
int Recursion(int N,int m)
{
if(N==1)
{
return 0;
}
else
{
return (Recursion(N-1,m) + m)%N;
}
}
int Joseph_2_2(int N,int m)
{
assert(N>0&&m>0);
returnRecursion(N,m);
}
int main()
{
cout<<Joseph_2_1(6,5)<<endl;
cout<<Joseph_2_2(6,5)<<endl;
system("pause");
return 0;
}


分析:

解法2与解法1比较起来最显著的特点就是代码少了很多,当然这个差别还不是最根本的!现在从时空性能来比较一下:

空间上,解法2不需要数组或者链表来保存约瑟夫环,只需要一个临时变量“ret”来保存结果就成,因此空间复杂度是O(1);

再来看时间复杂度,由于只需要一重循环(从2循环到N),因此时间复杂度为O(N)!而解法1的最小时间复杂度都是O(N*m),当m非常大时(可能比N大许多),解法2比解法1效率高很多!

约瑟夫的变种问题:

1.N个人的约瑟夫环,从1开始数数,数到m就被execute。请求出第i(0<i<N)次被execute的人的下标。

2.(POJ1012):k个好人和k个坏人站成一个圈(k个好人连续站在前k个位置),请找到最小的m使得在第一个好人被execute之前所有的坏人均已经被execute了。为了简化,假设0<k<14。例如:当k=3时,最小的m是5;当k=4时,最小的m是30。

一步一步求解约瑟夫(Joseph)问题相关推荐

  1. 求解约瑟夫(Joseph)问题---Java算法小练

    约瑟夫问题,是一个计算机科学和数学中的问题,在计算机编程的算法中,类似问题又称为约瑟夫环,又称"丢手绢问题". -百度百科 相信大部分人在刚开始接触Java算法都会遇到约瑟夫问题, ...

  2. 【深度学习基础】一步一步讲解卷积神经网络

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送 本文转自:一步一步讲解卷积神经网络 卷积神经网络(Convoluti ...

  3. 一步一步教你反向传播,求梯度(A Step by Step Backpropagation Example)

    本文是我在学习反向传播时翻译的一篇文章.原文链接如下. A Step by Step Backpropagation Example 实例学习 在这个例子里,我们将制作一个小型神经网络.它有两个输入, ...

  4. 生成对抗网络的损失函数如何设计_理解生成对抗网络,一步一步推理得到GANs(一)...

    作者:Joseph Rocca 编译:ronghuaiyang 导读 GANs在2014年提出,然后就在图像生成领域取得了巨大的成功,但是其背后的原理却不是那么好理解,这篇文章带你从最基础的随机变量开 ...

  5. 调试JDK源码-一步一步看HashMap怎么Hash和扩容

    调试JDK源码-一步一步看HashMap怎么Hash和扩容 调试JDK源码-ConcurrentHashMap实现原理 调试JDK源码-HashSet实现原理 调试JDK源码-调试JDK源码-Hash ...

  6. 一步一步指引你在Windows7上配置编译使用Caffe(https://github.com/fengbingchun/Caffe_Test)

    之前写过几篇关于Caffe源码在Windows764位上配置编译及使用过程,只是没有把整个工程放到网上,最近把整个工程整理清理了下,把它放到了GitHub上.下面对这个工程的使用作几点说明: 1.   ...

  7. 一步一步实现扫雷游戏(C语言实现)(三)

    使用WIN32API连接窗口 此项目相关博文链接 一步一步实现扫雷游戏(C语言实现)(一) 一步一步实现扫雷游戏(C语言实现)(二) 一步一步实现扫雷游戏(C语言实现)(三) 一步一步实现扫雷游戏(C ...

  8. 一步一步学Silverlight 2系列(3):界面布局

    概述 Silverlight 2 Beta 1版本发布了,无论从Runtime还是Tools都给我们带来了很多的惊喜,如支持框架语言Visual Basic, Visual C#, IronRuby, ...

  9. 一步一步写算法(之图结构)

    原文:一步一步写算法(之图结构) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 图是数据结构里面的重要一章.通过图,我们可以判断两个点之间是 ...

最新文章

  1. 使用hyperopt(Bayesian optimization)为xgboost模型挑选最优参数进行模型构建、by Cross Validation
  2. java 线程面试题_JAVA多线程面试题(一)
  3. 在3D世界中创建不同的相机模式——检查对象是否可见
  4. python好学嘛-Python好学吗?Python学习路线
  5. 用python来构建一个word文档-写文章
  6. java面向对象计算器怎么写_Java对象简单实用案例之计算器实现代码
  7. HTML/CSS学习笔记01【概念介绍、基本标签】
  8. vue项目引入字体图标iconfont
  9. 常见的6种线程池及简单使用
  10. 关于配置了数据库方言为MySQLInnoDBDialect后Hibernate不能自动建表的问题
  11. 201671010140. 2016-2017-2 《Java程序设计》java学习第十六周
  12. xshell左侧导航栏_Axure教程:(初级)导航中的页面切换
  13. Win7如何修改开机动画
  14. 计算机操作系统版本号怎么查看,Windows系统版本怎么看?2种查看windows版本的方法介绍...
  15. MYSQL 判断一个时间段是否在另一个时间段内。
  16. C#中导出Excel的单元格属性设置
  17. 这次,大数据工程师赢了!
  18. 在控制台,打印出某个具体的变量,并监听其变化
  19. STL中Vector的内存分配机制
  20. Android车载应用开发与分析(13)- 系统设置-蓝牙设置

热门文章

  1. [原创] 网站联盟 账号通行证 一次登陆,畅通www
  2. SpringBoot2.x系列教程(四十五)Spring Boot集成WebSocket实现技术交流群功能
  3. Wireshark—高级特性命令行模式
  4. c语言595驱动数码管,使用74hc595驱动一位数码管
  5. ICCV2021 SDR2HDR论文笔记:A New Journey from SDRTV to HDRTV
  6. firebird java_JAVA连接Firebird数据库
  7. LaTex排版论文(中文处理,篇章结构,特殊字符,插图,表格,浮动环境,数学公式,自定义命令和环境等)
  8. 2021-08-09校网比赛C题
  9. 最喜欢的一种人生应该是名利淡泊一点的人生,是一个努力奋斗的人生
  10. 鱼C_python的一些题