當前位置: 妍妍網 > 碼農

劉謙春晚魔術揭秘:約瑟夫環的數學魅力,JS實作下!

2024-02-19碼農


今年春晚劉謙的魔術堪稱驚艷全場,那麽他這個魔術實作的原理是什麽呢?今天,就讓咱們使用 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號牌)

見證奇跡的時刻!

  1. 從4張牌開始,對折撕成8張排成ABCDABCD。

  2. 根據名字長度將頂部牌放到底部,位置變化不影響結果。譬如2次,最後變成CDABCDAB;譬如3次,最後換成DABCDABC。但無論怎麽操作,第4張和第8張牌都是一樣的。

  3. 將頂部3張牌隨意插入中間,確保第1張和第8張牌相同。這一步非常重要!因為操作完之後必然出現第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張牌移動到末尾
functionmoveCardBack(n, arr{
// 迴圈n次,把佇列第一張牌放到佇列末尾
for (let i = 0; i < n; i++) {
const moveCard = arr.shift(); // 彈出隊頭元素,即第一張牌
arr.push(moveCard); // 把原隊頭元素插入到序列末尾
}
return arr;
}
// 定義一個函式,用於把牌堆頂n張牌移動到中間的任意位置
functionmoveCardMiddleRandom(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}\t\t此時序列為:${arr.join('')}`);
const sadness = arr.shift(); // 煩惱都丟掉
console.log(`煩惱都丟掉:${sadness}\t\t此時序列為:${arr.join('')}`);
}
console.log(`---\n最終結果:剩下的牌為${arr[0]},步驟4中留下來的牌也是${restCard}`);







透過上述程式碼,我們可以模擬劉謙春晚魔術的整個過程,並驗證其背後的數學邏輯。

以下為執行結果: