公平组合游戏

不是写给自己看的。

ICG

考虑一类两个玩家的零和博弈游戏:

  1. 游戏有一个局面,双方轮流操作,每次操作转移到一个新局面。
  2. 能进行的操作与当前局面相关,与轮到哪一方操作无关(公平性)
  3. 无法操作的一方失败。
  4. 局面之间的转移无环。

这可以看做以局面为节点,转移边的有向无环图上沿边交替移动一个棋子。也就是 ICG 等价于其对应的 有向图游戏。

结论:任一 ICG 要么先手 存在必胜策略(简称先手必胜),要么后手 存在必胜策略(简称先手必败),两者恰有其一。

证明:

考虑归纳定义一个局面是先手必败的(P-position)当且仅当两者至少满足一个:

  1. 这个局面是终止局面(无法进一步操作 / 出度为 0
  2. 这个局面不能到达任何必败局面。

定义一个局面是先手必胜的(N-position)当且仅当:

  1. 这个局面可以到达至少一个必败局面。

相当于证明任何局面要么是 P-position,要么是 N-position。因为这个图是 DAG,可以从每个出度为 0 的点出发归纳证明。

一般有向图游戏

对于一般的有向图游戏,我们可以直接拓扑排序求出一个点是必胜还是必败局面,还不需要引入 SG 函数。题外话:这种思想也可以拿来求有环的类公平组合游戏,并判断平局。

Nim 游戏

局面:n 堆石子,数目分别为 a1,a2,,an

操作:任选一堆石子 i,令 aix 个,必须满足 0x<ai

结论:先手必胜当且仅当初始 a1,a2,,an 的异或和不为 0。否则先手必败。

证明:

根据定义,证明一种判断 position 的性质的方法的正确性,只需证明三个命题:

  1. 这个判断将所有终止状态判为 P-position;
  2. 根据这个判断被判为 N-position 的局面一定可以移动到某个 P-position;
  3. 根据这个判断被判为 P-position 的局面无法移动到某个 P-position。

第一个命题显然,终止状态只有一个,就是全 0,异或仍然是 0

第二个命题,即对于某个局面 a1,a2,,an,若异或和不为 0,一定存在某个合法的移动,将 ai 改变成 ai 后满足异或和为 0。不妨设 a1,a2,,an 异或和为 k,则一定存在某个 ai,它的二进制表示在 k 的最高位上是 1。这时 aik<ai 一定成立,因为前者不满足。则我们可以将ai改变成 ai=aik,此时异或和为 0

第三个命题,即即对于某个局面 a1,a2,,an,若异或和为 0,一定不存在某个合法的移动,将 ai 改变成 ai 后满足异或和为 0,否则 ai=ai

用这种特殊的有向图游戏的思想,可以解决下面的问题。

SG 函数

考虑 有向图游戏的和:有多个 完全独立 的有向图游戏,它们共同构成一个新的游戏。这个游戏中:

  1. 局面即为所有有向图游戏的局面共同构成的状态。
  2. 操作为选择恰好一个可操作的有向图游戏做恰好一次操作。

因为局面变为了指数级,所以直接跑不再可行,必须借助相互独立的性质计算。


设有向图游戏 G 的一个局面 p,定义

SG(p)=0 当且仅当 p 是一个必败局面。

SG(p)=x>0 当且仅当 0y<xp 可以转移到某个 SG(q)=y 的局面 q;并且 不能转移到 SG(q)=x 的任何局面 q

发现这个东西其实和 Nim 游戏没有本质区别,只是把 ai 换成了 SG(p)。但是有一个问题:没有保证 x>0 时不能转移到 SG(q)>x 的局面 q。但可以证明这是没有必要的,因为根据定义,若这一步转移到 [0,x) 均不可获胜,下一步对方仍然可以转移回 SG(p)=x。注意 pp,并非形成了环只是 SG 值不变。

于是得到结论:

  1. s 为有向图游戏 G 的起点,定义 SG(G)=SG(s)
  2. 游戏 G 先手必胜,当且仅当所有 SG(G) 的异或和不为 0;否则先手必败。

Depot

AGC016F Games on DAG

给定一个有向无环图 G=(V,E),定义 原游戏 为 从 1,2 两个节点出发形成的两个有向图游戏的和。求满足 EE 的所有 2|E| 个生成子图 G=(V,E) 中,使得原游戏先手必胜的数目。n15

等价于 sg1sg2。这个难以求解,考虑改为 2|E| 减去使得 sg1=sg2 的子图数目。


首先考虑求解 sg1=0 的方案数。根据数据范围,设 fP 表示 SG 值为 0 的集合为 PV 情况下的方案数。另外

1P,fP=0

这个东西不用 DP,可以 O(2nm) 直接算,只需考虑四类边在子图的存在性:

首先考虑 P 内部,由定义这些边均不能存在。然后考虑从 P 连向 N 的边,由定义,这些边可以随便连。接下来是从 N 连向 P 的边,由定义每个 pN 至少应有一条这样的边存在。然后对于 N 内部 ,这些点之间可以随便连,因为只需要 SG0 就符合状态。直接乘法原理求出四类边的添加方案数。最后总方案数即为 f

注意这时候我们考虑的 N 集合只限定了 pN,SG(p)1 的条件,并没有考虑具体 SG 值。


考虑如何保证 sg1=sg2k,这时候有人类智慧:

SG(p)=0,1,2, 依次加入。具体来说:考虑除去所有 P 中节点,得到导出子图 G[N],容易证明其中每个节点 相比原来此节点 SG 值恰好减少 1。所以我们可以做 O(n) 轮 DP 解决。

gS 表示只考虑点集 SV,要求 sg1=sg2 的方案数。其中

[1S][2S],gS=0

注意到这个条件使得 sg1=sg2 相等(因为两点在同一轮被加入)。

对于 gS,每次枚举子集 N,从 gN 转移。然后按照前述算法统计G[P],cross(P,N),cross(N,P) 的边,对于 G[N] 中的边,它就相当于只考虑这个导出子图G[N]的满足条件的方案数,即子问题 gN

最后答案即为 gV。复杂度 O(3nn)

#include <bits/stdc++.h>
#define popc __builtin_popcount
#define ctz __builtin_ctz

typedef long long ll;
const int maxn = 18, maxj = 1 << maxn, mod = 1000000007;

int n, m, all;
int e[maxn], f[maxj];

int main() {
	int i, j, u, v, k, real, N, P, sum = 1;
	scanf("%d%d", &n, &m), all = (1 << n) - 1;
	for (i = 0; i < m; ++i)
		scanf("%d%d", &u, &v), e[--u] |= 1 << --v, sum = sum * 2 % mod;
	for (f[0] = 1, i = 2; i <= all; i += 2) // 保证 1 不在其中
		// 枚举子集
		for (j = i; ; j = (j - 1) & i) {
			ll res = 1;
			real = i + (i >> 1 & 1); // 若 2 在其中,加入 1
			N = j + (j >> 1 & 1), P = real & ~N;
			// 枚举其中节点,转移。
			for (k = real; k; k &= k - 1)
				v = __builtin_ctz(k), res = res * (N >> v & 1 ? (1 << __builtin_popcount(e[v] & P)) - 1 : 1 << __builtin_popcount(e[v] & N)) % mod;
			f[i] = (f[i] + res * f[j]) % mod;
			if (!j) break;
		}
	sum -= f[all - 1], printf("%d\n", sum + (sum >> 31 & mod));
	return 0;
}
posted @   音街ウナ  阅读(29)  评论(1编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示