【算法与数据结构】—— 博弈论(高阶篇之尼姆博弈)

博弈论之尼姆博弈

尼姆博弈(Nimm Game):
有任意堆物品,每堆物品的个数是任意的,双方轮流从中取物品,每一次只能从一堆物品中取部分或全部物品,最少取一件,取到最后一件物品的人获胜。

分析:
我们先来看假设有三堆物品时的情况
这种情况最有意思,它与二进制有密切关系,我们用(a,b,c)表示某种局势,则前几种奇异局势如下:
第一种是(0,0,0),此时无论是谁面对该局势,都必败
第二种是(0,n,n),此时只要与对手拿走一样多的物品,最后都将导致(0,0,0)出现。比如对手在这种情况下,其于第二堆物品中先取走k个物品,即局势变为(0,n-k,n),那么轮到我们的时候,我们就在第三堆中也取走k个物品,即局势变为(0,n-k,n-k)。这样不断重复下去,最终对手总会先把某堆物品取光,那么等到我的回合时我再将剩余那一堆的物品取光。于是最终出现奇异局势(0,0,0)给对方
接下来分析一下当三堆物品均不为0时的第一个奇异局势:(1,2,3),此时无论先手怎么拿,接下来对手都可以将其变为(0,n,n)的情形,从而使得对方输掉比赛。比如:
若先手在第一堆中取走1个物品,即局势变为(0,2,3),那么后手就在第三堆中取走1个物品,使局势变为(0,2,2)。此时出现局势(0,n,n)给对方;
若先手在第二堆中取走1个物品,即局势变为(1,1,3),那么后手就在第三堆中取走3个物品,使局势变为(1,1,0)。此时出现局势(n,n,0)给对方;
若先手在第二堆中取走2个物品,即局势变为(1,0,3),那么后手就在第三堆中取走2个物品,使局势变为(1,0,1)。此时出现局势(n,0,n)给对方;
若先手在第三堆中取走1个物品,即局势变为(1,2,2),那么后手就在第一堆中取走1个物品,使局势变为(0,2,2)。此时出现局势(0,n,n)给对方;
若先手在第三堆中取走2个物品,即局势变为(1,2,1),那么后手就在第二堆中取走2个物品,使局势变为(1,0,1)。此时出现局势(n,0,n)给对方;
若先手在第三堆中取走3个物品,即局势变为(1,2,0),那么后手就在第二堆中取走1个物品,使局势变为(1,1,0)。此时出现局势(n,n,0)给对方;

在离散数学中有一种逻辑运算叫做“异或”,其算术符号为“⊕”
这个运算的法则是“相同为0,不同为1”,即1⊕1=0,0⊕0=0,而1⊕0=1,0⊕1=1
我们来看一下奇异局势(1,2,3)在进行异或运算之后的结果:
首先先将1(二进制为01)和2(二进制为10)进行异或运算:
01
10
——
11
然后将得到的结果再与3(二进制为11)进行异或运算:
11
11
——
00
发现最终的结果为0
对于奇异局势(0,n,n)也一样,结果也是0
实际上,我们不难发现,对于任何奇异局势(a,b,c)都有a⊕b⊕c = 0
因此对于某个给出的局势(a,b,c),我们判断其是否为奇异局势的方法就是判断a⊕b⊕c是否为0

注意到对于任何数a,都有a⊕a=0
且根据异或运算的结合律和交换律我们不难得到 a⊕b⊕(a⊕b)=(a⊕a)⊕(b⊕b)=0⊕0=0
所以从一个非奇异局势向一个奇异局势转换的方式可以是:
① 使 a = c⊕b
② 使 b = a⊕c
③ 使 c = a⊕b
比如说对于某个局势(14,21,39)
由于14⊕21=27,而39-27=12,所以从39中拿走12个物体即可达到奇异局势(14,21,27)
又比如说对于局势(55,81,121)
由于55⊕81=102,而121-102=19,所以从121中拿走19个物品就形成了奇异局势(55,81,102)

总结:
以上谈论的全都是关于n=3时的情况,而实际上,我们把n(n≥3)推广,对于上述的推理依然是成立的
即,判断某个局势(a1,a2,……,an)是否为奇异局势,我们的方法是将a1⊕a2⊕……⊕an
若得到的结果为0则说明其为一个奇异局势,否则不是
而将一个非奇异局势转换为一个奇异局势转换的方法可以是:
(1) 使 a1 = a2⊕……⊕an
……
(i) 使 ai = a1⊕……⊕a(i-1)⊕a(i+1)……⊕an
……
(n) 使 an = a1⊕……⊕an-1



---经典题型---



HDU2176 取(m堆)石子游戏
问题描述
m堆石子,两人轮流取,只能在1堆中取,取完者胜。先取者负输出No,先取者胜输出Yes,然后输出怎样取子。例如5堆 5,7,8,9,10先取者胜,先取者第1次取时可以从有8个的那一堆取走7个剩下1个,也可以从有9个的中那一堆取走9个剩下0个,也可以从有10个的中那一堆取走7个剩下3个。

输入数据
输入有多组。每组第1行是m(m<=200000),后面m个非零正整数,m=0时退出。

输出数据
先取者负输出No,先取者胜输出Yes,然后输出先取者第1次取子的所有方法。如果从有a个石子的堆中取若干个后剩下b个后会胜就输出a b。参看样例输出

样例输入
2
45 45
3
3 6 9
5
5 7 8 9 10
0

样例输出
No
Yes
9 5
Yes
8 1
9 0
10 3


分析:
这道题是实际就是尼姆博弈
题目的要求是,对于其输入的某个局势先判断其是否为奇异局势
是则表示先手必输,输出”No”;反之则先手必胜,输出”Yes”
对于非奇异局势,还要求你将所有的能让该非奇异局势转换为奇异局势的方案都输出
比如对于局势(3,6,9),由于该局势为非奇异局势,故先手必胜,因此需要先输出”Yes”
接着在这个局势中,发现只有将9变为5才能使得该非奇异局势变为奇异局势(3,6,5)
故还需要再在下一行输出”9 5”
显然,本题的难点也正是在这个输出取子方案上

首先要知道,判断某个局势(a1,a2,……,an)是非为奇异局势的方法是将a1⊕a2⊕……⊕an
如果得到的结果为0就说明该局势是奇异局势
而转换方案是:
(1) 使 a1 = a2⊕……⊕an
……
(i) 使 ai = a1⊕……⊕ai-1⊕ai+1……⊕an
……
(n) 使 an = a1⊕……⊕an-1
那么我们在判断了某个局势是否奇异之后,就可以通过枚举的方式来把上面的 a1-an都求出来,从而得到某个ai应该剩下多少才能形成一个奇异局势
但是需要注意的是,并不是对于每个求出的ai都是可以的
比如说对于局势(3,6,9),可以很容易算出a1= a2⊕a3 = 6⊕9 = 15
但是15>3,我们显然无法将第一堆中的物品(3个)拿成15个,因此我们还需要对每个枚举出的结果进行判断,而判断的正是a1⊕……⊕ai-1⊕ai+1……⊕an是否小于初始局势中的对应的ai
对于计算ai,这里也有个技巧。我们不必在每次的枚举中都计算a1⊕……⊕ai-1⊕ai+1……⊕an,这样显然极易超时。实际上,我们在一开始就把a1⊕……⊕an的结果放进某个变量var中,在之后求某个ai时,仅仅只需要用var⊕ai就可以了(根据异或运算的性质:n⊕n=0,n⊕0=n):
var ⊕ ai = (a1⊕……⊕ai-1⊕ai⊕ai+1⊕……⊕an ) ⊕ ai
= a1⊕……⊕ai-1⊕ (ai⊕ai) ⊕ai+1⊕……⊕an
= a1⊕……⊕ai-1⊕ 0 ⊕ai+1⊕……⊕an
= a1⊕……⊕ai-1⊕ai+1⊕……⊕an = ai


下面直接给出本题的完整代码:

#include<iostream>
using namespace std;
const int MAX=200010;
int ary[MAX];
int main()
{
	int m,var,temp;
	while(cin>>m)
	{
		if(m==0) break;
		var=0;
		for(int i=0;i<m;i++){
			cin>>ary[i];
			var^=ary[i];
		}
		if(var==0) cout<<"No"<<endl;
		else{
			cout<<"Yes"<<endl;
			for(int i=0;i<m;i++){
				temp=var^ary[i];
				if(temp<=ary[i]) cout<<ary[i]<<" "<<temp<<endl;
			}
		}
	}
	return 0;
}
发布了38 篇原创文章 · 获赞 75 · 访问量 9520

猜你喜欢

转载自blog.csdn.net/the_ZED/article/details/104360776