博弈论,NIM游戏,台阶型,集合型,SG函数,详解

文章目录

    • 一、Nim游戏
      • 1.1问题描述
      • 1.2定理
        • 1.2.1定理内容
        • 1.2.2定理证明
      • 1.3OJ练习
        • 1.3.1模板OJ
        • 1.3.2P1247 取火柴游戏
    • 二、台阶型Nim游戏
      • 2.1问题描述
      • 2.2结论及证明
        • 2.2.1结论
        • 2.2.2结论证明
      • 2.3OJ练习
        • 2.3.1Georgia and Bob
    • 三、有向图游戏,SG函数
      • 3.1定义
        • 3.1.1有向图游戏
        • 3.1.2Mex运算
        • 3.1.3SG函数
        • 3.1.4有向图游戏的和
      • 3.2定理及证明
        • 3.2.1定理内容
        • 3.2.1定理证明
      • 3.3有向无环图上的棋子游戏
        • 3.3.1问题描述
        • 3.3.2思路分析
        • 3.3.3原题链接
        • 3.3.4AC代码
      • 3.4 集合型NIM游戏
        • 3.4.1问题描述
        • 3.4.2思路分析
        • 3.4.3原题链接
        • 3.4.4AC代码
      • 3.5普通NIM游戏与有向图游戏的关系
      • 3.6OJ练习-POJCutting Game
        • 3.6.1原题链接
        • 3.6.2思路分析
        • 3.6.3AC代码


一、Nim游戏

1.1问题描述

甲,乙两个人玩 nim 取石子游戏。

nim 游戏的规则是这样的:地上有 n 堆石子(每堆石子数量小于 10^4),每人每次可从任意一堆石子里取出任意多枚石子扔掉,可以取完,不能不取。每次只能从一堆里取。最后没石子可取的人就输了。假如甲是先手,且告诉你这 n 堆石子的数量,他想知道是否存在先手必胜的策略。

上面这种游戏被称为NIM博弈。对于游戏过程中面临的状态,如果玩家在这种状态下无论进行任何行动,都会输掉游戏,我们称该状态为必败态。同样的,如果玩家在这种状态下无论进行任何行动,都会赢得游戏,我们称该状态为必胜态

1.2定理

1.2.1定理内容

NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ …… ^ An ≠ 0

1.2.2定理证明

下面采用的证明方法是NIM博弈问题常用证明方法:

  • 证明:必胜态的后继状态至少存在一个必败态
    • 若A1 ^ A2 ^ …… ^ An = s,设s最高位是第k位,则A1~An中有奇数个第k位为1,不妨从中取出Ai,那么Ai ^ s <= Ai
    • 我们可以减少Ai为Ai ^ s,那么此时有A1 ^ A2 ^ … ^ Ai ^ s ^ … ^ An = s ^ s = 0
    • 于是就得到了一个必败态
  • 证明:必败态的后继状态均为必胜态
    • 由于A1 ^ A2 ^ …… ^ An = 0,于是所有位置上1的个数为偶数
    • 无论我们取走哪一堆,都会使某一位上1的个数为奇数,从而得到必胜态

因此必胜态和必败态必然交替出现先手者若以必胜态开局,总能使得自己处于不败之地,直到对手失败

1.3OJ练习

1.3.1模板OJ

原题链接

P2197 【模板】Nim 游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

AC代码

#include <iostream>
#include <cstring>
using namespace std;
#define int long long
const int N = 1e7 + 10, mod = 1e9 + 7;
int n, res;
void solve()
{cin >> n, res = 0;for (int i = 0, a; i < n; i++)cin >> a, res ^= a;res ? cout << "Yes\n" : cout << "No\n";
}
signed main()
{ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);//freopen("in.txt", "r", stdin);int _ = 1;cin >> _;while (_--)solve();return 0;
}
1.3.2P1247 取火柴游戏

原题链接

P1247 取火柴游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

AC代码

#include <iostream>
#include <cstring>
using namespace std;
#define int long long
const int N = 5e5 + 10, mod = 1e9 + 7;
int n, res, a[N];
void solve()
{cin >> n, res = 0;for (int i = 0; i < n; i++)cin >> a[i], res ^= a[i];if (!res){cout << "lose";return;}for (int i = 0; i < n; i++){if ((a[i] ^ res) >= a[i])continue;cout << a[i] - (a[i] ^ res) << ' ' << i + 1 << '\n', a[i] = a[i] ^ res;break;}for (int i = 0; i < n; i++)cout << a[i] << ' ';
}
signed main()
{ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);freopen("in.txt", "r", stdin);int _ = 1;// cin >> _;while (_--)solve();return 0;
}

