【蓝桥杯】 高僧斗法 (C++)(博弈论问题)

在解决高僧斗法(博弈论问题)之前我们要了解下什么是***平等组合游戏***

1.平等组合游戏
两人游戏。
两人轮流走步。
有一个状态集,而且通常是有限的。
有一个终止状态,到达终止状态后游戏结束。
游戏可以在有限的步数内结束。
规定好了哪些状态转移是合法的。
所有规定对于两人是一样的。

2.当确定这是一个平等组合游戏的问题之后,我们就可以使用博弈论的结论来解决了。在解决这个问题之前,我们通过一个经典的博弈论问题来看这个结论如何使用的。

3.经典的Nim游戏

两个选手面前有n堆石子,每堆石子个数分别为a1、a2…an;
游戏规则:
两名选手A、B轮流任选一个石子堆拿石头(每名选手都不会失误),每次拿的石子数必须大于0;
那么问A先手拿多少才能保证稳赢(即先手赢)。

我们可以看出这是一个平等组合游戏,我们就可以使用Nim类游戏的博弈论结论了

博弈论结论:
对于一个Nim游戏的局面(a1,a2,…,an),它是P-position(后手赢)当且仅当a1 ^ a2 ^ …^ an = 0 时其中^表示异或(xor)运算。反之不等于0时,先手赢。

那么若A先手,A要想稳赢,就要选一堆石子(如第i堆),从第i堆拿走k个石子后,使a1 ^ a2 …^ (ai - k) ^ an = 0;那么就可以保证第一次拿完之后B属于先手,此时B必输,后手A赢。

注:博弈论的证明可参考这位大佬的文章 (我没看懂。。以后有时间在研究)https://blog.csdn.net/wang3312362136/article/details/79303794

4.该回归正题我们要解决的高僧斗法问题了。。。

问题描述
  古时丧葬活动中经常请高僧做法事。仪式结束后,有时会有“高僧斗法”的趣味节目,以舒缓压抑的气氛。
  节目大略步骤为:先用粮食(一般是稻米)在地上“画”出若干级台阶(表示N级浮屠)。又有若干小和尚随机地“站”在某个台阶上。最高一级台阶必须站人,其它任意。(如图1所示)
  两位参加游戏的法师分别指挥某个小和尚向上走任意多级的台阶,但会被站在高级台阶上的小和尚阻挡,不能越过。两个小和尚也不能站在同一台阶,也不能向低级台阶移动。
  两法师轮流发出指令,最后所有小和尚必然会都挤在高段台阶,再也不能向上移动。轮到哪个法师指挥时无法继续移动,则游戏结束,该法师认输。
  对于已知的台阶数和小和尚的分布位置,请你计算先发指令的法师该如何决策才能保证胜出。
输入格式
  输入数据为一行用空格分开的N个整数,表示小和尚的位置。台阶序号从1算起,所以最后一个小和尚的位置即是台阶的总数。(N<100, 台阶总数<1000)
输出格式
  输出为一行用空格分开的两个整数: A B, 表示把A位置的小和尚移动到B位置。若有多个解,输出A值较小的解,若无解则输出-1。
样例输入
1 5 9
样例输出
1 4
样例输入
1 5 8 10
样例输出
1 3

这道题我们可以把问题简化成Nim拿石子问题,怎么简化的呢??
接下来想想:
如果我们把台阶上的人两两成对,然后把两个人间的台阶数看成石子数,每移动一次人,相应的距离不就变小了。不就相当于拿走了几个“台阶”吗,那不就成了Nim问题了。但是有个问题,两队之间的台阶怎么办(比如第1个人和第2个人成一堆,第3个人和第4个人成一堆,那第2个和第3个人之间的台阶怎么办?)。
我们想一个问题。假如A第一次移动后保证可以稳赢,那么B即使移动了每对人的后一个(如2),家下来A让可以移动相应对的第一个相同的步数,使其恢复原来的状态。所以我们不用考虑两个堆之间的台阶数。

具体实现代码注释:(有了这个结论问题超简单了)

#include<iostream>
#include<sstream> 

int A[120] = {0}; //用于存放输入的数据 
int B[60] = {0};//用于存放相邻两个人间的台阶数。 

using namespace std;
int main(){
	string s;
	getline(cin,s);
	istringstream ss(s);
	int n = 0;
	int e;
	while(ss >> e){ //获取输入 
		A[n++] = e;
	}
	int t1 = 0;
	int sum = 0;
	for(int i = 0;i < n - 1;i++){//这里我们计算出所有相邻两个人间的台阶数 
		B[i] = A[i + 1] - A[i] - 1;
	}
	for(int j = 0;j < n - 1;j += 2){//我们只对(0,1) (2,3)...这样的堆进行
									//异或运算,前面我们介绍了为什么不考虑(1,2) 
		sum ^= B[j];
	}
	if(sum == 0){//当为0时说明后手赢,那么A是没有机会的,因为他俩都很聪明,没有失误 
		cout<<"-1";
		return 0;
	}
	for(int i = 0;i < n - 1;i++){//因为要找稳赢的第一步,且数要最小,因此从i=0开始遍历B 
		for(int j = 1;j <= B[i];j++){//当计算到B[i](B[i]表示i人和i+1人间的台阶数)这个区间时,枚举所有这个区间内步数 
			sum = 0;
			B[i] -= j;//表示把i这个人向上走j个台阶,那么B[i]要减少j 
			if(i != 0){ //当i!=0时,此时移动会影响前一个区间(区间变大) 
				B[i - 1] += j;//变大j 
			}
			for(int j = 0;j < n - 1;j += 2){ //A做完决策后,进行计算 
				sum ^= B[j];
			}
			if(sum == 0){//后手赢,说明A第一次决策完后,正好轮到B决策,那么A就成了后手,sum==0,就是表示后手赢。 
				cout<<A[i]<<" "<<A[i] + j;//输出结果 
				return 0;
			}else{//sum!=0, 先手赢,说明A第一次决策完后,正好轮到B决策,
				//B是先手,B会必赢,则A第一次的决策是错误的 ,要撤回操作。 
				B[i] += j;
				if(i != 0){
					B[i - 1] -= j;
				}
			}
		}
	}
	return 0;
} 

猜你喜欢

转载自blog.csdn.net/weixin_39909619/article/details/88385245