昨夜春晚上刘谦的两个戏法扮演都十分精彩,尤其是第二个戏法,他演绎了经典的约瑟夫环问题!

什么是约瑟夫环问题?

约瑟夫环(Josephus problem)是一个经典的数学问题,最早由古罗马历史学家弗拉维奥约瑟夫斯提出,但它的姓名是在19世纪由德国数学家约瑟夫乔瑟夫斯(Josef Stein)命名的。

问题的描述是这样的:假设有n个人(编号从1到n)站成一个圆圈,从第一个人开端报数,签到某个数字(例如k)的人就被杀死,然后从下一个人开端重新报数并持续这个进程,直到只剩余一个人留下来。

问题的关键是找出存活下来的那个人的编号。

结合扑克牌解说约瑟夫环问题

1、考虑最简单的状况

假设有2张牌,编号别离是1和2。

首先将1放到后边,丢掉2。剩余的便是最开端放在最上边的那张1。

2、稍微杂乱一点的状况,牌的张数是2的n次方

比方有8张牌,编号别离是1、2、3、4、5、6、7、8。

第一轮会把2、4、6、8丢掉,剩余1、3、5、7按次序放在后边,又退化成了4张牌的状况。

第二轮会把3、7丢掉,剩余1、5按次序放在后边,又退化成了2张牌的状况。

第三轮把5丢掉,剩余1,便是最初在最前面的那张。

结论:如果牌的张数是2^n,最终剩余的一定是最开端放在牌堆顶的那张。

3、考虑恣意的状况,牌的张数是2^n+m

比方牌的张数是11,等于8+3。把1放到后边,把2丢掉,把3放到后边,把4丢掉,把5放到后边,把6丢掉,现在剩余的编号序列是7、8、9、10、11、1、3、5,这又是8张牌的状况!最终一定剩余的是现在牌堆顶的7!

因此,只需提前知道牌的张数,就一定能马上推导出最终是剩余哪一张牌。一切的魔法都是数学!!都是算法!!

见证奇观的时刻!戏法的流程

  1. 4张牌对折后扯开,便是8张,叠放在一起便是ABCDABCD。留意,ABCD四个数字是彻底等价的。
  2. 依据姓名字数,把顶上的牌放到下面,但怎样放都不会改动循环序列的相对方位。比如2次,最终变成CDABCDAB;比如3次,最终换成DABCDABC。但无论怎样操作,第4张和第8张牌都是相同的。
  3. 把顶上3张插到中心恣意方位。这一步十分重要!由于操作完之后必定出现第1张和第8张牌是相同的!以姓名两个字为例,能够写成BxxxxxxB(这里的x是其他和B不同的牌)。
  4. 拿掉顶上的牌放到一边,记为B。剩余的序列是xxxxxxB,总共7张牌。
  5. 南方人/北方人/不确定,别离拿顶上的1/2/3张牌插到中心,可是不会改动剩余7张牌是xxxxxxB的成果。
  6. 男生拿掉1张,女生拿掉2张。也便是男生剩余6张,女生剩余5张。别离是xxxxxB和xxxxB。
  7. 循环7次,把最顶上的放到最底下,男生和女生别离会是xxxxBx和xxBxx。
  8. 最终履行约瑟夫环进程!操作到最终只剩余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或2
for (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}tt此刻序列为:${arr.join('')}`);
    const sadness = arr.shift();  // 烦恼都丢掉
    console.log(`烦恼都丢掉:${sadness}tt此刻序列为:${arr.join('')}`);
}
console.log(`---n最终成果:剩余的牌为${arr[0]},过程4中留下来的牌也是${restCard}`);

这段代码完成了昨夜春晚上刘谦的第二个戏法扮演的进程,并提供了详细的解说。享用戏法的魅力吧!

春晚刘谦戏法的模拟程序

春晚刘谦戏法的模拟程序