思维卡互动魔术
程序简介
思维卡互动魔术,具体可以看参考资料,我稍稍介绍一下,现实中,这个魔术的道具是由八张特别设计的卡片组成,一共存在 52 个数字,每张卡片都扣掉了一些数字,保留了一些数字。魔术前需要你想一个数字,我将八张卡片依次给你看,你告诉我卡片上有没有你想的数字。最后我一定能猜到你所想的数字^-^。
这个卡片游戏我感觉叫魔术都不恰当,只是单纯的数字游戏。视频说原理是二进制的,我被误导了一会,不完全是,最后发现是最基本的排列组合问题。
我们将,扣掉和保留,简化为:无和有,再简化为:0 和 1,这样的话,52 个数字就各有八位二进制码。
因为一定会判断正确,所以不会存在两个数字的八位二进制码一模一样,也就是说,八位二进制码就是每个数字的指纹。
二进制的原理就到此为止了。我们现在考虑指纹的问题。
通过视频可知,最后我一定能猜到你所想数字的方法是:将你告诉我,有你所想数字的卡片丢弃;没有你所想数字的卡片留下,再将留下的卡片重叠,此时会留有唯一一个空缺,就一定是你想的那个数字(有那个数字的卡片都丢弃了)。
即,留下的卡片上的所有数字加上你所想的数字必须构成全集。这个条件还必须对每个数字都成立。
用数字来说就是,你所想的数存在一个独一无二的,由 0 和 1 构成的指纹,它与剩下的指纹构成一整套位数相同的指纹集。每张卡片就对应这套指纹集的某一位,卡片数就对应着这套指纹集的位数。我会将你的指纹位数为 1 的卡片舍去。剩下来的卡片(剩下来的位数),对于其他所有的数(指纹)来说,至少出现一次 1。
举个例子(舍去的位数(卡片)我统一补零,方便理解,0 表示无这个数字):
可以成功的指纹集(操作后只有自己是全零):
三张卡片(三位指纹),三个数字(三个指纹)。
指纹集~~~~~(011,101,110)C(2,3)
一号指纹操作后(000,100,100),可以判断成功
二号指纹操作后(010,000,010),可以判断成功
三号指纹操作后(001,001,000),可以判断成功四张卡片(四位指纹),六个数字(六个指纹)。
指纹集~~~~~(1100,1010,1001,0110,0101,0011)C(2,4)
一号指纹操作后(0000,0010,0001,0010,0001,0011),可以判断成功
二号指纹操作后(0100,0000,0001,0100,0101,0001),可以判断成功
三号指纹操作后(0100,0010,0000,0110,0100,0010),可以判断成功
四号指纹操作后(1000,1000,1001,0000,0001,0001),可以判断成功
五号指纹操作后(1000,1010,1000,0010,0000,0010),可以判断成功
六号指纹操作后(1100,1000,1000,0100,0100,0000),可以判断成功
不可以成功的指纹集(操作后出现多个全零):
三张卡片(三位指纹),三个数字(三个指纹)。
指纹集~~~~~(100,110,001)
一号指纹操作后(000,010,001),可以判断成功
二号指纹操作后(000,000,001),不可以判断成功
三号指纹操作后(100,110,000),可以判断成功四张卡片(四位指纹),五个数字(五个指纹)。
指纹集~~~~~(1000,0101,0010,0001,0110)
一号指纹操作后(0000,0101,0010,0001,0110),可以判断成功
二号指纹操作后(1000,0000,0010,0000,0010),不可以判断成功
三号指纹操作后(1000,0101,0000,0001,0100),可以判断成功
四号指纹操作后(1000,0100,0010,0000,0110),可以判断成功
五号指纹操作后(1000,1001,0000,0001,0000),不可以判断成功
所以这个指纹是有条件的,我们根据上面的这个规则进行指纹设计。
此时我们大胆假设一下,一套无重复指纹中 0 和 1 的绝对位数应该保持一致。这样的话,设计就简化为排列组合问题,我们因此还可以得知每种设计的最大容载量。
至于绝对位数能否不一致?不同位数指纹能否混用?我暂时无法证明。
所以原魔术八张卡片最多可以容纳 C(4,8)共 70 个数字;又因为 C(3,7)等于 35,所以 52 个数字至少需要八张卡片。
有同学会问,判断一个数,用二分法,用黄金比分割,不是更快吗?不完全是,因为我们要设计成卡片(固定不变的),所以情况就有所不同。
举个例子(32 个数,即 32 个指纹):
思维卡互动魔术,排列组合方法。我至少需要 C(3,7)或 C(4,7)等于 35,即 7 张卡片,就能完成所有指纹的识别,至多需要 C(1,32),即 32 张卡片。
二分法。正常判断某个数,需要 5 步,为此需要特别设计识别它的 5 张卡片。这时我要判断另一个数,这 5 张就不一定够了,就需要加新卡片。最后你发现,为了能完成所有指纹的识别,你设计了好多好多卡片,而这个数一定大于 32。但实际情况是,你第一步判断是否大于 16 之后,你就筛选了卡片,每一步你判断完,你都会筛选卡片,所以你才只需要 5 步。
这个魔术的重点就是不筛选卡片!
第一次更新:
- 代码更新,现在兼容 vc2010,不兼容 vc6.0
vc2010:_stscanf_s,vc6.0:_stscanf
vc2010:_stprintf_s,vc6.0:_stprintf - 添加了打乱按钮。打乱操作:将同一指纹集中不同编号的指纹两两对调,多次操作后实现打乱,让其中蕴藏的规律更模糊。
- 添加了提前结束功能。有的时候运气好,不需要使用全部卡片就能提前得知结果。现在确定唯一解时即可结束,不需要判断剩下的卡片了。
- 游戏区域盒子绘制函数更新,边框,填充,位置都更新了。
程序执行效果
完整源代码
////////////////////////////////////////
// 程序:思维卡互动魔术
// 作者:Gary
// 编译环境:Visual C++ 2010,EasyX_20211109
// 编写日期:2021-9-17
# include <graphics.h>
# include <string>
# include <time.h>
# include <stdlib.h>
static HWND hOut; // 画布
// 定义一个结构体,盒子
struct Node1
{
int num; // 是否排除参数
int boxnum; // 编号
int lifenum[20]; // 二进制存储
int posx, posy; // 坐标
};
// 定义一个结构体,按钮
struct Node2
{
int posx1, posy1, posx2, posy2; // 坐标
LPTSTR text; // 内容
};
// 定义一个类
class Gary
{
public:
void carry (); // 主进程
void initialization (); // 初始化
void draw (); // 绘制函数
int check (); // 判定函数
void move (); // 窗口主视角
int num_button; // 按钮数量
int A; // 排列组合参数一
int B; // 排列组合参数二
int C; // 排列组合参数三
int M; // 总数
int flag; // 轮数
int exit1, exit2; // 主循控制参数
Node1 box[300]; // 盒子
Node2 boxm[10]; // 按钮
};
// 绘制函数
void Gary::draw ()
{
int i, j;
TCHAR s[25];
// 画布初始化
setbkcolor (WHITE);
setfillcolor (WHITE);
settextcolor (BLACK);
cleardevice ();
// 对 M 个数字进行绘制
setlinecolor (WHITE);
for (j = 0; j < M; j++)
{
// 读编号
i = box[j].boxnum;
// 判断是否已经排除
switch (box[i].num)
{
// 未排除
case 0:
{
setfillcolor (RGB (221, 221, 221));
setbkcolor (RGB (221, 221, 221));
break;
}
// 已排除
case 1:
{
setfillcolor (RGB (255, 156, 68));
setbkcolor (RGB (255, 156, 68));
break;
}
default:break;
}
// 方框绘制
fillrectangle (box[i].posx, box[i].posy, box[i].posx + 50, box[i].posy + 50);
// 编号绘制
if (box[i].lifenum[flag] == 1)
{
_stprintf_s (s, _T ("%0.1d"), i); outtextxy (box[i].posx + 15, box[i].posy + 15, s);
}
}
// 其他元素绘制
setfillcolor (WHITE);
setbkcolor (WHITE);
setlinecolor (BLACK);
line (100, 0, 100, 500);
line (0, 0, 750, 0);
line (750, 0, 750, 500);
line (0, 500, 750, 500);
// 变量载入绘制
fillrectangle (10, 20, 90, 80); outtextxy (20, 40, _T ("张数:"));
_stprintf_s (s, _T ("%0.1d"), A); outtextxy (60, 40, s);
// 按钮绘制
for (i = 0; i < num_button; i++)
{
// 边框
fillrectangle (boxm[i].posx1, boxm[i].posy1, boxm[i].posx2, boxm[i].posy2);
// 文字
outtextxy (boxm[i].posx1 + (boxm[i].posx2 - boxm[i].posx1) / 2 - textwidth (boxm[i].text) / 2, boxm[i].posy1 + 20, boxm[i].text);
}
// 结束判定
j = check ();
// 结束提醒
if (j != -1)
{
i = 3;
fillrectangle (boxm[i].posx1, boxm[i].posy1, boxm[i].posx2, boxm[i].posy2);
// 结果绘制
outtextxy (20, 440, _T ("结果:"));
_stprintf_s (s, _T ("%0.1d"), j); outtextxy (60, 440, s);
FlushBatchDraw ();
// 弹框提醒
MessageBox (hOut, s, _T ("已经猜出来了啦,结果是"), MB_OK);
}
FlushBatchDraw ();
}
// 判断函数
int Gary::check ()
{
int i, j, k;
j = 0;
// 分析所有数字
for (i = 0; i < M; i++)
{
// 未排除统计
if (box[i].num == 0)
{
j++;
k = i;
}
}
// 仅剩一个数字未排除则结束
if (j == 1)
{
exit2 = 1;
return k;
}
// 其他情况
else
{
return -1;
}
}
// 初始化函数
void Gary::initialization ()
{
int i, j, k, t, h;
// 计算至少需要几张卡片
C = 0;
A = 1;
while (C < M)
{
// 排列组合 C=C(B)(A)
A++;
C = 1;
B = A / 2;
for (i = A; i > A - B; i--)
{
C *= i;
}
for (i = B; i > 1; i--)
{
C /= i;
}
// 当 C 大于目标值 M 则退出
}
// 需要 A 张卡片
// 每个数字存在于 B 张卡片
// 从最小成立的十进制数开始
j = 1;
for (i = B; i > 0; i--)
{
j *= 2;
}
j -= 1;
// 编号置零
flag = 0;
// 编号小于 C 时保持赋值
while (flag < C)
{
// 对当前编号的所有 A 个位置置零
for (i = 0; i < A; i++)
{
box[flag].lifenum[i] = 0;
}
// 标记置零
k = 0;
// 十进制数临时储存
t = j;
// 赋值
while (t != 0)
{
// 十进制转化为二进制
for (i = 1, h = 0; i <= t; h++)
{
i *= 2;
}
// 根据二进制赋值
box[flag].lifenum[h - 1] = 1;
// 标记加一
k++;
// 原数减去只有二进制最高位置一时所代表的值
t -= i / 2;
}
// 只有当标记同 B 一致时,属于有效赋值
if (k == B)
{
// 编号加一
flag++;
}
// 十进制数加一
j++;
}
k = ((M - 1) / 10 + 1);
// 坐标初始化
flag = 0;
for (i = 0; i < k && flag < M; i++)
{
for (j = 0; j < 10 && flag < M; j++)
{
box[flag].num = 0;
box[flag].posx = 100 + i * 50 + (6 - k / 2) * 50;
box[flag].posy = j * 50;
box[flag].boxnum = flag;
flag++;
}
}
// 按钮初始化
num_button = 4;
// 矩形按钮
boxm[0].posx1 = 10; boxm[0].posy1 = 120; boxm[0].posx2 = 90; boxm[0].posy2 = 180; boxm[0].text = _T ("无");
boxm[1].posx1 = 10; boxm[1].posy1 = 220; boxm[1].posx2 = 90; boxm[1].posy2 = 280; boxm[1].text = _T ("有");
boxm[2].posx1 = 10; boxm[2].posy1 = 320; boxm[2].posx2 = 90; boxm[2].posy2 = 380; boxm[2].text = _T ("总数更改");
boxm[3].posx1 = 10; boxm[3].posy1 = 420; boxm[3].posx2 = 90; boxm[3].posy2 = 480; boxm[3].text = _T ("打乱顺序");
// 第一张牌
flag = 0;
draw ();
FlushBatchDraw ();
}
// 窗口主视角函数,获取用户操作
void Gary::move ()
{
// 鼠标定义
ExMessage m;
TCHAR ss[25];
int i, j, k, t;
exit2 = 0;
while (exit2 == 0)
{
if (peekmessage (&m, EM_MOUSE | EM_KEY))
{
// 左键单击判断
if (m.message == WM_LBUTTONDOWN)
{
// 判断是否点击了按钮
for (i = 0; i < num_button; i++)
{
if (m.x > boxm[i].posx1 && m.y > boxm[i].posy1 && m.x < boxm[i].posx2 && m.y < boxm[i].posy2)
{
break;
}
}
switch (i)
{
// 无按钮,对应现实是把牌留下,牌上存在的数字视为排除
case 0:
{
// 对 M 个数字进行分析
for (i = 0; i < M; i++)
{
// 排除 flag 位为 1 的数字
if (box[i].lifenum[flag] == 1)
{
box[i].num = 1;
}
}
flag++;
// flag 位为 A 时结束
if (flag == A)
{
flag--;
exit2 = 1;
}
draw ();
break;
}
// 有按钮,对应现实是把牌拿掉,所以不进行操作
case 1:
{
flag++;
// flag 位为 A 时结束
if (flag == A)
{
flag--;
exit2 = 1;
}
draw ();
break;
}
// 重新开始按钮
case 2:
{
// 总数重置
InputBox (ss, 10, _T ("输入总数目(21 ~ 130)"));
_stscanf_s (ss, _T ("%d"), &i);
if (i >= 21 && i <= 130)
{
M = int (i);
}
else
{
MessageBox (hOut, _T ("输入错误,不在范围内"), _T ("来自小豆子的提醒"), MB_OK);
}
// 初始化
initialization ();
draw ();
break;
}
// 打乱顺序按钮
case 3:
{
// 重回第一张牌
flag = 0;
// 是否排除参数置零
for (i = 0; i < M; i++)
{
box[i].num = 0;
}
// 打乱
for (i = 0; i < M / 2; i++)
{
// 随机选择
k = rand () % M;
// 交换
for (j = 0; j < A; j++)
{
t = box[i].lifenum[j];
box[i].lifenum[j] = box[k].lifenum[j];
box[k].lifenum[j] = t;
}
}
draw ();
break;
}
default:break;
}
}
}
}
}
// 主进程
void Gary::carry ()
{
// 窗口定义
hOut = initgraph (751, 501);
// 随机种子初始化
SetWindowText (hOut, _T ("思维卡互动魔术"));
M = 52;
// 进程控制
exit1 = 0;
BeginBatchDraw ();
while (exit1 == 0)
{
initialization ();
move ();
}
EndBatchDraw ();
closegraph ();
}
// 主函数
int main (void)
{
Gary G;
G.carry ();
return 0;
}
添加评论
取消回复