二、台阶型Nim游戏

2.1问题描述

有1~n级台阶,第 i 级台阶上摆放ai个石子,每次操作可将第k级台阶上的石子移一些到第k - 1级台阶上,移到第0级台阶(地面)的石子不能再移动。
如果一个人没有石子可以移动,他就输了,问先手是否必胜。

2.2结论及证明

2.2.1结论

必胜态为:奇数级台阶的石子数异或和不为0。必败态为:和必胜态相反

2.2.2结论证明
  • 证明:必胜态的后继状态至少存在一个必败态
    • 若A1 ^ A3 ^…… ≠ 0 ,那么必然存在ai(i为奇数),ai ^ s <= ai,我们操作第i级台阶使其变为ai ^ s
    • 那么后继状态奇数级台阶石子数异或和为0,为必败态
  • 证明:必败态的后继状态均为必胜态
    • 由于若A1 ^ A3 ^…… = 0,无论玩家操作奇数级台阶还是偶数级台阶都会使得某一奇数级台阶石子数改变
    • 从而使得A1 ^ A3 ^…… ≠ 0

2.3OJ练习

2.3.1Georgia and Bob

原题链接

1704 – Georgia and Bob (poj.org)

思路分析

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们发现主席左移等效于空白右移

那么这就转化为了台阶型NIM问题

如上图中有四段连续空白块,相当于四个台阶,需要说明的是,我们只统计最后一个主席左边的空白块,最后一个主席右边相当于地面

那么我们直接按照台阶型NIM来做即可

AC代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
#define int long long
const int N = 5e5 + 10, mod = 1e9 + 7;
int n, res, a[N], b[N];
void solve()
{cin >> n, res = 0;for (int i = 1; i <= n; i++)cin >> a[i];sort(a + 1, a + 1 + n);for (int i = n, j = 1; i >= 1; i--)b[j++] = a[i] - a[i - 1] - 1;for (int i = 1; i <= n; i += 2)res ^= b[i];if (res)cout << "Georgia will win\n";elsecout << "Bob will win\n";
}
signed main()
{ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);//freopen("in.txt", "r", stdin);int _ = 1;cin >> _;while (_--)solve();return 0;
}

码蹄集 (matiji.net)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
typedef pair<int, int> PII;
const int mod = 998244353, inv2 = 499122177;
const int N = 5e5 + 5;
#define lc p << 1
#define rc p << 1 | 1
int n, tot, id[N];
struct data
{int i, x, y;
} datas[N];
struct node
{int l, r;int cnt, s, pw;
} tr[N << 2];
int qp(int a, int b)
{int res = 1;while (b){if (b & 1)res = res * a % mod;b >>= 1, a = a * a % mod;}return res;
}void pushup(int p)
{tr[p].s = (tr[lc].s * tr[rc].pw % mod + tr[rc].s) % mod, tr[p].pw = tr[lc].pw * tr[rc].pw % mod, tr[p].cnt = tr[lc].cnt + tr[rc].cnt;
}
void build(int p, int l, int r)
{tr[p].l = l, tr[p].r = r, tr[p].pw = 1;if (l == r)return;int mid = (l + r) >> 1;build(lc, l, mid), build(rc, mid + 1, r);
}void update(int p, int c, int x)
{if (tr[p].l == tr[p].r){if (!tr[p].cnt)tr[p].s = inv2;tr[p].cnt += x;tr[p].pw = tr[p].pw * qp(inv2, x) % mod;return;}int mid = (tr[p].l + tr[p].r) >> 1;if (c <= mid)update(lc, c, x);elseupdate(rc, c, x);pushup(p);
}
PII query(int p, int l, int r)
{PII res, lp, rp;if (l <= tr[p].l && tr[p].r <= r)return make_pair(tr[p].s, tr[p].pw);int mid = (tr[p].l + tr[p].r) >> 1;if (r <= mid)return query(lc, l, r);if (l > mid)return query(rc, l, r);lp = query(lc, l, r), rp = query(rc, l, r);res.first = (lp.first * rp.second % mod + rp.first) % mod, res.second = lp.second * rp.second % mod;return res;
}
signed main()
{ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);//freopen("in.txt", "r", stdin);cin >> n;for (int i = 0; i < n; i++){cin >> datas[i].i >> datas[i].x >> datas[i].y;if (datas[i].i == 1)id[++tot] = datas[i].x;}sort(id + 1, id + tot + 1);tot = unique(id + 1, id + tot + 1) - id - 1;build(1, 1, tot);for (int i = 0, a, b; i < n; i++){if (datas[i].i == 1)update(1, lower_bound(id + 1, id + tot + 1, datas[i].x) - id, datas[i].y);else{a = lower_bound(id + 1, id + tot + 1, datas[i].x) - id, b = upper_bound(id + 1, id + tot + 1, datas[i].y) - id - 1;cout << (a <= b ? query(1, a, b).first : 0) << '\n';}}return 0;
}

