图游戏的概念
任何局面都认为是图中的点,每一个局面都可以通过一种行动,走向图中的下一个点
如果当前行动有若干个,那么后继节点就有若干个。最终,必败局面的点认为不再有后继节点
那么公平组合游戏(ICG),就可以对应成一张图
SG函数(Sprague-Grundy函数),如下是SG返回值的求解方式,俗称mex过程
最终必败点是A,规定SG(A) = 0
假设状态点是B,那么SG(B) = 查看B所有后继节点的sg值,其中没有出现过的最小自然数
SG(B) != 0,那么状态B为必胜态;SG(B) == 0,那么状态B为必败态
SG定理(Bouton定理)
如果一个ICG游戏(总),由若干个独立的ICG子游戏构成(分1、分2、分3…),那么:
SG(总) = SG(分1) ^ SG(分2) ^ SG(分3)… 任何ICG游戏都是如此,正确性证明类似尼姆博弈
当数据规模较大时,要善于通过对数器的手段,打印SG表并观察,看看能不能发现简洁规律
SG(x):不在后继节点的最小自然数
题目1:SG函数求解过程展示:巴什博弈
一共有n颗石子,两个人轮流拿,每次可以拿1~m颗石子
拿到最后一颗石子的人获胜,根据n、m返回谁赢
对数器验证
通过观察sg表,一样可以得到巴什博弈最简洁的结论
package class096;
import java.util.Arrays;
// 巴什博弈(SG函数求解过程展示)
// 一共有n颗石子,两个人轮流拿,每次可以拿1~m颗石子
// 拿到最后一颗石子的人获胜,根据n、m返回谁赢
// 对数器验证
public class Code01_BashGameSG {
// 发现结论去求解,时间复杂度O(1)
// 充分研究了性质
public static String bash1(int n, int m) {
return n % (m + 1) != 0 ? "先手" : "后手";
}
// sg函数去求解,时间复杂度O(n*m)
// 不用研究性质
// 其实把sg表打印之后,也可以发现性质,也就是打表找规律
public static String bash2(int n, int m) {
int[] sg = new int[n + 1];
boolean[] appear = new boolean[m + 1];
for (int i = 1; i <= n; i++) {
Arrays.fill(appear, false);
for (int j = 1; j <= m && i - j >= 0; j++) {
appear[sg[i - j]] = true;
}
for (int s = 0; s <= m; s++) {
if (!appear[s]) {
sg[i] = s;
break;
}
}
}
// System.out.println("打印 n = " + n + ", m = " + m + " 的sg表");
// for (int i = 0; i <= n; i++) {
// System.out.println("sg(" + i + ") : " + sg[i]);
// }
return sg[n] != 0 ? "先手" : "后手";
}
// 为了验证
public static void main(String[] args) {
int V = 1000;
int testTimes = 10000;
System.out.println("测试开始");
for (int i = 0; i < testTimes; i++) {
int n = (int) (Math.random() * V);
int m = (int) (Math.random() * V);
String ans1 = bash1(n, m);
String ans2 = bash2(n, m);
if (!ans1.equals(ans2)) {
System.out.println("出错了!");
}
}
System.out.println("测试结束");
int n = 100;
int m = 6;
bash2(n, m);
}
}
题目2:SG定理用法展示:尼姆博弈
一共有 n 堆石头,两人轮流进行游戏
在每个玩家的回合中,玩家需要 选择任一 非空 石头堆,从中移除任意 非零 数量的石头
如果不能移除任意的石头,就输掉游戏
返回先手是否一定获胜
对数器验证
通过观察sg表,以及分析总游戏的异或结果,一样可以得到尼姆博弈最简洁的结论
package class096;
import java.util.Arrays;
// 尼姆博弈(SG定理简单用法展示)
// 一共有 n 堆石头,两人轮流进行游戏
// 在每个玩家的回合中,玩家需要 选择任一 非空 石头堆,从中移除任意 非零 数量的石头
// 如果不能移除任意的石头,就输掉游戏
// 返回先手是否一定获胜
// 对数器验证
public class Code02_NimGameSG {
// 时间复杂度O(n)
// 充分研究了性质
public static String nim1(int[] arr) {
int eor = 0;
for (int num : arr) {
eor ^= num;
}
return eor != 0 ? "先手" : "后手";
}
// sg函数去求解
// 过程时间复杂度高,但是可以轻易发现规律,进而优化成最优解
// 证明不好想,但是从sg表出发,去观察最终的解,要好做很多
public static String nim2(int[] arr) {
int max = 0;
for (int num : arr) {
max = Math.max(max, num);
}
int[] sg = new int[max + 1];
boolean[] appear = new boolean[max + 1];
for (int i = 1; i <= max; i++) {
Arrays.fill(appear, false);
for (int j = 0; j < i; j++) {
appear[j] = true;
}
for (int s = 0; s <= max; s++) {
if (!appear[s]) {
sg[i] = s;
break;
}
}
}
// 打印sg表之后,可以发现,sg[x] = x
// 那么eor ^= sg[num] 等同于 eor ^= num
// 从sg定理发现了最优解
int eor = 0;
for (int num : arr) {
eor ^= sg[num];
}
return eor != 0 ? "先手" : "后手";
}
// 为了验证
public static int[] randomArray(int n, int v) {
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
ans[i] = (int) (Math.random() * v);
}
return ans;
}
public static void main(String[] args) {
int N = 200;
int V = 1000;
int testTimes = 10000;
System.out.println("测试开始");
for (int i = 0; i < testTimes; i++) {
int n = (int) (Math.random() * N) + 1;
int[] arr = randomArray(n, V);
String ans1 = nim1(arr);
String ans2 = nim2(arr);
if (!ans1.equals(ans2)) {
System.out.println("出错了!");
}
}
System.out.println("测试结束");
}
}
题目3:两堆石头的巴什博弈
有两堆石头,数量分别为a、b
两个人轮流拿,每次可以选择其中一堆石头,拿1~m颗
拿到最后一颗石子的人获胜,根据a、b、m返回谁赢
来自真实大厂笔试,没有在线测试,对数器验证
通过观察sg表,以及分析总游戏的异或结果,一样可以得到最简洁的结论
public static int MAXN = 101;
public static String[][][] dp = new String[MAXN][MAXN][MAXN];
// 动态规划方法彻底尝试
// 为了验证
public static String win1(int a, int b, int m) {
if (m >= Math.max(a, b)) {
return a != b ? "先手" : "后手";
}
if (a == b) {
return "后手";
}
if (dp[a][b][m] != null) {
return dp[a][b][m];
}
String ans = "后手";
for (int pick = 1; pick <= Math.min(a, m); pick++) {
if (win1(a - pick, b, m).equals("后手")) {
// 后续过程的赢家是后续过程的后手
// 那就表示此时的先手,通过这个后续过程,能赢
ans = "先手";
}
if (ans.equals("先手")) {
// 后续过程的赢家是后续过程的后手
// 那就表示此时的先手,通过这个后续过程,能赢
break;
}
}
for (int pick = 1; pick <= Math.min(b, m); pick++) {
if (win1(a, b - pick, m).equals("后手")) {
// 后续过程的赢家是后续过程的后手
// 那就表示此时的先手,通过这个后续过程,能赢
ans = "先手";
}
if (ans.equals("先手")) {
break;
}
}
dp[a][b][m] = ans;
return ans;
}
// sg定理
public static String win2(int a, int b, int m) {
int n = Math.max(a, b);
int[] sg = new int[n + 1];
boolean[] appear = new boolean[m + 1];
for (int i = 1; i <= n; i++) {
Arrays.fill(appear, false);
for (int j = 1; j <= m && i - j >= 0; j++) {
appear[sg[i - j]] = true;
}
for (int s = 0; s <= m; s++) {
if (!appear[s]) {
sg[i] = s;
break;
}
}
}
return (sg[a] ^ sg[b]) != 0 ? "先手" : "后手";
}
// 时间复杂度O(1)的最优解
// 其实是根据方法2中的sg表观察出来的
public static String win3(int a, int b, int m) {
return a % (m + 1) != b % (m + 1) ? "先手" : "后手";
}
题目4:三堆石头拿取斐波那契数博弈
有三堆石头,数量分别为a、b、c
两个人轮流拿,每次可以选择其中一堆石头,拿取斐波那契数的石头
拿到最后一颗石子的人获胜,根据a、b、c返回谁赢
来自真实大厂笔试,每堆石子的数量在10^5以内
没有在线测试,对数器验证
通过观察sg表,很难得到最简洁的结论,索性不优化了,反正数据量允许
package class096;
import java.util.Arrays;
// 三堆石头拿取斐波那契数博弈
// 有三堆石头,数量分别为a、b、c
// 两个人轮流拿,每次可以选择其中一堆石头,拿取斐波那契数的石头
// 拿到最后一颗石子的人获胜,根据a、b、c返回谁赢
// 来自真实大厂笔试,每堆石子的数量在10^5以内
// 没有在线测试,对数器验证
public class Code04_ThreeStonesPickFibonacci {
// 如果MAXN变大
// 相应的要修改f数组
public static int MAXN = 201;
// MAXN以内的斐波那契数
public static int[] f = { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 };
public static String[][][] dp = new String[MAXN][MAXN][MAXN];
// 动态规划方法彻底尝试
// 为了验证
public static String win1(int a, int b, int c) {
// 假设当前的先手来行动
// 注意不是全局的先手,是当前的先手来行动!
// 当前!当前!当前!
if (a + b + c == 0) {
// 当前的先手,面对这个局面
// 返回当前的后手赢
return "后手";
}
if (dp[a][b][c] != null) {
return dp[a][b][c];
}
String ans = "后手"; // ans : 赢的是当前的先手,还是当前的后手
for (int i = 0; i < f.length; i++) {
if (f[i] <= a) {
if (win1(a - f[i], b, c).equals("后手")) {
// 后续过程的赢家是后续过程的后手
// 那就表示当前的先手,通过这个后续过程,能赢
ans = "先手";
break;
}
}
if (f[i] <= b) {
if (win1(a, b - f[i], c).equals("后手")) {
// 后续过程的赢家是后续过程的后手
// 那就表示当前的先手,通过这个后续过程,能赢
ans = "先手";
break;
}
}
if (f[i] <= c) {
if (win1(a, b, c - f[i]).equals("后手")) {
// 后续过程的赢家是后续过程的后手
// 那就表示当前的先手,通过这个后续过程,能赢
ans = "先手";
break;
}
}
}
dp[a][b][c] = ans;
return ans;
}
// sg定理
public static int[] sg = new int[MAXN];
public static boolean[] appear = new boolean[MAXN];
// O(10^5 * 24 * 2)
public static void build() {
for (int i = 1; i < MAXN; i++) {
Arrays.fill(appear, false);
for (int j = 0; j < f.length && i - f[j] >= 0; j++) {
appear[sg[i - f[j]]] = true;
}
for (int s = 0; s < MAXN; s++) {
if (!appear[s]) {
sg[i] = s;
break;
}
}
}
}
public static String win2(int a, int b, int c) {
return (sg[a] ^ sg[b] ^ sg[c]) != 0 ? "先手" : "后手";
}
public static void main(String[] args) {
build();
System.out.println("测试开始");
for (int a = 0; a < MAXN; a++) {
for (int b = 0; b < MAXN; b++) {
for (int c = 0; c < MAXN; c++) {
String ans1 = win1(a, b, c);
String ans2 = win2(a, b, c);
if (!ans1.equals(ans2)) {
System.out.println("出错了!");
}
}
}
}
System.out.println("测试结束");
// 试图找到简洁规律,想通过O(1)的过程就得到sg(x)
// 于是打印200以内的sg值,开始观察
// 刚开始有规律,但是在sg(138)之后开始发生异常波动
// 这道题在考的时候,数据量并没有大到需要O(1)的过程才能通过
// 那就用build方法计算sg值,不再找寻简洁规律
// 考试时一切根据题目数据量来决定是否继续优化
for (int i = 0; i < MAXN; i++) {
System.out.println("sg(" + i + ") : " + sg[i]);
}
}
}
题目5:E&D游戏
桌子上有2n堆石子,编号为1、2、3…2n
其中1、2为一组;3、4为一组;5、6为一组…2n-1、2n为一组
每组可以进行分割操作:
任取一堆石子,将其移走,然后分割同一组的另一堆石子
从中取出若干个石子放在被移走的位置,组成新的一堆
操作完成后,组内每堆的石子数必须保证大于0
显然,被分割的一堆的石子数至少要为2
两个人轮流进行分割操作,如果轮到某人进行操作时,所有堆的石子数均为1,判此人输掉比赛
返回先手能不能获胜
测试链接 : https://www.luogu.com.cn/problem/P2148
通过观察sg表,确实有最简洁的结论,但是也太难观察了吧!多练!以后遇到类似的就会了!
每一组的sg值:先打一小部分,观察规律,因为石子太多
(a-1)|(b-1)最低的0位置
package class096;
// 计算两堆石子的SG值
// 桌上有两堆石子,石头数量分别为a、b
// 任取一堆石子,将其移走,然后分割同一组的另一堆石子
// 从中取出若干个石子放在被移走的位置,组成新的一堆
// 操作完成后,组内每堆的石子数必须保证大于0
// 显然,被分割的一堆的石子数至少要为2
// 两个人轮流进行分割操作,如果轮到某人进行操作时,两堆石子数均为1,判此人输掉比赛
// 计算sg[a][b]的值并找到简洁规律
// 本文件仅为题目5打表找规律的代码
public class Code05_EDGame1 {
public static int MAXN = 1001;
public static int[][] dp = new int[MAXN][MAXN];
public static void build() {
for (int i = 0; i < MAXN; i++) {
for (int j = 0; j < MAXN; j++) {
dp[i][j] = -1;
}
}
}
public static int sg(int a, int b) {
if (a == 1 && b == 1) {
return 0;
}
if (dp[a][b] != -1) {
return dp[a][b];
}
boolean[] appear = new boolean[Math.max(a, b) + 1];
if (a > 1) {
for (int l = 1, r = a - 1; l < a; l++, r--) {
appear[sg(l, r)] = true;
}
}
if (b > 1) {
for (int l = 1, r = b - 1; l < b; l++, r--) {
appear[sg(l, r)] = true;
}
}
int ans = 0;
for (int s = 0; s <= Math.max(a, b); s++) {
if (!appear[s]) {
ans = s;
break;
}
}
dp[a][b] = ans;
return ans;
}
public static void f1() {
System.out.println("石子数9以内所有组合的sg值");
System.out.println();
System.out.print(" ");
for (int i = 1; i <= 9; i++) {
System.out.print(i + " ");
}
System.out.println();
System.out.println();
for (int a = 1; a <= 9; a++) {
System.out.print(a + " ");
for (int b = 1; b < a; b++) {
System.out.print("X ");
}
for (int b = a; b <= 9; b++) {
int sg = sg(a, b);
System.out.print(sg + " ");
}
System.out.println();
}
}
public static void f2() {
System.out.println("石子数9以内所有组合的sg值,但是行列都-1");
System.out.println();
System.out.print(" ");
for (int i = 0; i <= 8; i++) {
System.out.print(i + " ");
}
System.out.println();
System.out.println();
for (int a = 1; a <= 9; a++) {
System.out.print((a - 1) + " ");
for (int b = 1; b < a; b++) {
System.out.print("X ");
}
for (int b = a; b <= 9; b++) {
int sg = sg(a, b);
System.out.print(sg + " ");
}
System.out.println();
}
}
public static void f3() {
System.out.println("测试开始");
for (int a = 1; a < MAXN; a++) {
for (int b = 1; b < MAXN; b++) {
int sg1 = sg(a, b);
int sg2 = lowZero((a - 1) | (b - 1));
if (sg1 != sg2) {
System.out.println("出错了!");
}
}
}
System.out.println("测试结束");
}
// 返回status最低位的0在第几位
public static int lowZero(int status) {
int cnt = 0;
while (status > 0) {
if ((status & 1) == 0) {
break;
}
status >>= 1;
cnt++;
}
return cnt;
}
public static void main(String[] args) {
build();
f1();
System.out.println();
System.out.println();
f2();
System.out.println();
System.out.println();
f3();
}
}
package class096;
// E&D游戏
// 桌子上有2n堆石子,编号为1、2、3...2n
// 其中1、2为一组;3、4为一组;5、6为一组...2n-1、2n为一组
// 每组可以进行分割操作:
// 任取一堆石子,将其移走,然后分割同一组的另一堆石子
// 从中取出若干个石子放在被移走的位置,组成新的一堆
// 操作完成后,组内每堆的石子数必须保证大于0
// 显然,被分割的一堆的石子数至少要为2
// 两个人轮流进行分割操作,如果轮到某人进行操作时,所有堆的石子数均为1,判此人输掉比赛
// 返回先手能不能获胜
// 测试链接 : https://www.luogu.com.cn/problem/P2148
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Code05_EDGame2 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
int t = (int) in.nval;
for (int i = 0; i < t; i++) {
in.nextToken();
int n = (int) in.nval;
int sg = 0;
for (int j = 1, a, b; j <= n; j += 2) {
in.nextToken();
a = (int) in.nval;
in.nextToken();
b = (int) in.nval;
sg ^= lowZero((a - 1) | (b - 1));
}
if (sg != 0) {
out.println("YES");
} else {
out.println("NO");
}
}
out.flush();
out.close();
br.close();
}
public static int lowZero(int status) {
int ans = 0;
while (status > 0) {
if ((status & 1) == 0) {
break;
}
status >>= 1;
ans++;
}
return ans;
}
}
1279

被折叠的 条评论
为什么被折叠?



