约瑟夫环问题 JS

约瑟夫环问题 JS

create by db on 2021-7-8 15:53:37
Recently revised in 2021-7-8 19:51:30

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

闲时要有吃紧的心思,忙时要有悠闲的趣味

目录

题目描述

返回目录

问题来历

 传说罗马人占领了乔塔帕特,41 个犹太人被围堵在一个山洞里。他们拒绝被俘虏,而决定集体自杀,大家决定了一个自杀方案,41 个人围成一个圈,由第 1 个人开始顺时针报数,每报数为 3 的人立刻自杀,然后再由下一个人重新从 1 开始报数,依旧是每报数为 3 的人立刻自杀,依次循环下去。其中两位犹太人并不想自杀,是数学家约瑟夫和他的朋友,他们发现了自杀方案的规律,选了两个特定的位置,最后只剩下他们两人,而活了下来。那么这两个位置分别是什么?

问题描述

 这个问题转化成要解决的通用问题:即 n 个人围成一个圈,这 n 个人的编号从 0——(n-1), 第一个人(编号为 0 的人)从 1 开始报数,报数为 m 的人离开,再从下一个开始从 1 开始报数,报数为 m 的人离开,依次循环下去,直到剩下最后一个人(也可以剩最后两个,少循环一次就是了),那么,把最后一个人的编号打印出来 。

思路分析

思路一:循环标记

 用一个数组来存储 n 个人在圈内的状态,全部标识为 1,即长度为 n 的数组所有元素都为 1 ,用一个报数器来记录报数了几次,只有被标识为 1 的人才能够报数,当报数器的值与 m 相等时,就让这个人离开,则标识为 0,并且让记录出圈人数的变量加 1,然后将报数器清零,当纪录变量等于 n-1 时,游戏结束。

思路二:递归标记

 过程同上,不过我们用递归取代第一层循环。

思路三:动态规划

 所谓动态规划就是:找关系。本题基本逻辑是这样的:n 个人出局一个时,总人数就变成了 n-1 个人,此时要处理的问题实际上就是 n-1 个人的问题,以此类推下去,要处理的就是 n = 2 的问题,因为当 n = 1 时,游戏就结束了。那么用一个方程来表示 n = 2 的解决过程:f(2,m) 很容易看到的规律是:m 是双数留下的就是 0,m 是单数留下的就是 1,用 m%2 来表示结果(此为递归算法的基本条件)。然后要找到 f(3,m) 和 f(2,m) 的对应关系,更精确点来说是 f(n,m) 和 f(n-1,m)
步骤为:先找基准条件,再通过一个简单的例子来找 f(n,m) 和 f(n-1,m) 的对应关系。

 首先举一些简单的例子,找下规律:

这是要处理的问题:n = 5,m = 2

0 1 2 3 4 从第 0 位开始报数,第 1 位报数为 2
0 2 3 4 离开第一个人

2 3 4 0 (*) 上面的数字可以写成这样,因为从第 2 位开始报数
0 1 2 3 (**) 这是 n = 4, m = 2,要处理的问题,接下来就处理这个问题
0 2 3 离开第二个人

比较(*)式和(**)式,你可以找到规律:((**)+2)%5 则转化为(*)式了

2 3 0 (*) 上面的数字可以写成这样,因为从第 2 位开始报数
0 1 2 (**) 这是 n = 3,m = 2,要处理的问题,接下来就处理这个问题
0 2 离开第三个人

比较(*)式和(**)式,你可以找到规律:((**)+2)%4 则转化为(*)式了

2 0 (*) 上面的数字可以写成这样,因为从第 2 位开始报数
0 1 (**) 这是 n = 2,m=2,要处理的问 题,我们已经知道答案了,即 2%2 = 0

比较(*)式和(**)式,你可以找到规律:((**)+2)%3 则转化为(*)式了

0 离开第四个人, 游戏结束,那么要往上层回溯了

上面的公式可以让(**)式等价于(*)式,即在(**)式处理问题,得到的结果,通过这个公式就能得出(*)式的结果
复制代码

 因此我们可以得出对应关系的公式:f(n,m) = (f(n-1,m)+m)%n

 即当我们得到了 f(n-1,m) 的解答时,f(n,m) 的解答也就出来了

 动态规划(递归)的思路是从上往下分解,再一层层往上回溯的。当 n = 2,m = 2 时,我们得到的解是 0,将这个 0 套入公式,往上回溯一层,所以当 n = 3,m = 2 时,我们得到的解是 (0+2)%3 = 2,将这个 2 套入公式,往上回溯一层,所以当 n = 4,m = 2 时,我们得到的解是 (2+2)%4 = 0,将这个 0 套入公式,往上回溯一层,所以当 n = 5,m = 2 时,我们得到的解是 (0+2)%5 = 2,因此我们解决了 f(5, 2) 的问题。

AC 代码

题解一:循环标记

