刘谦春晚魔术揭秘:约瑟夫环的数学魅力,JS实现下!

数据智能相依偎 2024-02-22 17:00:55

今年春晚刘谦的魔术堪称惊艳全场,那么他这个魔术实现的原理是什么呢?今天,就让咱们使用 JS 是实现这个魔术。

约瑟夫环问题简介

约瑟夫环问题源自古罗马,由历史学家约瑟夫斯提出,而其数学模型则在19世纪被命名。问题设定如下:n个人围成一圈,从第一人开始报数,每报到第k个人,该人就会被淘汰。游戏继续进行,直到最后只剩下一个人。我们的目标是找出这个幸存者的编号。

用扑克牌解读约瑟夫环情景一:最简单的情况

设想我们有两张牌,编号为1和2。我们先将1号放到底部,然后移除2号。结果,最初位于顶部的1号牌幸存下来。

情景二:牌数为2的n次幂

设想有8张牌,编号从1到8。在第一轮中,我们会移除所有偶数编号的牌(2、4、6、8),剩余1、3、5、7。这些剩下的牌按顺序放到底部,问题就变成了4张牌的情况。

重复这个过程,最终我们发现,如果牌数是2^n张,幸存的总是最初位于顶部的那张牌。

情景三:任意数量的牌

对于任意数量的牌(比如11张),我们可以将其表示为2^n+m(在这个例子中是8+3)。通过重复上述过程,我们会发现最终幸存的牌是最初位于顶部第m+1位的牌(在这个例子中是7号牌)

见证奇迹的时刻!从4张牌开始,对折撕成8张排成ABCDABCD。根据名字长度将顶部牌放到底部,位置变化不影响结果。譬如2次,最后变成CDABCDAB;譬如3次,最后换成DABCDABC。但无论怎么操作,第4张和第8张牌都是一样的。将顶部3张牌随意插入中间,确保第1张和第8张牌相同。这一步非常重要!因为操作完之后必然出现第1张和第8张牌是一样的!以名字两个字为例,可以写成BxxxxxxB(这里的x是其他和B不同的牌)。拿掉顶上的牌放到一边,记为B。剩下的序列是xxxxxxB,一共7张牌。南方人/北方人/不确定,分别拿顶上的1/2/3张牌插到中间,但是不会改变剩下7张牌是xxxxxxB的结果。男生拿掉1张,女生拿掉2张。也就是男生剩下6张,女生剩下5张。分别是xxxxxB和xxxxB。循环7次,把最顶上的放到最底下,男生和女生分别会是xxxxBx和xxBxx。最后执行约瑟夫环过程!操作到最后只剩下1张。当牌数为6时(男生),剩下的就是第5张牌;当牌数为5时(女生),剩下的就是第3张牌。Bingo!就是第4步拿掉的那张牌!

下面是完整的 JavaScript 代码实现:

// 定义一个函数,用于把牌堆顶n张牌移动到末尾function moveCardBack(n, arr) { // 循环n次,把队列第一张牌放到队列末尾 for (let i = 0; i < n; i++) { const moveCard = arr.shift(); // 弹出队头元素,即第一张牌 arr.push(moveCard); // 把原队头元素插入到序列末尾 } return arr;}// 定义一个函数,用于把牌堆顶n张牌移动到中间的任意位置function moveCardMiddleRandom(n, arr) { // 插入在arr中的的位置,随机生成一个idx // 这个位置必须是在n+1到arr.length-1之间 const idx = Math.floor(Math.random() * (arr.length - n - 1)) + n + 1; // 执行插入操作 const newArr = arr.slice(n, idx).concat(arr.slice(0, n)).concat(arr.slice(idx)); return newArr;}// 步骤1:初始化8张牌,假设为"ABCDABCD"let arr = ["A", "B", "C", "D", "A", "B", "C", "D"];console.log("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。");console.log("此时序列为:" + arr.join('') + "\n---");// 步骤2(无关步骤):名字长度随机选取,这里取2到5(其实任意整数都行)const nameLen = Math.floor(Math.random() * 4) + 2;// 把nameLen张牌移动到序列末尾arr = moveCardBack(nameLen, arr);console.log(`步骤2:随机选取名字长度为${nameLen},把第1张牌放到末尾,操作${nameLen}次。`);console.log(`此时序列为:${arr.join('')}\n---`);// 步骤3(关键步骤):把牌堆顶三张放到中间任意位置arr = moveCardMiddleRandom(3, arr);console.log(`步骤3:把牌堆顶3张放到中间的随机位置。`);console.log(`此时序列为:${arr.join('')}\n---`);// 步骤4(关键步骤):把最顶上的牌拿走const restCard = arr.shift(); // 弹出队头元素console.log(`步骤4:把最顶上的牌拿走,放在一边。`);console.log(`拿走的牌为:${restCard}`);console.log(`此时序列为:${arr.join('')}\n---`);// 步骤5(无关步骤):根据南方人/北方人/不确定,把顶上的1/2/3张牌插入到中间任意位置// 随机选择1、2、3中的任意一个数字const moveNum = Math.floor(Math.random() * 3) + 1;arr = moveCardMiddleRandom(moveNum, arr);console.log(`步骤5:我${moveNum === 1 ? '是南方人' : moveNum === 2 ? '是北方人' : '不确定自己是哪里人'},\把${moveNum}张牌插入到中间的随机位置。`);console.log(`此时序列为:${arr.join('')}\n---`);// 步骤6(关键步骤):根据性别男或女,移除牌堆顶的1或2张牌const maleNum = Math.floor(Math.random() * 2) + 1; // 随机选择1或2for (let i = 0; i < maleNum; i++) { // 循环maleNum次,移除牌堆顶的牌 arr.shift();}console.log(`步骤6:我是${maleNum === 1 ? '男' : '女'}生,移除牌堆顶的${maleNum}张牌。`);console.log(`此时序列为:${arr.join('')}\n---`);// 步骤7(关键步骤):把顶部的牌移动到末尾,执行7次arr = moveCardBack(7, arr);console.log(`步骤7:把顶部的牌移动到末尾,执行7次`);console.log(`此时序列为:${arr.join('')}\n---`);// 步骤8(关键步骤):执行约瑟夫环过程。把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。console.log(`步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。`);while (arr.length > 1) { const luck = arr.shift(); // 好运留下来 arr.push(luck); console.log(`好运留下来:${luck}\t\t此时序列为:${arr.join('')}`); const sadness = arr.shift(); // 烦恼都丢掉 console.log(`烦恼都丢掉:${sadness}\t\t此时序列为:${arr.join('')}`);}console.log(`---\n最终结果:剩下的牌为${arr[0]},步骤4中留下来的牌也是${restCard}`);

通过上述代码,我们可以模拟刘谦春晚魔术的整个过程,并验证其背后的数学逻辑。

以下为执行结果:

转自:程序员Sunday
0 阅读:0

数据智能相依偎

简介:感谢大家的关注