三、有向图游戏,SG函数

3.1定义

3.1.1有向图游戏

给定一个有向无环图,图中有唯一一个起点,起点处放有一个棋子,两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏

3.1.2Mex运算

设S表示一个非负整数集合。定义Mex(S)为求出不属于集合S的最小非负整数的运算,即:
$$
mex(S) = min{x},x \in N, x \notin S

$$

3.1.3SG函数

在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1,y2……yk,定义SG(x)为x的后继节点y1,y2……yk的SG函数值构成的集合再执行mex运算的结果,即:
S G ( x ) = m e x ( { S G ( y 1 ) , S G ( y 2 ) , … … , S G ( y k ) } ) SG(x)=mex(\{SG(y1),SG(y2),……,SG(yk)\}) SG(x)=mex({SG(y1),SG(y2),……,SG(yk)})
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.1.4有向图游戏的和

设G1,G2,……,Gm是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi上行动一步G被称为有向图游戏G1,G2……Gm的和。

有向图游戏的和的SG函数值等于它包含的各个子游戏的SG函数值的异或和,即:
S G ( x ) = S G ( y 1 ) ⊕ S G ( y 2 ) , … … , ⊕ S G ( y k ) SG(x)=SG(y1)\oplus SG(y2),……,\oplus SG(yk) SG(x)=SG(y1)SG(y2),……,SG(yk)

3.2定理及证明

3.2.1定理内容
  • 有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
  • 有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
3.2.1定理证明

证明思路仍然为证明必胜态必败态交替出现

  • 证明:必胜态的后继状态至少存在一个必败态
    • 设SG(y1) ^ SG(y2),……,^SG(yk) = s,设s的最高位为第k位
    • 可以找到第k位为1的SGi,由于Gi = mex({SG(yi)})(yi为i的后继节点)
    • 则SGi必然可以转移到SGi ^ s
  • 证明:必败态的后继状态均为必胜态
    • 无论选择哪个有向图游戏进行移动,都会使得新的有向图游戏和的SG函数值异或和不为0
  • 证毕

其实对于SG函数可以这样理解:

在一个没有出边的节点上,棋子不能移动,它的SG值为0,对应必败局面。

若一个节点的某个后继节点SG值为0,在mex运算后,该节点的SG值大于0。
这等价于,若一个局面的后继局面中存在必败局面,则当前局面为必胜局面。

若一个节点的后继节点SG值均不为0,在mex运算后,该节点的SG值为0。这
等价于,若一个局面的后继局面全部为必胜局面,则当前局面为必败局面。

3.3有向无环图上的棋子游戏

3.3.1问题描述

给定一个有n个节点和m条边的有向无环图,k个棋子所在的节点编号。

两名玩家交替移动棋子,每次只能将任意一颗棋子沿有向边移到另一个点,无法移动者视为失败。

如果两人都采用最优策略,问先手是否必胜。

3.3.2思路分析

k个棋子都是独立的,我们可以把k个棋子看作k个有向图游戏,先建图然后用SG定理进行判断即可。

3.3.3原题链接

信息学奥赛一本通(C++版)在线评测系统 (ssoier.cn)

3.3.4AC代码
#include <iostream>
#include <algorithm>
#include <unordered_set>
#include <cstring>
using namespace std;
#define int long long
const int N = 2e3 + 10, M = 12010, mod = 1e9 + 7;
struct edge
{int v, nxt;
} edges[M];
int head[N], f[N], idx = 0;
void addedge(int u, int v)
{edges[idx] = {v, head[u]}, head[u] = idx++;
}
int n, m, k, res = 0;
int sg(int x)
{if (~f[x])return f[x];unordered_set<int> s;for (int i = head[x]; ~i; i = edges[i].nxt)s.insert(sg(edges[i].v));for (int i = 0;; i++)if (!s.count(i))return f[x] = i;return -1;
}
void solve()
{memset(head, -1, sizeof head), memset(f, -1, sizeof f);cin >> n >> m >> k;for (int i = 0, a, b; i < m; i++)cin >> a >> b, addedge(a, b);for (int i = 0, x; i < k; i++)cin >> x, res ^= sg(x);res ? cout << "win" : cout << "lose";
}
signed main()
{ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);//freopen("in.txt", "r", stdin);int _ = 1;// cin >> _;while (_--)solve();return 0;
}

