zoukankan      html  css  js  c++  java
  • 算法学习————SG函数和SG定理

    其实我自己也不是很明白吧,之前考过几次博弈但我都觉得太难没学SG

    SG函数应用的场景

    组合游戏

    在竞赛中,组合游戏的题目一般有以下特点

    1. 题目描述一般为A,B,2人做游戏

    2. A,B交替进行某种游戏规定的操作,每操作一次,选手可以在有限的操作(操作必须合法)集合中任选一种。

    3. 对于游戏的任何一种可能的局面,合法的操作集合只取决于这个局面本身,不取决于其它因素(跟选手,以前的所有操作无关)

    4. 如果当前选手无法进行合法的操作,则为负

    必胜点和必败点的概念

    必败点(P点) 前一个(previous player)选手将取胜的点称为必败点

    必胜点(N点) 下一个(next player)选手将取胜的点称为必胜点

    SG函数

    先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3

    对于任意状态x,定义SG(x) = mex(S),其中S是x后继状态的SG函数值的集合。如x有三个后继状态a,b,c的SG值分别为SG(a),SG(b),SG(c),那么SG(x)=mex{SG(a),SG(b),SG(c)}。 这样当我没有后继状态的时候集合S的终态必然是空集,所以SG函数的终态为SG(x)=0,当且仅当x为必败点P时。

    虽然我也不知道为什么要这么求,但是数学就是这么神奇呀

    SG定理

    游戏和的SG函数等于各子游戏的SG函数的Nim和。

    公式说明:(SG(x_1,x_2,x_3dots x_n) = SG(x_1)igoplus SG(x_2)dotsigoplus SG(x_n))

    应用

    具体怎么应用呢?我可以递归求出一部分情况的SG值,然后瞪眼发现规律

    例题:[SDOI2009]E&D

    为了好好的总结,我应该是以后多会按照这个套路来写SG了

    首先肯定是要先打表了,递归把所有他的后继情况求出来回溯的时候对于所有后继状态取mex

    一个小tips:递归的时候数组可能会发生改变所以我这里vis定义在函数内,不过这要视写的代码而定

    毕竟考场上没人告诉你对错,对着错误的表推结论,肯定是推不出来的,大家还是要谨慎一些

    打表的代码:

    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    #include <map>
    #define B cout<<"Breakpoint"<<endl;
    #define O(x) cout<<#x<<" "<<x<<" "<<endl;
    #define o(x) cout<<x<<" "<<x<<" ";
    using namespace std;
    int read(){
    	int x = 1,a = 0;char ch = getchar();
    	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
    	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
    	return x*a;
    }
    const int maxn = 1e5+10; 
    int T;
    map<pair<int,int>,int> mp;
    int getSG1(int a,int b){
    	bool vis[maxn];
    	if (mp.count(make_pair(a,b))) return mp[make_pair(a,b)];
    	memset(vis,0,sizeof(vis));
    	for (int i = 1;i < a;i++) vis[getSG1(i,a-i)] = 1;
    	for (int i = 1;i < b;i++) vis[getSG1(i,b-i)] = 1;
    	for (int i = 0;i <= 10000;i++) if (!vis[i]){mp[make_pair(a,b)] = i;break;}
    	return mp[make_pair(a,b)];
    }
    int a[maxn],ans[15][15];
    int main(){
    	for (int i = 1;i <= 20;i++){
    		for (int j = 1;j <= 20;j++){
    			ans[i][j] = getSG1(i,j);
    		}
    	}
    	for (int i = 1;i <= 20;i++){
    		for (int j = 1;j <= 20;j++) cout<<ans[i][j]<<" ";
    		cout<<endl;
    	}
    	return 0;
    }
    

    然后这个表打出来应该是这个样子的:

    我们不难发现以下几点:(因为我注意到网上题解都是说,不难发现规律,但是直接给了结论)

    • 相同的数字连在一起的最小单位是个3个方格的三角形,我们就可以看成是两边(横纵坐标有且仅有一个为奇数)等于中间

    • 0的位置横纵坐标都是奇数

    • 由第一条我们可以想,能否解决中间的块,就能解决两边的块,这个我觉得会稍微难一点

    对于中间块(x,y)的SG值,他等于(x/2,y/2)的SG值+1,也就是说每个对角线上2的幂的位置是递增的

    因为我会除以2,所以这个复杂度是一个log的,我们可以求出每一组石子的SG值,然后看异或起来是否等于0就能解决此题了

    代码:

    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    #include <map>
    #define B cout<<"Breakpoint"<<endl;
    #define O(x) cout<<#x<<" "<<x<<" "<<endl;
    #define o(x) cout<<x<<" "<<x<<" ";
    using namespace std;
    int read(){
    	int x = 1,a = 0;char ch = getchar();
    	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
    	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
    	return x*a;
    }
    int getSG(int x,int y){
    	if (x&1 && y&1) return 0;
    	if (x&1) return getSG(x+1,y);
    	if (y&1) return getSG(x,y+1);
    	return getSG(x >> 1,y >> 1)+1;
    }
    int T,n;
    int main(){
    	T = read();
    	while (T--){
    		n = read();
    		int ans = 0;
    		for (int i = 1;i <= (n >> 1);i++){
    			int x = read(),y = read();
    			ans ^= getSG(x,y);
    		}
    		if (ans == 0) printf("NO
    ");
    		else printf("YES
    ");
    	} 
    	return 0;
    } 
    
  • 相关阅读:
    NFC Basics(基本NFC)——翻译自developer.android.com
    【LeetCode】Sort Colors 解题报告
    发送手机物理标识请求
    Java编程介绍
    emacs 中使用git diff命令行
    OpenJudge百炼习题解答(C++)--题4074:积水量
    编程之美2.9 斐波那契数列
    Application Architecture Determines Application Performance
    程序包javax.servlet.annotation不存在
    mysql 严格模式 Strict Mode说明
  • 原文地址:https://www.cnblogs.com/little-uu/p/14988624.html
Copyright © 2011-2022 走看看