快来学!人人都可以学会撩妹的魔术,附加代码模拟【码农的情怀与浪漫】

代码模拟扑克读心术(21张牌魔术), 关于魔术得数学原理与证明

有这样一个魔术,我相信大家多少都自己玩过或者见别人玩过

魔术流程:

  1. 随便找出21张纸牌。
  2. 让观众随便找出一张,并让其他观众看到。(魔术师自己是不看,经过下面的过程我们就会知道那是张什么牌了)
  3. 把21张牌分成3堆,每堆7张。(让牌背对自己,左到右排三张,从上到下依次按顺序排成三叠)
  4. 从第一堆牌开始询问观众他所选的牌有没有在里面
  5. 将有的那一堆放在没有的两堆中间,然后又开始分三堆,询问观众。(如此循环一共三次)这样之后观众的牌就在整叠牌中的第十一张(已经知道答案是第11张了)
  6. 展露观众所选的牌了,向观众说:在玩21张牌是,第一张牌是有邪气的,我们一般都不用。将第一张牌放一边。(这里的台词可以自己发挥)
  7. 这样整叠牌还剩20张,将所有牌交给观众。告诉观众自己就会把牌找出来。让观众随便说一个10到20中间的任何一个数。比方观众说17
  8. 那就让观众将牌一张一张,往桌上发共发17张。让观众把手上剩下的牌放一边
  9. 再让观众拿起桌上的牌(刚发的那叠牌),问1+7等于多少。自然是8,那好就再往桌上发8张牌
  10. 最后做一个魔术动作,揭开第一张牌就是观众所选的牌了

有一天作者心血来潮,想写代码来证明下模拟下该魔术

接下来是模拟得全部过程

步骤一

初始化54张扑克


const pokers = ["JOKER", "BLACK"]
const nums = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
const colors = ["♦", "♣", "♥", "♠"]

for (let index = 0; index < colors.length; index++) {
    const color = colors[index];
    for (let index = 0; index < nums.length; index++) {
        const num = nums[index];
        pokers.push(color + num)
    }
}
步骤二

进行洗牌

  1. 洗牌方案1
//每次随机取一张跟最后一张交换
function shuffle(pokers) {
    var count = pokers.length; //扑克未交换的牌数

    while (count > 0) {
        const index = Math.floor(Math.random() * count);
        var tmp = pokers[pokers.length - 1];
        pokers[pokers.length - 1] = pokers[index];
        pokers[index] = tmp
        count--
    }
}
  1. 洗牌方案2
//不断抽牌放到一边 产生了新数组
function shuffle2(pokers) {
    var res = []
    while (pokers.length > 0) {
        res.push(pokers.splice(Math.floor(Math.random() * pokers.length), 1)[0])
    }
    return res
}

两种洗牌方案得区别:方案一不产生新的数组,不断得随机抽取没有洗过得牌放到牌堆得最后一张.方案二是不断随机抽取牌放到一旁,直到所有牌都抽取完毕。在这里,方案一没有产生新得数组,方案二产生了新的数组

步骤三

挑取其中21张(这里其实可以忽略第二部,直接第三步,但是为了模拟更真实的情况,我们还是进行了洗牌操作)

 for (let index = 0; index < 21; index++) {
     list21.push(pokers.splice[Math.floor(Math.random() * pokers.length)])
}
步骤四

21 张牌进行分组,一张张发牌,从左到右分成三堆

    var list7_1 = list21.filter((ele,index)=>index%3==0)
    var list7_2 = list21.filter((ele,index)=>index%3==1)
    var list7_3 = list21.filter((ele,index)=>index%3==2)
步骤五

模拟观众选牌,并告诉魔术师在哪一堆

    var questions = [
        {
            type: 'input',
            name: 'ChooseCard',
            message: `选定一张牌,如果在第1组,请输入1","如果在第2组,请输入2","如果在第3组,请输入3\n`
        }
    ]

    const answers = await inquirer.prompt(questions)

    const group = answers['ChooseCard']

    console.log(`选定第${group}组`);
步骤六

三堆牌叠在一起,把观众选定的那一组放在牌中间

    list21 = []
    
    if (group == 1) {
        list21.push(...list7_2)
        list21.push(...list7_1)
        list21.push(...list7_3)
    } else if (group == 2) {
        list21.push(...list7_1)
        list21.push(...list7_2)
        list21.push(...list7_3)
    } else if (group == 3) {
        list21.push(...list7_2)
        list21.push(...list7_3)
        list21.push(...list7_1)
    }
步骤七