3.4 集合型NIM游戏

3.4.1问题描述

给定m个整数组成的集合ai,给定n堆石子的数量bi。

两名玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数目必须是集合a中的整数,最后无法进行操作的人视为失败。

如果两人都采用最优策略,问先手是否必胜。

3.4.2思路分析

每堆石子都是孤立的,把n堆石子看做n个有向图游戏。然后利用SG定理即可。

对于子节点的寻找直接根据集合a内元素进行判断即可,省去了建图。

3.4.3原题链接

Problem - 1536 (hdu.edu.cn)

3.4.4AC代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
#define int long long
const int N = 10005, M = 105, mod = 1e9 + 7;
int a[M], f[N], k, m, n;
int sg(int x)
{if (~f[x])return f[x];bool vis[M]{0};for (int i = 0; i < k && x >= a[i]; i++)vis[sg(x - a[i])] = 1;for (int i = 0;; i++)if (!vis[i])return f[x] = i;return -1;
}
void solve()
{while (cin >> k, k){memset(f, -1, sizeof f);for (int i = 0; i < k; i++)cin >> a[i];sort(a, a + k), cin >> m;while (m--){cin >> n;int res = 0;for (int i = 0, x; i < n; i++)cin >> x, res ^= sg(x);res ? cout << 'W' : cout << 'L';}cout << '\n';}
}
signed main()
{ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);//freopen("in.txt", "r", stdin);int _ = 1;while (_--)solve();return 0;
}

3.5普通NIM游戏与有向图游戏的关系

将一个有x个石子的堆视为节点x,当y < x时,节点x可以到y。
由n个堆组成的Nim游戏,可以视为n个有向图游戏。
而显然对于有x个石子的堆显然可以抵达0~x-1的所有堆,那么其sg值就是x

于是我们就可以省去sg的计算,直接由每堆石子数目的异或和来得到答案

3.6OJ练习-POJCutting Game

3.6.1原题链接

2311 – Cutting Game (poj.org)

3.6.2思路分析

我们自底向上思考

如果当前状态为1 * x或者x * 1,那么该玩家必胜

向上推一层,对于2*3、3*2和2*2三个状态,无论怎么剪都会剪出来一个必胜态,于是2 * 3、3 * 2和2 * 2就是必败态

不失一般性地考虑,对于m*n,只考虑2……m-2 * n和m * 2……n-2的后继状态,那么剪一次会有两个子节点,它们两个不互相独立

也就是说,我们要把两个子节点看成一个组合状态,其sg值为s(y1) ^ s(y2)(y1y2对应某种裁剪策略产生的两个子节点)

然后我们就可以跑sg了,根据根节点的sg值输出即可

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.6.3AC代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <set>
using namespace std;
const int N = 210, mod = 1e9 + 7;
int m, n, f[N][N];
int sg(int x, int y)
{if (~f[x][y])return f[x][y];set<int> s;for (int i = 2; i <= x - 2; i++)s.insert(sg(i, y) ^ sg(x - i, y));for (int i = 2; i <= y - 2; i++)s.insert(sg(x, i) ^ sg(x, y - i));for (int i = 0;; i++)if (!s.count(i))return f[x][y] = f[y][x] = i;return -1;
}
void solve()
{memset(f, -1, sizeof f);while (cin >> m >> n){sg(m, n) ? cout << "WIN\n" : cout << "LOSE\n";}
}
signed main()
{ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);//freopen("in.txt", "r", stdin);int _ = 1;while (_--)solve();return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/503928.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【王道操作系统】ch1计算机系统概述-04操作系统结构

文章目录 【王道操作系统】ch1计算机系统概述-04操作系统结构操作系统的内核操作系统的体系结构考纲新增内容&#xff08;红色为全新内容&#xff0c;黄色为原有内容&#xff09;&#xff1a;01 分层结构02 模块化03 宏内核&#xff08;大内核&#xff09;和微内核04 外核 【王…

