妙妙小豆子

胆子大一点,想开一点。长风破浪会有时,直挂云帆济沧海

思维卡互动魔术 铜牌收录

程序简介

思维卡互动魔术,具体可以看参考资料,我稍稍介绍一下,现实中,这个魔术的道具是由八张特别设计的卡片组成,一共存在 52 个数字,每张卡片都扣掉了一些数字,保留了一些数字。魔术前需要你想一个数字,我将八张卡片依次给你看,你告诉我卡片上有没有你想的数字。最后我一定能猜到你所想的数字^-^。

这个卡片游戏我感觉叫魔术都不恰当,只是单纯的数字游戏。视频说原理是二进制的,我被误导了一会,不完全是,最后发现是最基本的排列组合问题。

我们将,扣掉和保留,简化为:无和有,再简化为:0 和 1,这样的话,52 个数字就各有八位二进制码。
因为一定会判断正确,所以不会存在两个数字的八位二进制码一模一样,也就是说,八位二进制码就是每个数字的指纹。
二进制的原理就到此为止了。我们现在考虑指纹的问题。
通过视频可知,最后我一定能猜到你所想数字的方法是:将你告诉我,有你所想数字的卡片丢弃;没有你所想数字的卡片留下,再将留下的卡片重叠,此时会留有唯一一个空缺,就一定是你想的那个数字(有那个数字的卡片都丢弃了)。
即,留下的卡片上的所有数字加上你所想的数字必须构成全集。这个条件还必须对每个数字都成立。
用数字来说就是,你所想的数存在一个独一无二的,由 0 和 1 构成的指纹,它与剩下的指纹构成一整套位数相同的指纹集。每张卡片就对应这套指纹集的某一位,卡片数就对应着这套指纹集的位数。我会将你的指纹位数为 1 的卡片舍去。剩下来的卡片(剩下来的位数),对于其他所有的数(指纹)来说,至少出现一次 1。

举个例子(舍去的位数(卡片)我统一补零,方便理解,0 表示无这个数字):

可以成功的指纹集(操作后只有自己是全零):

  1. 三张卡片(三位指纹),三个数字(三个指纹)。
    指纹集~~~~~(011,101,110)C(2,3)
    一号指纹操作后(000,100,100),可以判断成功
    二号指纹操作后(010,000,010),可以判断成功
    三号指纹操作后(001,001,000),可以判断成功

  2. 四张卡片(四位指纹),六个数字(六个指纹)。
    指纹集~~~~~(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),可以判断成功

不可以成功的指纹集(操作后出现多个全零):

  1. 三张卡片(三位指纹),三个数字(三个指纹)。
    指纹集~~~~~(100,110,001)
    一号指纹操作后(000,010,001),可以判断成功
    二号指纹操作后(000,000,001),不可以判断成功
    三号指纹操作后(100,110,000),可以判断成功

  2. 四张卡片(四位指纹),五个数字(五个指纹)。
    指纹集~~~~~(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 个指纹):

  1. 思维卡互动魔术,排列组合方法。我至少需要 C(3,7)或 C(4,7)等于 35,即 7 张卡片,就能完成所有指纹的识别,至多需要 C(1,32),即 32 张卡片。

  2. 二分法。正常判断某个数,需要 5 步,为此需要特别设计识别它的 5 张卡片。这时我要判断另一个数,这 5 张就不一定够了,就需要加新卡片。最后你发现,为了能完成所有指纹的识别,你设计了好多好多卡片,而这个数一定大于 32。但实际情况是,你第一步判断是否大于 16 之后,你就筛选了卡片,每一步你判断完,你都会筛选卡片,所以你才只需要 5 步。

这个魔术的重点就是不筛选卡片!

第一次更新:

  1. 代码更新,现在兼容 vc2010,不兼容 vc6.0
    vc2010:_stscanf_s,vc6.0:_stscanf
    vc2010:_stprintf_s,vc6.0:_stprintf
  2. 添加了打乱按钮。打乱操作:将同一指纹集中不同编号的指纹两两对调,多次操作后实现打乱,让其中蕴藏的规律更模糊。
  3. 添加了提前结束功能。有的时候运气好,不需要使用全部卡片就能提前得知结果。现在确定唯一解时即可结束,不需要判断剩下的卡片了。
  4. 游戏区域盒子绘制函数更新,边框,填充,位置都更新了。

程序执行效果

完整源代码

////////////////////////////////////////
// 程序:思维卡互动魔术
// 作者: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;
}

参考资料

添加评论