目录
一、魔术《守岁共此时》的步骤
二、揭秘魔术《守岁共此时》
三、数学模型约瑟夫问题(约瑟夫环)
四、编程复现魔术《守岁共此时》
五、程序运行结果
一、魔术《守岁共此时》的步骤
在今年的春晚里,魔术师刘谦表演了一个和纸牌相关的魔术,其精妙的设计带给观众极高的参与感,今天将从数学的角度感受这一魔术的魅力。刘谦在 2024 年春晚表演的魔术《守岁共此时》的全过程如下所示:
首先打乱四张牌,将四张牌对半分开把其中一半放在另一半下面,然后根据你名字长放对应的牌数到牌底,然后最上面三张牌插在剩下牌中间,位置可以任意但是必须在中间(即不能放在最上面或最下面),然后第一张牌就是我们要和最后牌比对的牌,之后以剩下牌继续,根据你是南方北方或者什么人来输入对应数字,拿对应的牌数放在剩下的牌中间,然后根据男女性别扔掉几张牌,然后上方牌放最下面重复7次,最后一步,最上面牌往下放一张再丢一张,以此重复,最后那张就和之前那张对应。
详细分解后的步骤如下所示:
①任意选四张牌,并把牌洗混。
②把扑克牌对折后撕开,让一堆放在另一堆上面,合并成8张牌。
③名字有几个字就把扑克牌从上面往下挪动几张。
④拿出最上面的三张牌,插入剩下牌的中间(位置随意)。
⑤把最上面的牌拿走放在口袋里。
⑥按照南北方人拿出不同的数量插入中间(南1北2不知道3)。
⑦按性别分别丢弃最上面的牌(男1女2)。
⑧嘴里喊出“见证奇迹的时刻”,每喊一个字,把一张牌从上面拿到最下面。
⑨“好运留下来,烦恼丢出去”,当喊出“好运留下来”时,把最上面的牌拿到最底下;当喊出“烦恼丢出去”时,把最上面的牌丢出去(扔掉),重复步骤直至仅剩一张牌。
⑩如果你步骤正确的话,剩余的半张牌和放在口袋里的半张牌会正好拼成一张牌。
二、揭秘魔术《守岁共此时》
三、数学模型约瑟夫问题(约瑟夫环)
在刘谦的魔术中,约瑟夫问题的核心思想被巧妙地应用于控制过程的结果。约瑟夫问题是通过固定间隔来选择和排除序列中的对象,直到只剩一个对象。在这个魔术中,尽管没有直接使用固定间隔的排除方法,但通过一系列精心设计的操作步骤(如牌的折叠、插入、丢弃等),实现了类似的控制效果,即无论参与者如何执行这些步骤,都会以一种预定的方式减少牌的数量,最终留下一张特定的牌。
据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
为了讨论方便,先把问题稍微改变一下,并不影响原意。问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求:胜利者的编号。
我们知道第一个人(编号一定是(m-1)mod n)出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m mod n的人开始):k,k+1,k+2, ... ,n-2,n-1,0,1,2,... ,k-2,并且从k开始报0,我们把他们的编号做一下转换:k -->0, k+1 -->1,k+2 -->2,...,k-2 -->n-2。
变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗。变回去的公式很简单,相信大家都可以推出来:x'=(x+k) mod n。
如何知道(n-1)个人报数的问题的解?只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ,这显然就是一个倒推问题。
思路出来了,下面写递推公式:令 f 表示 i 个人玩游戏报 m 退出最后胜利者的编号,最后的结果自然是 f[n]。递推公式:f[1]=0,f[i]=(f[i-1]+m) mod i,且(i>1)。
根据该数学原理可以将类似的游戏改编成数学题目,比如下面这道改编题目:
四、编程复现魔术《守岁共此时》
#define _CRT_SECURE_NO_WARNINGS 1 // 忽略掉一些编译器的安全警告,_CRT_SECURE_NO_WARNINGS是一个预编译指令
#include <stdio.h> // 包含标准输入输出的头文件
#include <windows.h> // 包含Windows操作系统的头文件,其中包含了Sleep函数
// 按照题目要求,将一段长度为len的数组arr中begin到end之间的元素进行反转
void func_back(int* arr, int len, int begin, int end)
{
// 计算需要进行多少次反转(end - len即为反转的次数)
int sorts = end - len;
// 外层循环表示进行sorts次反转
for (int y = 0; y < sorts; y++)
{
// 内层循环表示一次反转
for (int x = len - 1; x >= 0; x--)
{
// 使用异或交换两个数的值,实现交换
*(arr + x + begin) = *(arr + x + begin) ^ *(arr + x + 1 + begin);
*(arr + x + 1 + begin) = *(arr + x + begin) ^ *(arr + x + 1 + begin);
*(arr + x + begin) = *(arr + x + begin) ^ *(arr + x + 1 + begin);
}
begin++; // 反转完之后修改起始位置
}
}
// 打印一段长度为len的数组arr
void print(int* arr, int len)
{
for (int x = 0; x < len; x++)
{
printf("%d ", *(arr + x));
}
printf("\n");
}
// 将一段长度为arr_len的数组arr循环左移num位
void number_back(int* arr, int arr_len, int num)
{
for (int x = 0; x < num; x++)
{
int p = *arr;
for (int x = 0; x < arr_len - 1; x++)
{
*(arr + x) = *(arr + x + 1);
}
*(arr + arr_len - 1) = p;
}
}
int main()
{
// 输出一些提示信息
printf("——————————★★★★★★★★揭秘2024春晚刘谦魔术《守岁共此时》★★★★★★★★——————————\n");
Sleep(1000);
printf("首先,请输入你手中的四张扑克牌(为了方便这里只输入数字的牌)\n");
printf("(注意,每一个数字之间需要使用空格或者回车符分隔开。):\n");
// 定义并输入一个长度为8的数组arr,前4个元素存放输入的四张牌,后4个元素用于后续操作
int arr[8] = { 0 };
for (int x = 0; x < 4; x++)
{
scanf("%d", arr + x);
*(arr + x + 4) = *(arr + x);
}
Sleep(1000);
// 输出输入的四张牌
printf("\n你手中的四张扑克牌数字是:\n");
Sleep(1000);
print(arr, 4);
Sleep(1000);
// 第一步,将数组对折并叠放在一起
printf("\n第一步,你需要将其对折撕成两半,并叠放在一起,结果如下所示:\n");
Sleep(1000);
print(arr, 8);
Sleep(1000);
// 第二步,输入名字的长度并将牌向后移动相应的位数
printf("\n第二步,你需要输入你名字的长度:\n");
int name_len = 0;
scanf("%d", &name_len);
Sleep(1000);
printf("\n接下来将会根据名字长度向后放几张牌,结果如下所示:\n");
Sleep(1000);
for (int x = 0; x < name_len; x++)
{
number_back(arr, 8, 1);
print(arr, 8);
Sleep(1000);
}
// 第三步,将最前面三张牌放到剩下牌的中间
printf("\n第三步,将最前面三张牌放在剩下牌的中间,请输入目标位置\n");
printf("(注意:目标位置在剩下的牌中第几张后面就输入几):\n");
int the = 0;
scanf("%d", &the);
func_back(arr, 3, 0, the + 3);
Sleep(1000);
printf("\n结果如下所示:\n");
Sleep(1000);
print(arr, 8);
Sleep(1000);
// 将第一张牌放到最后
printf("\n此时,第一张牌“%d”就是最终要拼接的牌,把它拿出来单独放好。\n\n", *arr);
Sleep(1000);
number_back(arr, 8, 1);
printf("现在你手中的牌是:\n");
Sleep(1000);
print(arr, 7);
Sleep(1000);
// 第四步,根据选择的人对应数字,将最前面的牌放到剩余牌的中间
int the_2 = 0;
printf("\n第四步,如果你是南方人请输入1,北方人则输入2,不确定自己在南方还是北方就输入3:\n");
scanf("%d", &the_2);
Sleep(1000);
printf("\n根据魔术要求和你选的哪方人对应的数字,将会拿出相应张数的最前面的牌,并放到剩余的牌中间,请输入目标位置\n");
printf("(注意:目标位置在所有牌的第几张后面就输入几):\n");
int the_3 = 0;
scanf("%d", &the_3);
func_back(arr, the_2, 0, the_3);
Sleep(1000);
printf("\n现在你的牌变为:\n");
Sleep(1000);
print(arr, 7);
Sleep(1000);
// 第五步,根据性别扔掉最前面的牌
printf("\n第五步,如果你是男生请输入1,女生请输入2,将会根据所选数字扔掉最前面的牌:\n");
int the_4;
scanf("%d", &the_4);
number_back(arr, 8, the_4);
Sleep(1000);
printf("\n丢掉之后的牌为:\n");
Sleep(1000);
print(arr, 7 - the_4);
// 第六步,将最上面的牌放到最下面并重复操作7次
printf("\n第六步,将最上面的牌放在最下面,并且重复操作7次(因为“见证奇迹的时刻”共有7个字哈哈哈!),如下所示:\n");
for (int x = 0; x < 7; x++)
{
number_back(arr, 7 - the_4, 1);
printf("第%d次操作完成后牌为:", x + 1);
print(arr, 7 - the_4);
Sleep(1000);
}
int len = 7 - the_4;
Sleep(1000);
// 第七步,将最前面的牌留下或丢掉
printf("\n最后一步,好运留下来→第一张牌放到最下面;烦恼丢出去→第一张牌丢掉。\n");
while (len != 1)
{
number_back(arr, len, 1);
printf("好运留下来→");
print(arr, len);
Sleep(1000);
number_back(arr, len, 1);
len--;
printf("烦恼丢出去→");
print(arr, len);
Sleep(1000);
}
Sleep(1000);
// 输出最终留下的那张牌
printf("\n现在你的手中剩下的最后一张牌是:%d\n", *arr);
printf("与之前单独拿出来保存的那半张牌刚好是对应的!\n\n");
Sleep(1000);
printf("———————★★★★★★★★恭喜你!成功复现了2024春晚刘谦魔术《守岁共此时》★★★★★★★★———————\n\n");
return 0; // 程序结束
}