/**
 * @param {number} n 人数
 * @param {number} m 出圈报数
 * @return {number}
 */
function josephRing(n, m) {
  //当参数不满足条件时,这个游戏无法进行
  if (n <= 1 || m < 1) {
    console.log("you can't play Joseph's game. n must be bigger than 1, m must be bigger than 0")
    return
  }

  let arr = new Array(n).fill(1) //长度为n的数组,位置从0——n-1,就代表了 n 个人的编号,并将数组所有元素设定为 1代表未出圈
  let count = 0 //纪录出圈人数
  let num = 0 //报数器

  //设定循环结束条件:当 count = n-1,即只剩下一个人的时候,游戏结束`
  while (count < n - 1) {
    for (let i = 0; i < arr.length; i++) {
      //第二层循环,循环数组
      if (arr[i] === 1) {
        //当这个位置的元素为 1 时,就执行接下来的代码
        num++ //每经过一个元素为 1 的位置时,就让报数器加 1
        if (num === m) {
          //当报数器等于 m 时,就执行接下来的代码
          arr[i] = 0 //让这个位置的元素为 0,表示这个位置已经出圈了
          count++ //纪录出圈人数的变量加 1
          num = 0 //将报数器清零
        }
        //当 m = 1 时,只有当 count = n 才会退出第二层循环(for循环),此时数组内的所有元素都变为了 0,为了避免这个问题,必须要有这个 if 判断句,达到特定条件时强制退出
        //其实当 m = 1时,结果就是 n,也可以将 m = 1 作为特殊情况来处理,即写在 while 循环以外,如此 m = 1 时就不会进入循环
        if (count === n - 1) {
          break
        }
      }
    }
  }
  //找到数组中元素为 1 的位置,将这个位置输出
  let winner = arr.findIndex(item => item === 1) + 1
  console.log(`${winner} is the winner`)
}

//测试上面的代码,并且打印执行的时间,如此可以与其他解决方案的执行时间相比较
let start = new Date().getTime()
josephRing(10000, 3)
let end = new Date().getTime()
console.log('====' + (end - start) + '====')
复制代码

题解二:递归

/**
 * @param {number} n 人数
 * @param {number} m 出圈报数
 * @return {number}
 */
let count = 0 //纪录出圈人数
let num = 0 //报数器
function josephRing(n, m, arr) {
  //当参数不满足条件时,这个游戏无法进行
  if (n <= 1 || m < 1) {
    console.log("you can't play Joseph's game. n must be bigger than 1, m must be bigger than 0")
    return
  }
  //初始调用,创建长度为n的数组,位置从0——n-1,就代表了 n 个人的编号,并将数组所有元素设定为 1代表未出圈
  if (!arr) {
    arr = new Array(n).fill(1) 
  }
  //设定递归结束条件:当 count = n-1,即只剩下一个人的时候,游戏结束,返回胜者
  if (count === n - 1) {
    let winner = arr.findIndex(item => item === 1) + 1
    console.log(`${winner} is the winner`)
    return
  }
  // 循环数组,出圈更改状态
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === 1) {
      //当这个位置的元素为 1 时,就执行接下来的代码
      num++ //每经过一个元素为 1 的位置时,就让报数器加 1
      if (num === m) {
        //当报数器等于 m 时,就执行接下来的代码
        arr[i] = 0 //让这个位置的元素为 0,表示这个位置已经出圈了
        count++ //纪录出圈人数的变量加 1
        num = 0 //将报数器清零
      }
    }
  }
  // 递归调用
  josephRing(n, m, arr);
}

//测试上面的代码,并且打印执行的时间,如此可以与其他解决方案的执行时间相比较
let start = new Date().getTime()
josephRing(10000, 3)
let end = new Date().getTime()
console.log('====' + (end - start) + '====')
复制代码

题解三:动态规划

·

/**
 * @param {number} n 人数
 * @param {number} m 出圈报数
 * @return {number[]}
 */
function josephRing(n, m) {
  if (n <= 1 || m < 1) {
    console.log("you can't play Joseph's game. n must be bigger than 1, m must be bigger than 0")
    return
  }

  let r = 0
  for (let i = 2; i <= n; i++) {
    //会先计算 n = 2 时的结果,最终得到的 r 就是胜利者
    r = (r + m) % i
  }
  console.log(r + 1 + ' is the winner.')
}

//测试上面的代码,并且打印执行的时间,如此可以与其他解决方案的执行时间相比较
let start = new Date().getTime()
josephRing(10000, 3)
let end = new Date().getTime()
console.log('====' + (end - start) + '====')
复制代码

总结

返回目录

 路漫漫其修远兮,与诸君共勉。

参考文献

后记:Hello 小伙伴们,如果觉得本文还不错,记得点个赞或者给个 star,你们的赞和 star 是我编写更多更丰富文章的动力!GitHub 地址

文档协议

知识共享许可协议
db 的文档库db 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/danygitgit上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享