重复两次步骤四五六,这样牌一定会在21张的最中间

await new Promise(res=>{
    setTimeout(res,2000)
})

console.log("经过预测,你选中得牌是:" + list21[10]);

思考与扩展一

我们多次用上述程序模拟,结果是对的,但是人工选牌并输入始终次数有限。我们希望看到成千上万次的结果,所以我们把程序修改为程序自己选牌并且输出结果

//在洗牌并选定21张后,我们让程序随机帮我挑选一张牌
  var  choosePoker = list21[Math.floor(Math.random()*list21.length) ]

修改步骤五为

    var list7_1 = list21.filter((ele,index)=>index%3==0)
    var list7_2 = list21.filter((ele,index)=>index%3==1)
    var list7_3 = list21.filter((ele,index)=>index%3==2)

    const all7List = []
       
    all7List.push(list7_1 )
    all7List.push(list7_2)
    all7List.push(list7_3)
    
    const group = (all7List.findIndex(list=>{
        return list.find(ele=>ele==choosePoker)
    })) + 1 

    console.log(`系统选定第${group}组`);

    list21 = []

    if (group == 1) {
        list21.push(...list7_2)
        list21.push(...list7_1)
        list21.push(...list7_3)
    } else if (group == 2) {
        list21.push(...list7_1)
        list21.push(...list7_2)
        list21.push(...list7_3)
    } else if (group == 3) {
        list21.push(...list7_2)
        list21.push(...list7_3)
        list21.push(...list7_1)
    }

重复三次步骤四五六,打印系统选中的牌位置

    console.log("系统选中的牌所在的位置:"+ list21.findIndex(ele=>ele==choosePoker) );

执行1000次,上面结果都是返回10

思考与扩展二

关于这块的数学原理

在这里,笔者菜鸡的用两张数学稿纸来绘个图


111.png

222.png

我们发现其实原理就是不断的指定一组将其放在牌堆中间,继续重排后这一组牌会均匀分布在每一组的中间,比如说我们27张牌,经过用户一次指定就固定在了9张牌内,重排后再分组,用户继续选定就能固定在3张内,在继续重排分组,就能筛出用户选的那一张。

笔者发现

如果分了N组,我们每次分组都能确定出其中的 1/N
总共N组 ,每组M张牌 , 总共扑克数目 COUNT = N
M
总共经过 log(N)M 向上取整+1 次数分组,就可以确定观众选取的牌
*

我们来简单验证上述公式正确性,假设总共9张 ,我们取下边界值,有如下几个情况

  1. 3组 每组3个 log(3)3 = 1 => N = 1+1 = 2 <font color="red">(正解)</font>
  2. 9组 每组1个 log(9)1 = 0 => N = 0+1 = 1 <font color="red">(正解)</font>
  3. 1组 每个9个 log(1)9 无解 => N = 无解 <font color="red">(正解)</font>

疑问:这里如果是分成两组,是不是有点类似于咱们代码中的二分查找。至少,笔者愿意把这个当成N分查找的变体

思考与扩展三

根据上述公式,我们来扩展一个维度,看看超过21张或者无限多张的情况是怎么样的呢?

话不多说,用代码进行验证

选出牌的个数不一定是21,我们把其提取为一个变量

var choosePokersCount ;

修改步骤三
挑取其中21张改为挑取choosePokersCount 张

 for (let index = 0; index < choosePokersCount ; index++) {
     choosePokers.push(pokers.splice[Math.floor(Math.random() * pokers.length)])
}

结果打印改为

   const result = choosePokers[((choosePokersCount - 1) / 2)] == choosePoker
   console.log(`当前选取${choosePokersCount}张,执行结果为${result}` );

我们做下多值测试

for (let index = 0; index < 10; index++) {
    choosePokersCount = 9 + 6 * index

    if(choosePokersCount>54){
        break;
    }
    await test()
}

打印结果如下

choosePoker:♦5
当前选取9张,执行结果为true
choosePoker:♣J
当前选取15张,执行结果为true
choosePoker:♠A
当前选取21张,执行结果为true
choosePoker:♣4
当前选取27张,执行结果为true
choosePoker:♣6
当前选取33张,执行结果为true
choosePoker:♠A
当前选取39张,执行结果为true
choosePoker:♦5
当前选取45张,执行结果为false
choosePoker:♥Q
当前选取51张,执行结果为false

我们发现打印结果后面不准确,仔细思考,通过公式(总共经过 log(N)M 向上取整+1 次数分组,就可以确定观众选取的牌)发现超过27张扑克需要进行至少四次分组才能确定,我们多进行一次分组,所以我们把重复3次步骤四五六改成4次,再观察结果

