前言:
笔者有个很喜欢玩也很会玩这个游戏的同学,并且每次都玩不过他,于是怀疑他是不是摸索到了这个游戏必胜的诀窍。这位同学自称找到了“后手必胜的秘诀”,笔者却认为如此复杂的游戏并不存在“必胜”一说,于是两人打赌,赌注为若干瓶可乐。
本打算趁着周末证实“必胜”不存在,以此白嫖他几瓶可乐,没想到认真探索求证后,倒是把对方的观点给证实了(笑哭)。不过,虽然白给了可乐,但是获得了真理!原先以为这个游戏的输赢是随机的,不过咱今天终于从鼓里出来了:总当后手,总输,不是没有原因的。
游戏规则:
为了避免读者感觉结果和预期不符并且阅读完源码才发现是规则和自己想的不同导致的,在这里先描述一下游戏规则:
两个人,一开始每个人的双手都是1,然后先手玩家开始操作,即用自己的任意一只手“碰”对方的任意一只手,效果是自己那只手上的数字变成了碰之前的数和对方被碰的那只手的数的和。然后后手玩家再碰,再先手碰......(轮流)。在一个玩家的一次“碰”中,不能用自己已经消了(>=10)的手碰对方任意的手,也不能用自己任意的手碰对方已经消了的手,如果在一次“碰”后,自己的这只手>=10了,那么这只手就“消了”。直到有一个玩家的两只手全部“消了”,那么这个玩家胜出,游戏结束。
结论:
重要的放在前头,先说笔者得到的结论:
到10即消模式下,根据我使用C++验证以及那位同学的手算,结果为:后手必胜。(当然,如果你后手总是输的话,只是经验不够)
如果你想知道在初始情况为“对方和自己的双手都是1”时,后手是否必胜或者先手是否必胜,见下图(根据笔者计算,这个游戏不是“先手必胜”就是“后手必胜”)
(由于时间太久,只算到上限为“到64即消”的情况)
计算“先/后手必胜”源码(C++):
#include <iostream>
using namespace std;
int up; //大于等于up即消去,一般为10
struct player //每个玩家有两只手
{
public:
int h1;
int h2;
};
class Node //深搜过程中的节点
{
public:
player xian;
player hou;
bool xian_win = 0; //当前情况是否先手获胜
bool hou_win = 0; //当前情况是否后手获胜
Node(int a, int b, int c, int d)
{
xian.h1 = a;
xian.h2 = b;
hou.h1 = c;
hou.h2 = d;
}
Node() {}
Node& operator=(const Node& node)
{
if (this != &node) // 仅仅减少运算量,没有也没太大关系
{
xian = node.xian;
hou = node.hou;
xian_win = node.xian_win;
hou_win = node.hou_win;
}
return *this;
}
void operation_and_check(int player, int op) //player为1表示先手操作,0表示后手操作,op对应的操作方式见图示
{
if (player == 1)
{
switch (op)
{
case 1:
xian.h1 += hou.h1; break;
case 2:
xian.h1 += hou.h2; break;
case 3:
xian.h2 += hou.h1; break;
case 4:
xian.h2 += hou.h2; break;
}
}
else
{
switch (op)
{
case 1:
hou.h1 += xian.h1; break;
case 2:
hou.h1 += xian.h2; break;
case 3:
hou.h2 += xian.h1; break;
case 4:
hou.h2 += xian.h2; break;
}
}
if (xian.h1 >= up && xian.h2 >= up)
xian_win = 1;
else
xian_win = 0;
if (hou.h1 >= up && hou.h2 >= up)
hou_win = 1;
else hou_win = 0;
}
void operation_and_check_back(int player, int op) //是operation_and_check函数的反向操作
{
if (player == 1)
{
switch (op)
{
case 1:
xian.h1 -= hou.h1; break;
case 2:
xian.h1 -= hou.h2; break;
case 3:
xian.h2 -= hou.h1; break;
case 4:
xian.h2 -= hou.h2; break;
}
}
else
{
switch (op)
{
case 1:
hou.h1 -= xian.h1; break;
case 2:
hou.h1 -= xian.h2; break;
case 3:
hou.h2 -= xian.h1; break;
case 4:
hou.h2 -= xian.h2; break;
}
}
if (xian.h1 >= up && xian.h2 >= up)
xian_win = 1;
else
xian_win = 0;
if (hou.h1 >= up && hou.h2 >= up)
hou_win = 1;
else hou_win = 0;
}
void print() //打印当前局势
{
cout << "xian: " << xian.h1 << " " << xian.h2 << endl;
cout << "hou: " << hou.h1 << " " << hou.h2 << endl;
}
};
bool judge(Node node, int player, int op) //返回1表示当前操作不能完成,由于其中1或2个“操作数(手)”已经消掉了
{
if (player == 1)
{
switch (op)
{
case 1:
if (node.xian.h1 >= up || node.hou.h1 >= up)
return 1;
break;
case 2:
if (node.xian.h1 >= up || node.hou.h2 >= up)
return 1;
break;
case 3:
if (node.xian.h2 >= up || node.hou.h1 >= up)
return 1;
break;
case 4:
if (node.xian.h2 >= up || node.hou.h2 >= up)
return 1;
break;
}
}
else
{
switch (op)
{
case 1:
if (node.xian.h1 >= up || node.hou.h1 >= up)
return 1;
break;
case 2:
if (node.xian.h2 >= up || node.hou.h1 >= up)
return 1;
break;
case 3:
if (node.xian.h1 >= up || node.hou.h2 >= up)
return 1;
break;
case 4:
if (node.xian.h2 >= up || node.hou.h2 >= up)
return 1;
break;
}
}
return 0;
}
void print(int* v, int k) //打印v[1:k]
{
for (int i = 1; i <= k; ++i)
{
cout << v[i] << " ";
}cout << endl;
}
int main()
{
int xian_hand1;
int xian_hand2;
int hou_hand1;
int hou_hand2;
cout << "请任意输入一种双方手指表示的数字的情况,以作为初始情况:" << endl;
cout << "输入先手玩家的第一只手的数字:";
cin >> xian_hand1;
cout << "输入先手玩家的第二只手的数字:";
cin >> xian_hand2;
cout << "输入后手玩家的第一只手的数字:";
cin >> hou_hand1;
cout << "输入后手玩家的第二只手的数字:";
cin >> hou_hand2;
for (int i = 2; i <= 100; ++i) //去掉for循环,确定up的值,即可只求出一种特定情况下是否后手必胜或者先手必胜。
{
up = i;
cout << "当两个数字的和大于等于" << up << "即被消掉时,";
Node node(xian_hand1, xian_hand2, hou_hand1, hou_hand2);
Node temp; //深搜时用于试探一个分支对当前node的改变
int v[200] = { 0 }; //记录路径
int k = 1; //表示节点在树中的层,根节点为1层,当k奇数,当前分支的选择是先手做出的,若偶数,则是后手做出选择。
int win = 2; //表示k指向的那个节点是否具有后手必胜的性质,0不具有,1具有,2未知或错误
while (k >= 1) //深搜
{
if (v[k] < 4)
{
v[k]++;
temp = node;
temp.operation_and_check(k % 2, v[k]);
if (judge(node, k % 2, v[k]) == 1) //此限界保证操作可行性可行
continue;
if (temp.hou_win == 1 || temp.xian_win == 1) //答案节点
{
//print(v, k);//用于打印答案节点
//temp.print();
//cout << endl;
if (temp.hou_win == 1 && k % 2 == 0)
{
//cout << "1 置v[" << k << "]=4" << endl;
v[k] = 4;
win = 1;
}
else if (temp.xian_win == 1 && k % 2 == 1)
{
//cout << "2 置v[" << k << "]=4" << endl;
v[k] = 4;
win = 0;
}
}
else //向下继续展开,更新迭代量
{
win = 2;
k++;
node = temp;
}
}
else
{
v[k] = 0;
k--;
node.operation_and_check_back(k % 2, v[k]);
if (k % 2 == 0 && win == 1) //剪枝,e.g.对于后手来说,若已经找到了一个通向后手必胜的子节点,那就不用再找了。
{
//cout << "3 置v[" << k << "]=4" << endl;
v[k] = 4;
}
else if (k % 2 == 1 && win == 0) //剪枝。e.g.对于先手来说,若已经找到了一个通向后手必输的节点,那就它了,不用再找了。
{
//cout << "4 置v[" << k << "]=4" << endl;
v[k] = 4;
}
}
}
if (win == 1)
cout << "后手必胜" << endl;
else if (win == 0)
cout << "先手必胜" << endl;
else
cout << "error" << endl;
}
return 0;
}
代码简要分析:
整体来看是迭代深搜+限界。每个节点表示当前一种状态(包含两个玩家每个手的数字以及是否获胜的信息等,详见Node类)。在搜索前,初始化用一个根节点node,表示初始状态,然后在循环中进行深搜,同时使用v[]数组记录路径,方便迭代量的迭代。win迭代量是个很关键的迭代量,他表示了当前k指向的节点是否具有“后手必胜”的性质,如果未知用2表示。
限界(剪枝)有三个,一是不能“碰”这一操作的两个操作数中不能有已经“消了”的;二是如果已经有一个玩家的两只手都消了,那么到达答案节点,不继续展开;三是,对于后手选择的分支来说,如果已经找到了一个后手必胜的子节点,那就选择它,不用再找其他的,对于先手选择的分支来说,如果已经找到了一个后手必输的子节点,那就选择它,不用再找其他的(对应于源码221,227,246,251行的条件语句,“v[k]=4”相当于剪枝)。
开头的全局变量up是"用手表示的最大的数+1"即“到up即消去”的意思。可以在for循环中按顺序改变up的值以实现对多种游戏玩法的“后手必胜”性质的求解。
在源码中对于operation_and_check(int player, int op)中的op操作数的解释见下图:
(上图中的箭头可以看做C语言中的+=操作)
例子
举个栗子(输入3 8 8 13)
一些其他功能:
由于这个游戏不是先手必胜就是后手必胜,所以如果想知道一种情况下是否是先手必胜也是很简单的,例如:若输入c d a b得到后手必胜的结果,那么可知输入a b c d是先手必胜的。
改变两个玩家两只手的初始状态,合理运用源码,可以获得“棋谱”一样的效果,帮你在和小伙伴的对战中让你处于不败之地!
从源码来看,为什么这个游戏有先/后手必胜的性质?
如果把两个玩家对战过程看做树上从根节点的发展,那么这条节点链一定是有尽头的(因为手上的数单增,直到>=10消了),这棵树也是一棵从根节点展开的、有最深层数的树。由于每一个节点的win(是否后手必胜的属性)是由他的最多4个子节点决定的,并且每个叶子结点的“后手是否必胜”的属性已知,所以可以倒推至根节点,也就是输入的初始状态下是否后手必胜,这是0或1中的一个值,是确定的。
碰手指游戏源码(和算法对战)
/*
* player和computer进行碰手指游戏,每一个操作(operation)都可以使用1~4的一个数字来描述:
* 1:用当前回合的一方的较小的数碰对方的较小的数,(简记“小小”,以下均采用简记法)
* 2:小大
* 3:大小
* 4:大大
* 在这个游戏中,电脑是先手。
* 在源代码中,你可以修改MAX和VELOCITY宏
* 例如如果你想玩到10就消的游戏模式,那就把MAX修改为9即可
* !!!如果你在一个对局中不想继续进行下去了,在下一次input操作数的时候输入100即可开启下一轮或退出!!!
*/
#include <iostream>
#include <vector>
#include <cassert>
#include <malloc.h>
#include <string>
#include <Windows.h>
//下面两个宏可以根据喜好自行设置。
#define MAX 9 //用手指能表示的最大数字(例如max=9,就是到10即消),max过大可能加载时间太长,不建议超过10
#define VELOCITY 3 //字符输出速度(ms)
//#define TIAOSHI //调试用
int p1_win = 0;
int p2_win = 0;
using namespace std;
void delay_say(string s)
{
for (int i = 0; i < s.length(); ++i)
{
cout << s.at(i);
Sleep(VELOCITY);
}
}
void read_vector(vector<int> v)
{
for (vector<int>::iterator it = v.begin(); it < v.end(); ++it)
{
cout << *it << " ";
}
}
class H {
public:
int val = 1; //这个手的值
bool ok = 0; //这个手是否完成任务
};
class P {
public:
H h1;
H h2;
int h_num = 2; //剩余的手的数量
H* hmin() //每次调用均可取到对应的值
{
H* ret;
if (h1.ok == 0 && h2.ok == 1)
ret = &h1;
else if (h1.ok == 1 && h2.ok == 0)
ret = &h2;
else
{
ret = &h1;
if (h2.val < h1.val)
ret = &h2;
}
return ret;
}
H* hmax() //每次调用均可取到对应的值
{
H* ret;
if (h1.ok == 0 && h2.ok == 1)
ret = &h1;
else if (h1.ok == 1 && h2.ok == 0)
ret = &h2;
else
{
ret = &h1;
if (h2.val > h1.val)
ret = &h2;
}
return ret;
}
};
int judge(vector<int> v, bool describe) //返回值:0:未完成 1:p1赢 2:p2赢 不用考虑步数超过的问题
{
P p1, p2; //p1先手
for (int i = 0; i < v.size(); ++i) //i=0,2,4,6..时对p1操作,其他对p2操作
{
//cout << "NOW is" << v[i] << endl;
int op = v[i];
P* p_me, * p_you;
if (i % 2 == 0)
{
p_me = &p1;
p_you = &p2;
}
else
{
p_me = &p2;
p_you = &p1;
}
switch (op)
{
case 1:
(p_me->hmin())->val += (p_you->hmin())->val;
break;
case 2:
(p_me->hmin())->val += (p_you->hmax())->val;
break;
case 3:
(p_me->hmax())->val += (p_you->hmin())->val;
break;
case 4:
(p_me->hmax())->val += (p_you->hmax())->val;
break;
default:
assert(0);
}
if (p_me->h1.val > MAX && p_me->h1.ok == 0)
{
p_me->h1.ok = 1;
p_me->h_num--;
}
if (p_me->h2.val > MAX && p_me->h2.ok == 0)
{
p_me->h2.ok = 1;
p_me->h_num--;
}
if (describe == 1 && i == v.size() - 1)
{
//cout << "走完第" << i << "步,当前情况为:" << endl;
delay_say("computer: ");
cout << p1.h1.val;
cout << " " << p1.h2.val << endl;
//cout << " (" << p1.h1.ok << ")(" << p1.h2.ok << ")" << endl;
cout << " ";
if (p1.h1.ok == 1) delay_say("消了 ");
else cout << " ";
if (p1.h2.ok == 1) delay_say("消了");
cout << endl;
//cout<< " " << p2.h2.val << endl;
delay_say("player: ");
cout << p2.h1.val;
cout << " " << p2.h2.val << endl;
//cout << " (" << p2.h1.ok << ")(" << p2.h2.ok << ")" << endl;
cout << " ";
if (p2.h1.ok == 1) delay_say("消了 ");
else cout << " ";
if (p2.h2.ok == 1) delay_say("消了");
cout << endl;
//cout << "p1.num is: " << p1.h_num << " p2.hnum is: " << p2.h_num << endl;
}
if (p1.h_num == 0 || p2.h_num == 0) //游戏结束
{
if (i < v.size() - 1)
{
return 3; //序列太长了(测试用)
}
if (p1.h_num == 0)
{
return 1;
}
else
{
return 2;
}
}
}
return 0;//未完成
}
vector<int> op_arr; //当前节点指针p位置对应的操作序列
int height; //当前p指针所在的深度(从0计)
class Son {
public:
class Node* son_ptr = NULL;
float win_psb; //走这个分支能赢的可能性
};
class Node {
public:
int op;
bool is_a_leaf;
class Node* father;
Son son[4];
int judge;
};
Node* new_node()
{
Node* p;
p = (Node*)malloc(sizeof(Node));
p->father = NULL;
p->is_a_leaf = 0;
p->judge = 0;
p->op = 0;
for (int i = 0; i < 4; ++i)
{
p->son[i].son_ptr = NULL;
}
return p;
}
void psb(Node* p)//输出当前p所在节点分支上p1获胜的概率(在每次循环p回溯前调用)
{
//cout <<"\t psb is: " << p->father->son[p->op - 1].win_psb << " ";
//if (p->father->son[p->op - 1].win_psb != 0 && p->father->son[p->op - 1].win_psb != 1)
//{
// cout << p->father->son[p->op - 1].win_psb << endl;
//}
return;
}
Node* Root;
void make_tree()
{
//初始化全局变量
op_arr.push_back(1);
op_arr.push_back(1);
height = 1;
Node* root = new_node();
Root = root;
root->father = (Node*)malloc(sizeof(Node));
root->op = 1;
root->son[0].son_ptr = new_node();
Node* p = root->son[0].son_ptr;
p->op = 1;
p->father = root;
int shuliang = 0;
int progress = 1;//显示加载进度
while (p != root->father)
{
#ifdef TIAOSHI
cout << ++shuliang << " ";
cout << "当前vector:";
for (vector<int>::iterator it = op_arr.begin(); it < op_arr.end(); ++it)
{
cout << *it << " ";
}
#endif
p->judge = judge(op_arr, 0);
/* cout << "judge为:" << p->judge << endl;
cout << "一次" << endl;
for (int i = 0; i < 4; ++i)
{
if (p->son[i].son_ptr == NULL)
{
cout << "son[" << i << "] is NULL" << endl;
}
}*/
if (p->judge == 0) //非叶子结点
{
if (p == root)
{
cout << "已加载" << progress++ << "/4" << endl;
}
//cout << "非叶子";
p->is_a_leaf = 0;
int find_pos = 99999; //位置
Node* sp = NULL;
for (int i = 0; i < 4; ++i)
{
if (p->son[i].son_ptr == NULL)
{
p->son[i].son_ptr = new_node();
sp = p->son[i].son_ptr;
find_pos = i;
break;
}
}
if (sp == NULL) //没有可以继续找的son
{
//cout << " 找不到";
if (height % 2 == 0)//取平均
{
//cout << "取平均";
float sum = 0;
for (int i = 0; i < 4; ++i)
{
sum += p->son[i].win_psb;
}
p->father->son[(p->op) - 1].win_psb = sum / (float)4;
}
else
{
//cout << "取最大";
float max = 0;
for (int i = 0; i < 4; ++i)
{
if (max < p->son[i].win_psb)
{
max = p->son[i].win_psb;
}
}
p->father->son[(p->op) - 1].win_psb = max;
}
height--;
op_arr.pop_back();
if (p->father == p)cout << "No" << endl;
psb(p);//
p = p->father;
}
else //有
{
//cout << "可以找" ;
sp->father = p;
sp->op = find_pos + 1;
p = sp;
op_arr.push_back(p->op);
height++;
}
}
else if (p->judge == 1 || p->judge == 2) //叶子
{
if (p->judge == 1)
p1_win++;
else
p2_win++;
#ifdef TIAOSHI
cout << "\tjudge is: " << p->judge;
#endif
//cout << "叶子" << endl;
p->is_a_leaf = 1;
if (height % 2 == 1)
{
p->father->son[(p->op) - 1].win_psb = 0;
//cout << "返0";
}
else
{
p->father->son[(p->op) - 1].win_psb = 1;
//cout << "返1";
}
psb(p);//
p = p->father;
height--;
op_arr.pop_back();
}
else
{
assert(0);
}
//cout << endl;
#ifdef TIAOSHI
cout << endl;
#endif
}
}
Node* find_node(vector<int> v)
{
Node* p = Root;
assert(v[0] == 1 && v.size() >= 1);
for (int i = 0; i < v.size() - 1; ++i)
{
p = p->son[v[i + 1] - 1].son_ptr;
}
return p;
}
void read_info(vector<int> v)
{
cout << "序列";
read_vector(v);
cout << "对应的节点信息为:" << endl;
cout << "height:" << v.size() - 1 << endl;
Node* p = find_node(v);
if (p->is_a_leaf == 1)
{
cout << "叶子" << endl;
return;
}
for (int i = 0; i < 4; ++i)
{
cout << "son[" << i << "] :" << p->son[i].win_psb << endl;
}
return;
}
void say_round(int r)
{
delay_say("\nRound ");
cout << r;
string s;
switch (r % 10)
{
case 1:
s = "st"; break;
case 2:
s = "nd"; break;
case 3:
s = "rd"; break;
default:
s = "th"; break;
}
if (r % 2 == 0)
delay_say(s + " (computer) :\n");
else
delay_say(s + " (player) :\n");
return;
}
void describe(vector<int> v)
{
cout << "图形化描述:" << endl;
judge(v, 1);
}
int main()
{
delay_say("Please read the comments at the beginning of the source code first!!!\n");
delay_say("the program is running , please wait for a moment ...\n");
make_tree();
//cout << "p1_win:" << p1_win << " p2_win:" << p2_win << endl;
//cout << Root->son[1].son_ptr->son[1].win_psb << endl;
//cout << "玩家随机做出选择情况下,computer获胜概率是:" << 100 * Root->father->son[0].win_psb << "%" << endl;这个概率不太靠谱,当做没这事就好...
//cout << "节点数据查询" << endl;
//while (1)
//{
// vector<int> v;
// int len;
// cin >> len;
// for (int i = 0; i < len; ++i)
// {
// int x; cin >> x;
// v.push_back(x);
// }cout << endl;
// read_info(v);
//}
while (1)
{
A:
delay_say("input \"q\" to quit, input \"b\" to begin!\n");
string s;
cin >> s;
if (s.at(0) == 'q')
{
cout << "quit" << endl;
break;
}
else if (s.at(0) == 'b')
{
cout << "begin(computer will intitate):" << endl;
}
else
{
cout << "请重新输入:" << endl;
goto A;
}
vector<int> op_v;
op_v.push_back(1);
//开始
Node* p = Root;
int round = 0;
delay_say("the initial Situation:\ncomputer:\t1\t1\nplayer:\t\t1\t1\n");
say_round(round++);
delay_say("computer choose 1 as operation\n");
describe(op_v);
while (1)
{
say_round(round);
int op;
if (round % 2 == 1)//player
{
delay_say("please input your operation(1~4): ");
B:
cin >> op;
if (!(op == 1 || op == 2 || op == 3 || op == 4 || op == 100))
{
delay_say("please input again(1~4): ");
goto B;
}
if (op == 100)
break;
}
else //computer
{
op = 0; //分支
for (int i = 1; i < 4; ++i)//1 2 3
{
if (p->son[i].win_psb > p->son[op].win_psb)
{
op = i;
}
}
op++; //操作数
delay_say("computer choose ");
cout << op;
delay_say(" as operation\n");
}
op_v.push_back(op);
describe(op_v);
p = p->son[op - 1].son_ptr; //向下走
if (p->is_a_leaf == 1)
{
if (round % 2 == 1)
{
delay_say("\nyou win!!你太棒了!!\n");
}
else
{
delay_say("\nyou loose!!你不行!!\n");
}
cout << endl;
break;
}
round++;
}
}
return 0;
}
(很久以前写的这个对战程序,感觉有点拉垮,有兴趣的读者可以玩玩看)
运行截图: