博弈论与 sg 函数

博弈论

定义 必胜状态先手必胜的状态必败状态先手必败的状态

通过推理,我们可以得出下面三条定理:

  • 定理 1:没有后继状态的状态是必败状态。
  • 定理 2:一个状态是必胜状态当且仅当存在至少一个必败状态为它的后继状态。
  • 定理 3:一个状态是必败状态当且仅当它的所有后继状态均为必胜状态。

对于定理 1,如果游戏进行不下去了,那么这个玩家就输掉了游戏。

对于定理 2,如果该状态至少有一个后继状态为必败状态,那么玩家可以通过操作到该必败状态;此时对手的状态为必败状态——对手必定是失败的,而相反地,自己就获得了胜利。

对于定理 3,如果不存在一个后继状态为必败状态,那么无论如何,玩家只能操作到必胜状态;此时对手的状态为必胜状态——对手必定是胜利的,自己就输掉了游戏。

组合游戏:

  1. 两个玩家轮流操作
  2. 游戏状态集有限,保证游戏是在有限步后结束
  3. 不会出现平局的
  4. (1, 2, 3)的后继状态都是先手必胜状态,因此(1, 2, 3)先手必败

规则:

  1. 必败状态 < - > 所有后继都是必胜状态
  2. 必胜状态 < - > 至少一个后继是必败状态
  3. 没有后继的状态是必败状态

Ferguson 游戏

解法时间复杂度非常大,数据范围大了就不行

1.开始有两个盒子,分别有m,n颗糖。

2.每次移动是将一个盒子清空而把另一个盒子的一些糖拿到被清空的盒子中,使得两个盒子至少各有一颗糖。

3.显然唯一的终态为(1,1)。

4.最后移动的游戏者获胜,(m,n)先手是胜还是败?

分析:

1.每一步k=m+n一定减小,k从小到大递推(DP)

2.代码输出所有k <20的必败状态

3.状态(n,m)和(m,n)是等价的,只输出了n <= m的情况

#include <cstdio>
#include <cstring>
#include <cmath>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <set>
#define ll long long
#include <map>
using namespace std;
bool dp[50][50] = {
    
    0}; // i, j 代表m n ,值代表输赢 

int main() {
    
    
	ios::sync_with_stdio(false);
	dp[1][1] = false;
	for(int k = 3; k < 99; k++) {
    
    
		for(int n = 1; n < k; n++) {
    
    
			int m = k - n;
			bool &w = dp[n][k - n];
			w = false;
			for(int i = 1; i < n; i++){
    
     // 清空右边的,并从左边的放i 个过来 
				if(!dp[i][n - i]) w = true; // 如果下一个阶段为必败点,则当前为必胜点 
			}
			for(int i = 1; i < m; i++) {
    
    
				if(!dp[i][m - i]) w = true;
			}
		}
	}
	/*
	for(int i = 1; i < 50; i++) {
		for(int j = 1; j <= i; j++) {
			cout << j << ' ' << i << ' ' << dp[j][i] << endl;
		}
	}*/
	
	return 0;
}

Chomp! 游戏

m * n 的棋盘,每次可以取走一个方格并拿走它右边和上面的所有方格,拿到左下角1 * 1的人输

给出m,n 问先生必胜还是必败? 答案:先手必胜

那么现在假设先手取最右上角的方格(m,n) ,接下来后手可以取某块石头(a, b) 使得自己进入必胜的局面。

事实上,先手在第一次取的时候就可以取石头(a, b) ,之后完全模仿后手的必胜步骤,迫使后手失败。

于是产生矛盾。因此不存在后手必胜策略,先手存在必胜策略。

注意:这个证明是非构造性存在性证明,也即只是证明了先手必胜策略的存在性,但没有构造出具体必胜策略。

虽然对于一些特殊的情况,比如棋盘是正方形、棋盘只有两行,可以找到必胜策略;但对于一般情况,还没有人能具体给出Chomp的一般性必胜策略。

约数游戏

1 ~ n 个数字。两个人轮流选择一个数,并把它和它的约数擦去,擦去最后一个数的人获胜,问谁会获胜,试着证明:

假设后手必胜: 那么后手有必胜策略。 假设先手拿1, 后手拿x (必胜点),那为啥不先拿x,直接赢了。。1是任何数的约数

Nim 游戏

有若干堆石子,每堆石子的数量ai都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。

(a, b, c) 为必败状态 <-> a ^ b ^ c = 0,称为Nim和

  1. (13, 12, 8) 的Nim和:为 9, 所以先手胜

如何取胜呢? 给对手一个必败状态呀 13 个 拿走4

适用于任一堆,全部异或

除法游戏

n * m 矩阵,元素为2 - 10000的正整数,每次可以选一行中的一个或多个大于1的整数,把他们中的每个数都变成它的某个因子,比如12可以变成1, 2, 3, 4, 6。不能操作的输(面临所有数都是1的情况)

分析:

  1. 每个数包含素因子个数,让一个数变成其因子,等价于拿掉一个或多个因子
  2. 每行对应一个火柴堆,每个数的素因子都可以看作是一根火柴

SG函数和SG定理

  1. 任意状态x,定义SG(x) = mex(S), S = {SG(y) | y 是x 的后继状态}

  2. mex(S) 表示不在S 内的最小非负整数

    • 若x 有5个后继状态,SG值分别为0, 1, 1, 2, 4,则SG(x) = 3,
  3. 终态的SG值显然为0,而其他值递推得出

  4. SG(x) = 0推出x为必败状态

  5. SG定理:游戏和SG函数等于各子游戏SG函数的Nim和

  6. 可以把各个游戏分而治之,简化问题

    • Bouton 定理可以看作SG定理在Nim 游戏中的直接应用,因为单堆Nim 游戏的SG函数满足SG(x) = x

石子游戏

n堆石子,分别a1, a2…an (a的大小在 long long 内)。两个游戏者轮流操作,每次选一堆,拿走至少一个石子,但不能拿走超过一半的石子。比如若有三堆石子,每堆分别有5, 1, 2个,则在下一轮中,游戏者可以从第一堆拿走1个或两个,第二堆不能拿,第三堆只能拿一个,谁不能拿石子就算输。

分析:

  1. 和Nim游戏不同,但是可以看作n个单堆游戏之和

  2. a 的范围很大,不能按照定义推出所有SG的函数值

  3. 写一个递推程序,看看单堆游戏的SG函数有无规律

    #include <cstdio>
    #include <cstring>
    #include <cmath>
    #include <iostream>
    #include <string>
    #include <algorithm>
    #include <vector>
    #include <set>
    #define ll long long
    #include <map>
    using namespace std;
    int main() {
          
          
    	ios::sync_with_stdio(false);
    	cin.tie(0);
    	int sg[100], vis[100];
    	sg[1] = 0;
    	cout << sg[1] << ' ';
    	for(int i = 2; i <= 30; i++) {
          
          
    		memset(vis, 0, sizeof(vis));
    		for(int j = 1; (j << 1) <= i; j++) {
          
          
    			vis[sg[i - j]] = 1;
    		}
    		for(int j = 0; ; j++) {
          
          
    			if(!vis[j]) {
          
          
    				sg[i] = j;
    				break;
    			}
    		} 
    		cout << sg[i] << ' ';
    	}
    	return 0;
    }
    

    打印的结果:0 1 0 2 1 3 0 4 2 5 1 6 3 7 0 8 4 9 2 10 5 11 1 12 6 13 3 14 7 15

  4. n为偶数时,sg(n) = n / 2, 但是为奇数似乎没有什么规律

  5. n为奇数的时候:sg(n) = sg(n / 2)

#include <cstdio>
#include <cstring>
#include <cmath>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <set>
#define ll long long
#include <map>
using namespace std;
ll sg(ll x) {
    
    
	return x % 2 == 0 ? x / 2 : sg(x / 2); 
}
int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0);
	int t;
	cin >> t;
	while(t--) {
    
    
		int n;
		ll a, v = 0;
		cin >> n;
		for(int i = 0; i < n; i++) {
    
    
			cin >> a;
			v ^= sg(a); // Nim 和
		}
		if(v) cout << "YES\n";
		else cout << "No\n";
	}
	return 0;
}

Treblecross 游戏

有n≤200个格子排成一行,其中一些格子里有字符x。两个游戏者轮流操作,每次可以选一个空格,在里面放上字符x。如果此时有3个连续的x出现,则该游戏者赢得比赛。初始情况下不会有3个x连续出现。你的任务是判断先手必胜还是必败,如果必胜,输出所有必胜策略。

分析:

  1. 如果有XX 或者X.X ,一定先手胜

  2. 所以我们不会在一个X的旁边以及旁边的旁边放X

  3. 整个游戏被x 和旁边的“禁区”分成了若干独立片段,每次可选择一个片段进行游戏,谁无法继续游戏就算输,这就是若干游戏的和

    口口OOXOO口口口口口 两边的口为两个子游戏,中间的OX 为禁区

  4. g(x) 为x个格子对应游戏的sg值

  5. g(x) = mex{g(x - 3), g(x - 4), g(x - 5), g(1) ^ g(x - 6), g(2) ^ g(7)…} 边界:g(0) = 0, g(1) = g(2) = g(3) = 1

    1. g(x - 3) 对应把x放在最左边各子,注意到最左边的3个格子都成为了禁区,剩下x - 3个
    2. g(2) ^ g(x - 7) 对应上述中的局面,左边两个各子和右边x - 7 个各子是两个独立的子游戏
  6. 计算初始局面的SG函数。枚举所有策略后继sg值为0的决策就是所求决策

https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=477&page=show_problem&problem=1502

其他情况:每个X旁边都有四个禁区(左右各两个),禁区用i表示,即…iiXii…,显然放到i这个地方是先手必败的

把整张图的禁区全部标记出来,问题变成了往没被标记的区域放X,没放一个X将有iiXii中的i被标记,被标记的格子不能走,问先手必胜/必败

每一段没被标记的区域都是一个子游戏,sg[i]表示长度为i的子游戏sg值

预处理即可

方案只需要枚举第一步走到哪里,重新判断该局面的SG函数是否为0即可

#include <cstdio>
#include <cstring>
#include <cmath>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <set>
#define ll long long
#include <map>
#define maxn 200

using namespace std;
int g[maxn + 10];

bool winning(char *state) {
    
    
	int n = strlen(state);
	for(int i = 0;  i < n - 2; i++) {
    
    
		if(state[i] == 'X' && state[i + 1] == 'X' && state[i + 2] == 'X') return false;
	}
	int no[maxn + 1]; // no[i] = 1 表示下标为1的格子是"禁区" , 离"X" 距离不超过2
	memset(no, 0, sizeof(no));
	no[n] = 1; // 哨兵
	for(int i = 0; i < n; i++) if(state[i] == 'X') {
    
    
		for(int d = -2; d <= 2; d++) {
    
    
			if(i + d >= 0 && i + d < n) {
    
    
				if(d != 0 && state[i + d] == 'X') return true; // 有两个距离不超过2的'X' 获胜
				no[i + d] = 1; 
			}
		}
	}
	
	int sg = 0, start = -1; // 当前子游戏的起点
	for(int i = 0;  i <= n; i++) {
    
    
		if(start < 0 && !no[i]) start = i;
		if(no[i] && start >= 0) sg ^= g[i - start];
		if(no[i]) start = -1;
	} 
	return sg != 0;
}

int mex(vector<int>& s) {
    
    
	if(s.empty()) return 0;
	sort(s.begin(), s.end());
	if(s[0] != 0) return 0;
	for(int i = 1; i < s.size(); i++) {
    
    
		if(s[i] > (s[i - 1] + 1)) return s[i - 1] + 1;
	}
	return s[s.size() - 1] + 1;
}
// 预处理g 数组
void init() {
    
    
	g[0] = 0;
	g[1] = g[2] = g[3] = 1;
	g[4] = g[5] = 2;
	for(int i = 4; i <= maxn; i++) {
    
    
		vector<int> s;
		s.push_back(g[i - 3]); // 放X在最左边 下标为0的格子
		s.push_back(g[i - 4]); // 放X在下标为1的格子
		if(i >= 5) s.push_back(g[i - 5]); // 放X在下标为2 的格子
		for(int j = 3;  j < i - 3; j++) {
    
    
			if(i - j - 3 >= 0) {
    
    
				s.push_back(g[j - 2] ^ g[i - j - 3]); // 子游戏
			}
			g[i] = mex(s);
		}
	}
}
int main() {
    
    
	ios::sync_with_stdio(false);
	init();
	int T;
	/*
	for(int i = 0; i < maxn; i++) {
		cout << g[i] << ' ';
	}
	*/
	cin >> T;
	while(T--) {
    
    
		char state[maxn + 10];
		cin >> state;
		int n = strlen(state);
		if(!winning(state)) cout << "LOSING\n\n";
		else {
    
    
			cout << "WINNING\n";
			vector<int> moves;
			for(int i = 0; i < n; i++) if(state[i] == '.') {
    
    
					state[i] = 'X';
					if(!winning(state)) moves.push_back(i + 1);
					state[i] = '.';
			}
			cout << moves[0];
			for(int i = 1; i < moves.size(); i++) {
    
    
				cout << ' ' << moves[i];
			}
			puts("");
		}
	}
	return 0;
}

盒子游戏(BoxGame,UVa12293)

  1. 两个相同的盒子,一个有n(n≤10%)个球,另一个有1个球。Alice,Bob 2人轮流操作。

  2. 每次清空球较少的那个盒子(如果球数相同,任意一个),从另一个里拿些球到这个,两个盒子非空。

  3. 无法操作 <==> (两盒子都只有1球) 者输。判断先手(A)胜还是后手(B)胜。

题解:

  1. 由于每次都是倒掉数量少的那个盒子,再对数量多的盒子进行分割,所以可以把规则简化为:初始时有n个球,每次只能拿走不多于n/2的球,最终状态为1个球,达到这个状态的玩家获胜。

  2. 简化游戏规则之后,可知这是一个典型的SG博弈,但是由于n的范围很大,不能直接求SG值,那就打表找规律,如下:

#include<bits/stdc++.h>
#define ll long long
#define maxn 25
int sg[maxn];
int vis[1000] = {
    
    0};
using namespace std;

void init() {
    
    
	sg[1] = 0;
	for(int i = 2; i<=35; i++) {
    
    
		memset(vis, 0, sizeof(vis));
		for(int j = (i + 1) / 2; j < i; j++) vis[sg[j]] = 1;
		for(int j = 0; ; j++) if(!vis[j]) {
    
    
				SG[i] = j;
				break;
			}
	}
	for(int i = 1; i<=32; i++) printf("%-2d ",i);
	putchar('\n');
	for(int i = 1; i<=32; i++) printf("%-2d ",sg[i]);
	putchar('\n');
}
int main() {
    
    
	init();
	return 0;
}

可知,当n为 2^i - 1时,先手输;否则先手赢

类Nim游戏

有N(N≤20000)堆石子,第i堆有a个(1≤a,$10%)。两人轮流取,每次可以选择一堆,取一或多个(可以取完),但不能同时在两堆或更多堆中取。第一个人可以任选一堆取,但后面每次取时须遵守以下规则:

1.如果对手刚才没有把一堆石子全部取走,则他只能继续在这堆石子里取;

2.只有当对手把一堆石子全部取走时,他才能换一堆石子取。谁取到最后一个石子就赢。假定游戏双方都绝顶聪明,谁会赢?

分析:

  1. 假设m个1, k个其他数组,分情况讨论:

  2. 情况1:k = 0, m若为奇数则先手胜

  3. 情况2:k > 0 :

    1. m为奇数:先手挨个把非1的堆取成1,之后就先手胜
    2. m为偶数:先手把k - 1个非1的堆取成1, 再把最后一个堆取完,给对手一个必败的局面,先手胜
  4. 每个数字为1, 且总数为偶数,先手败

猜你喜欢

转载自blog.csdn.net/qq_45560445/article/details/121589361