(左程云)算法讲解096【必备】博弈类问题必备内容详解-下

图游戏的概念
任何局面都认为是图中的点,每一个局面都可以通过一种行动,走向图中的下一个点
如果当前行动有若干个,那么后继节点就有若干个。最终,必败局面的点认为不再有后继节点
那么公平组合游戏(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;
	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值