choosePoker:♦8
当前选取9张,执行结果为true
choosePoker:♣7
当前选取15张,执行结果为true
choosePoker:♦7
当前选取21张,执行结果为true
choosePoker:♥3
当前选取27张,执行结果为true
choosePoker:♥K
当前选取33张,执行结果为true
choosePoker:♣J
当前选取39张,执行结果为true
choosePoker:♥J
当前选取45张,执行结果为true
choosePoker:♣2
当前选取51张,执行结果为true

果然,发现结果是符合预期的

思考与扩展四

问题: 既然分成3组可以经过几次可以选出观众的牌,那么我们再扩展一个维度,超过3组或者无限多组的情况是怎么样的呢?

我们继续修改代码

这次,挑出的牌不一定是3组,每组也不一定7张,我们把其定义为两个变量

var GROUP_COUNT  //总共多少组
var GROUP_SUM  //一组多少张牌

传统扑克54张,已经不够用了,我们定义一个广义的扑克列表

    var nums = []

    for (let index = 0; index < 1000; index++) {
        nums.push(index)
    }

    allPokers = []

    for (let index = 0; index < colors.length; index++) {
        const color = colors[index];
        for (let index = 0; index < nums.length; index++) {
            const num = nums[index];
            allPokers.push(color + num)
        }
    }

修改步骤三

    choosePokers = allPokers.slice(0, GROUP_COUNT * GROUP_SUM)

修改步骤五为

        var pokersGroups = []
        //分组
        for (let indexI = 0; indexI < GROUP_COUNT; indexI++) {
            var list = choosePokers.filter((ele, indexJ) => indexJ % GROUP_COUNT == indexI)
            pokersGroups.push(list)
        }
        
        //计算选定的是哪一叠
        const groupIndex = (pokersGroups.findIndex(list => {
            return list.find(ele => ele == choosePoker)
        }))
        
        //把选定那一叠放到中间
        pokersGroups.splice((pokersGroups.length - 1) / 2, 0, pokersGroups.splice(groupIndex, 1)[0]);
        
        choosePokers = []
        for (let index = 0; index < pokersGroups.length; index++) {
            choosePokers.push(...pokersGroups[index])
        }

程序化执行步骤四五六次数

 var handlerCount = 0; //统计步骤四五六执行的次数

    while (Math.pow(GROUP_COUNT, handlerCount) < GROUP_SUM) {
        choosePokers = await fenzu(choosePokers)
        handlerCount++;
    }

    //需要多执行一次
    choosePokers = await fenzu(choosePokers)

我们用程序做下多值测试

   for (let index = 1; index < 200 ; index++) {
        GROUP_COUNT = Math.floor(Math.random()*10) 
        GROUP_SUM = Math.floor(Math.random()*100) 
        
        if( GROUP_COUNT <= 1 ||GROUP_COUNT%2==0||GROUP_SUM%2==0|| GROUP_SUM*GROUP_COUNT > 4000 ){
            //GROUP_COUNT GROUP_SUM 必须为奇数  而且求和不能大于扑克总数 GROUP_COUNT要>1
            continue
        }

        console.log("GROUP_COUNT:"+GROUP_COUNT + " GROUP_SUM:"+GROUP_SUM);
        await ideal()
    }

修改打印结果

   const result = choosePokers[((GROUP_COUNT * GROUP_SUM - 1) / 2)] == choosePoker
   console.log(`当前分${GROUP_COUNT}组 ,每组分${GROUP_SUM}个 ,执行结果为${result}` );

最终结果打印符合预期

choosePoker:♠446
当前分9组 ,每组分79个 ,执行结果为true
choosePoker:♦101
当前分9组 ,每组分65个 ,执行结果为true
choosePoker:♠98
当前分9组 ,每组分43个 ,执行结果为true
choosePoker:♥932
当前分7组 ,每组分77个 ,执行结果为true
choosePoker:♠495
当前分3组 ,每组分19个 ,执行结果为true

思考与扩展五

问题: 如果是偶数组 ,每组偶数个,怎么办呢?

答案:偶数组也会不断的把观众选到的牌筛选到中间来,但是魔术师无法知道具体是哪一组,因为偶数组没有最中间的那一组之说,只能定位到中间的那两组。同理,每组偶数个也是一样的,多次筛选后观众选中的牌会到某一组的中间那两张,具体哪一张就要看运气了!

所有代码都托管在github,需要请点击 传送门

创作不易,请点赞支持下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容