公平组合游戏
不是写给自己看的。
ICG
考虑一类两个玩家的零和博弈游戏:
- 游戏有一个局面,双方轮流操作,每次操作转移到一个新局面。
- 能进行的操作与当前局面相关,与轮到哪一方操作无关(公平性)
- 无法操作的一方失败。
- 局面之间的转移无环。
这可以看做以局面为节点,转移边的有向无环图上沿边交替移动一个棋子。也就是 ICG 等价于其对应的 有向图游戏。
结论:任一 ICG 要么先手 存在必胜策略(简称先手必胜),要么后手 存在必胜策略(简称先手必败),两者恰有其一。
证明:
考虑归纳定义一个局面是先手必败的(P-position)当且仅当两者至少满足一个:
- 这个局面是终止局面(无法进一步操作 / 出度为
) - 这个局面不能到达任何必败局面。
定义一个局面是先手必胜的(N-position)当且仅当:
- 这个局面可以到达至少一个必败局面。
相当于证明任何局面要么是 P-position,要么是 N-position。因为这个图是 DAG,可以从每个出度为
一般有向图游戏
对于一般的有向图游戏,我们可以直接拓扑排序求出一个点是必胜还是必败局面,还不需要引入 SG 函数。题外话:这种思想也可以拿来求有环的类公平组合游戏,并判断平局。
Nim 游戏
局面:
操作:任选一堆石子
结论:先手必胜当且仅当初始
的异或和不为 。否则先手必败。
证明:
根据定义,证明一种判断 position 的性质的方法的正确性,只需证明三个命题:
- 这个判断将所有终止状态判为 P-position;
- 根据这个判断被判为 N-position 的局面一定可以移动到某个 P-position;
- 根据这个判断被判为 P-position 的局面无法移动到某个 P-position。
第一个命题显然,终止状态只有一个,就是全
第二个命题,即对于某个局面
第三个命题,即即对于某个局面
用这种特殊的有向图游戏的思想,可以解决下面的问题。
SG 函数
考虑 有向图游戏的和:有多个 完全独立 的有向图游戏,它们共同构成一个新的游戏。这个游戏中:
- 局面即为所有有向图游戏的局面共同构成的状态。
- 操作为选择恰好一个可操作的有向图游戏做恰好一次操作。
因为局面变为了指数级,所以直接跑不再可行,必须借助相互独立的性质计算。
设有向图游戏
发现这个东西其实和 Nim 游戏没有本质区别,只是把
于是得到结论:
- 若
为有向图游戏 的起点,定义 - 游戏
先手必胜,当且仅当所有 的异或和不为 ;否则先手必败。
Depot
AGC016F Games on DAG
给定一个有向无环图
,定义 原游戏 为 从 两个节点出发形成的两个有向图游戏的和。求满足 的所有 个生成子图 中,使得原游戏先手必胜的数目。
等价于
首先考虑求解
这个东西不用 DP,可以
首先考虑
注意这时候我们考虑的
考虑如何保证
将
依次加入。具体来说:考虑除去所有 中节点,得到导出子图 ,容易证明其中每个节点 相比原来此节点 值恰好减少 。所以我们可以做 轮 DP 解决。
设
注意到这个条件使得
对于
最后答案即为
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话