mybatisPlus中 Mapper层以及Service层的方法 整理分类 以及代码实操

MyBatis-Plus是一个基于MyBatis的增强工具&#xff0c;旨在简化开发、提高效率。它提供了通用的mapper和service&#xff0c;可以在不编写任何SQL语句的情况下&#xff0c;快速实现对单表的CRUD、批量、逻辑删除、分页等操作。 功能代码测试前提&#xff1a; 需要对mybatisPl…

HarmonyOS Full SDK的安装

OpenHarmony的应用开发工具HUAWEI DevEco Studio现在随着OpenHarmony版本发布而发布,只能在版本发布说明中下载,例如最新版本的OpenHarmony 4.0 Release。对应的需要下载DevEco Studio 4.0 Release,如下图。 图片 下载Full SDK主要有两种方式,一种是通过DevEco Studio下载…

黑马程序员微信小程序学习总结11.分包与uniapp

这里写目录标题 分包什么是分包分包加载规则 自定义tab&#xff08;略&#xff0c;后面项目使用到再学&#xff09;uniapp创建项目目录结构使用git管理项目新建页面配置tabbar修改导航栏 分包 什么是分包 分包加载规则 其他略&#xff0c;赶进度快进到uniapp&#xff0c;这些可…

黑马c++ STL部分 笔记(5) stack容器

stack是一种先进后出的数据结构&#xff0c;它只有一个出口。 栈中只有栈顶的元素才可以被外界使用&#xff0c;因此栈不允许有遍历行为。 栈可以判断容器是否为空。 栈可以返回元素个数。 栈中进入数据——入栈push。 栈中弹出数据——出栈pop。 stack常用接口 // stack常用…

2023年06月CCF-GESP编程能力等级认证Scratch图形化编程四级真题解析

一、单选题(共15题,共30分) 第1题 高级语言编写的程序需要经过以下( )操作,可以生成在计算机上运行的可执行代码。 A:编辑 B:保存 C:调试 D:编译 答案:D 第2题 排序算法是稳定的(Stable Sorting),就是指排序算法可以保证,在待排序数据中有两个相等记录的关…

堆与TopK问题分析

TopK问题 题目及思路分析 所谓TopK问题&#xff0c;在一组数据中找出前K个最大或者最小的数值&#xff0c;而使用TopK问题的解决思路的问题一般数据个数都比较大&#xff0c;如果直接用数组&#xff0c;则会导致数据无法一次性加载到内存从而难以比较&#xff0c;难者甚至因为…

C++之函数,指针

函数 1&#xff0c;函数概述 作用&#xff1a;将一段经常使用的代码封装起来&#xff0c;减少重复代码 一个较大的程序&#xff0c;一般分为若干份程序块&#xff0c;每个模块实现特定的功能 2&#xff0c;函数的定义 函数的定义一般有五个步骤&#xff1a; 1&#xff0c…

MATLAB中function_handle函数用法

目录 说明 创建对象 示例 命名函数求积分 匿名函数求积分 function_handle函数所表示的是函数的句柄。 说明 函数句柄是一种表示函数的 MATLAB 数据类型。函数句柄的典型用法是将函数传递给另一个函数。例如&#xff0c;可以将函数句柄用作基于某个值范围计算数学表达式的…

c++基础知识补充4

单独使用词汇 using std::cout; 隐式类型转换型初始化&#xff1a;如A a1,,此时可以形象地理解为int i1;double ji;&#xff0c;此时1可以认为创建了一个值为1的临时对象&#xff0c;然后对目标对象进行赋值&#xff0c;当对象为多参数时&#xff0c;使用&#xff08;1&#xf…

微信小程序云开发教程——墨刀原型工具入门(添加交互事件)

引言 作为一个小白&#xff0c;小北要怎么在短时间内快速学会微信小程序原型设计&#xff1f; “时间紧&#xff0c;任务重”&#xff0c;这意味着学习时必须把握微信小程序原型设计中的重点、难点&#xff0c;而非面面俱到。 要在短时间内理解、掌握一个工具的使用&#xf…

【多线程】CAS详解

目录 &#x1f334;什么是 CAS&#x1f338;CAS 伪代码 &#x1f38d;CAS 是怎么实现的&#x1f340;CAS 有哪些应⽤&#x1f338;实现原子类&#x1f338;实现自旋锁 &#x1f333;CAS 的 ABA 问题&#x1f338;**什么是 ABA 问题**&#xff1f;&#x1f338;ABA 问题引来的 B…