【学习笔记】博弈论基础
博弈论基础
这里主要讨论两人博弈的博弈,不讨论前沿的多人博弈。
点击查看目录
前置知识:
- 注意,无特殊说明,所有博弈论的题目均已双方会选择最优方案的前提下进行。
(所以据说我们 老师想要出一个概率出错的博弈论(
- 平等组合游戏 :两人轮流操作,限制条件对两人相同,有有限状态集合,有终止状态且可以达到的游戏。
所以说五子棋不算平等组合游戏,因为执黑不等于执白。
而我们的博弈游戏,大多是平等组合游戏。
- 必胜状态 ,必败状态 :
(以下所有都不用英文字母表示状态,答案是我猪脑过载反应不过来)
P-position
表示 previous
,代表先手必败,即后手必胜,N-position
表示 next
,代表先手必胜,后手必败。
很多博文都用字母表示,需要知道(
先手必胜与先手必败
在博弈论中,往往存在必胜与必败的状态。
例如:有一堆石子,数目为 ,初音ミク
和 歌爱ユキ
两个人一次只能取 个或者 个或者 个(不能不取),取到最后一个石子的人获胜,初音ミク
先手。
在这之中,明显的,如果 初音ミク
必败,因为她不能一次取完但是 歌爱ユキ
可以。
因此, 时,该状态是先手必败状态。
而对于其他的,初音ミク
可以通过取几个石子让 ,且转化为 歌爱ユキ
先手,所以必胜。
因此我们可以将状态转移构成一个有向无环图,每个状态要么是先手必胜,要么是先手必败,有:
-
终止状态是先手必败状态。
-
如果一个状态只能转移到必胜,这个状态就是必败。
-
如果可以转移到必败,这个状态就是必胜。
(部分边未画出)
nim游戏:
有 堆石子,初音ミク
和 歌爱ユキ
轮流选一堆石子取若干(不可不取),没有石子可取的人失败。
(我觉得看完前置知识可以思考一下所以可以不要往下急着走)
ewq
有点复杂是吧?
一点点来推吧:
最终状态是一个不剩,是必败状态。
-
如果只剩下一堆石子,全拿走可以转换到最终状态(必败状态),所以只剩下一堆石子是必胜状态。
-
如果只剩下两堆石子:
拿 举例子:
(ps:因为只要能转移到必败状态就是必胜状态,所以省去了一下边和点)
从左上到右下解释一下,因为 是先手必败,所以能转移到它的父局面 是先手必胜,只能转移到 的父局面 是先手必败, 能够转移到必败的 是先手必胜。
就像 dfs
一样,而存在大量的重合子,可以考虑记忆化搜索。
时间复杂度是 。
但是通过一个定理可以让时间复杂度极大地降低。
定理:如果所有石子数量亦或为 ,先手必败,否则先手必胜。
而证明这个定理就要证明三个小定理:
-
定理 :最终状态是必败状态。
-
定理 :对于 来说,一定存在某种对策使其等于 。
-
定理 :对于 来说,无论如何对策都没有继续等于 的可能。
我们主要来证明难以理解的定理 :
设 ,而其二进制最高位数是 。
要让其成为 ,那么就要让 。
其第 位置上数是 ,代表着一定有奇数个 的第 位置是 。
那么一定有 。
具有合法对策。
证毕。
Miku's Code
#include<bits/stdc++.h> using namespace std; #define il inline #define rg register int typedef long double llf; typedef long long ll; typedef pair<int,int> PII; const double eps=1e-8; namespace mystd{ il int Max(int a,int b)<%if(a<b) return b;return a; %> il int Min(int a,int b)<%if(a>b) return b;return a; %> il int Abs(int a)<% if(a<0) return a*(-1);return a; %> il double fMax(double a,double b)<%if(a<b) return b;return a; %> il double fMin(double a,double b)<%if(a>b) return b;return a; %> il double fAbs(double a)<% if(a<0) return a*(-1);return a; %> il int dcmp(double a){ if(a<-eps) return -1; if(a>eps) return 1; return 0; } }const int maxn=1e4+50; int T,n,a[maxn]; int mj; il void clear(){ for(rg i=1;i<=n;++i) a[i]=0; mj=0; } int main(){ scanf("%d",&T); while(T--){ scanf("%d",&n); for(rg i=1;i<=n;++i){ scanf("%d",&a[i]); } for(rg i=1;i<=n;++i) mj=mj^a[i]; if(mj==0) printf("No\n"); else printf("Yes\n"); clear(); } return 0; }
杂题乱写(?)
[ARC131C] Zero XOR
[ARC131C] Zero XOR
题面翻译
给定 堆石子,个数分别为 ,两两不同。
两个玩家轮流在上面操作,每次操作将任意一堆石子的个数变为 ,当拿走后 ,则该玩家获胜。
若先手有必胜策略,输出 Win
,否则输出 Lose
。
题目描述
机の上に 枚のクッキーがあります。クッキーの表面にはそれぞれ正の整数 が書かれており、これらはすべて異なります。
このクッキーを使って 2 人でゲームを行います。このゲームでは、各プレイヤーは次の行動を交互に行います。
机にあるクッキーを 1 枚選んで食べる。
その際に、机に残ったクッキーに書かれた整数の が になったならば、そのプレイヤーは勝利し、ゲームは終了する。
あなたは E869120 君に対戦を申し込みました。あなたは先手で、E869120 君は後手です。さて、両者が最適に行動したときに、あなたは E869120 君に勝ちますか?
とは 整数 のビット単位 XOR、 は、以下のように定義されます。
- を二進表記した際の ( ) の位の数は、 を二進表記した際の の位の数のうち一方のみが であれば 、そうでなければ である。
例えば、 となります (二進表記すると: )。
一般に、 個の整数 のビット単位 XOR は と定義され、これは の順番によらないことが証明できます。特に の場合、 は となります。
输入格式
入力は以下の形式で標準入力から与えられます。
输出格式
両者が最適に行動したときにあなたが勝つなら Win
、負けるなら Lose
と出力してください。
样例 #1
样例输入 #1
6 9 14 11 3 5 8
样例输出 #1
Lose
样例 #2
样例输入 #2
1 131
样例输出 #2
Win
样例 #3
样例输入 #3
8 12 23 34 45 56 78 89 98
样例输出 #3
Win
提示
制約
- はすべて異なる
- の は ではない
- 入力はすべて整数
Sample Explanation 1
この例では、あなたがどんな方法を使っても、E869120 君が最適に行動し続ければ負けてしまいます。 例えば、最初に が書かれたクッキーを食べるとしましょう。すると、次に E869120 君が が書かれたクッキーを食べることで、残ったクッキーに書かれた数 の が になるので、E869120 君が勝ちます。 それ以外の行動をとっても、最終的には E869120 君が勝ちます。
Sample Explanation 2
この例では、あなたは最初のターンで が書かれたクッキーを食べることしかできません。すると、机の上からクッキーがなくなるので、残ったクッキーに書かれた数の は になります。したがって、E869120 君が何もできないまま、あなたが勝ちます。
解题:
形式化一下:有长度为 的数列,任意删数,最后数列亦或和为 赢。
有两种情况:
-
把数全删了, 为奇数先手赢, 为偶数后手赢。
-
数列异或和等于数列某个数,删掉该数先手赢。
结束。
Miku's Code
#include<bits/stdc++.h> using namespace std; #define il inline #define rg register int typedef long double llf; typedef long long ll; typedef pair<int,int> PII; const double eps=1e-8; namespace mystd{ il int Max(int a,int b)<%if(a<b) return b;return a; %> il int Min(int a,int b)<%if(a>b) return b;return a; %> il int Abs(int a)<% if(a<0) return a*(-1);return a; %> il double fMax(double a,double b)<%if(a<b) return b;return a; %> il double fMin(double a,double b)<%if(a>b) return b;return a; %> il double fAbs(double a)<% if(a<0) return a*(-1);return a; %> il int dcmp(double a){ if(a<-eps) return -1; if(a>eps) return 1; return 0; } }const int maxn=400050; int n,a[maxn],res; int main(){ scanf("%d",&n); for(rg i=1;i<=n;++i){ scanf("%d",&a[i]); res=res^a[i]; } for(rg i=1;i<=n;++i){ if(res==a[i]) <% printf("Win");return 0; %> } if(n%2==0) <% printf("Lose");return 0; %> else <% printf("Win");return 0; %> return 0; }
[ARC143C] Piles of Pebbles
[ARC143C] Piles of Pebbles
题面翻译
edge 有 堆鹅卵石,她邀请高桥和青木参加一个游戏。
高桥和青木将轮流操作,高桥先手,当某个人无法操作是那个人就输了。
游戏有以下规则:
-
当前操作者需要选择一个或多个堆,移走 或 颗石子。如果当前是高桥操作,则移走 颗,是青木则移走 颗。
-
所有选择的堆的石子数均需大于等于移走的石子数。
若两个人都足够聪明,谁会赢?
Translated by Tx_Lcy
题目描述
小石の山が 個あります.最初, 番目の山には 個の小石があります.
これらを用いて,高橋君と青木君がゲームをします. 高橋君から始めて,交互に次の操作を行い,操作を行えなくなった方が負けとなります.
- 山を つ以上選び,選んだそれぞれの山から,高橋君の操作の場合は 個ずつ,青木君の操作の場合は 個ずつ小石を取り除く. ただし,小石の個数が足りない山を選ぶことはできない.
二人が最適に行動したとき,どちらがゲームに勝つかを求めてください.
输入格式
入力は以下の形式で標準入力から与えられる.
输出格式
このゲームに勝つのが高橋君の場合 First
を,青木君の場合 Second
を出力せよ.
样例 #1
样例输入 #1
2 1 1 3 3
样例输出 #1
First
样例 #2
样例输入 #2
2 1 2 3 3
样例输出 #2
Second
提示
制約
Sample Explanation 1
例えば,以下のようなゲームの進行が考えられます. - 高橋君が両方の山から石を つ取り除く. - 青木君が 番目の山から石を つ取り除く. - 高橋君が 番目の山から石を つ取り除く. - 青木君が 番目の山から石を つ取り除く. - 高橋君が 番目の山から石を つ取り除く. 青木君がどのように操作を行っても高橋君が勝つことができるので,答えは First
です.
解题:
啊啊,这还是第一次遇到规定两个人取的不一样的题目呢。
所以其实数列中所有数都对 取模,再计算的意义是相同的。
而针对已经取模后的数列 ,显然就有了最终状态:
- 数列中所有数 。
而针对数列中有数 的情况,我们分类讨论:
- 若 ,先手必赢。
因为可以取很多堆石子,把那些大于 的全取完就行。
- 若 再次分类讨论:
如果存在若干堆石子数大于 且 ,后手必胜,原因和上面相同,我可以一次取完。
反之,先手必胜。
Miku's Code
#include<bits/stdc++.h> using namespace std; #define il inline #define rg register int typedef long double llf; typedef long long ll; typedef pair<int,int> PII; const double eps=1e-8; namespace mystd{ il int Max(int a,int b)<%if(a<b) return b;return a; %> il int Min(int a,int b)<%if(a>b) return b;return a; %> il int Abs(int a)<% if(a<0) return a*(-1);return a; %> il double fMax(double a,double b)<%if(a<b) return b;return a; %> il double fMin(double a,double b)<%if(a>b) return b;return a; %> il double fAbs(double a)<% if(a<0) return a*(-1);return a; %> il int dcmp(double a){ if(a<-eps) return -1; if(a>eps) return 1; return 0; } }const int maxn=2e5+50; int n,x,y,a[maxn]; bool mj,jj; int main(){ scanf("%d %d %d",&n,&x,&y); ll mod=x+y; for(rg i=1;i<=n;++i){ scanf("%d",&a[i]); a[i]=a[i]%mod; if(a[i]>=x) mj=true; if(a[i]>=y && a[i]<x) jj=true; } if(mj==false) <% printf("Second");return 0; %> if(x<=y) <% printf("First");return 0; %> if(jj==true) <% printf("Second");return 0; %> return 0; }
[ABC261Ex] Game on Graph
解题:
样例分析:
(题意我觉得说的挺明白的)
把图都画出来:
-
样例 #1:排除贪心,如果是贪心应该先走左边,但显然我们的
Takahashi
和Aoki
都是绝顶聪明的,应该考虑动态规划。 -
样例 #2:如果有一个图没有任何一个点出度为 ,该图没有结果。
-
样例 #3:不是只要有环该图就没有结果,要考虑在当前点的人是谁,希望跑大的
Aoki
肯定跑死循环,但是希望跑小的Takahashi
肯定跑通向结束的边,因此我们的动态规划设计中应该包含“在该点的人”的信息。
设计方程:
我们先假设这是一个有向无环图,那么设 表示 Takahashi
和 Aoki
在点 时,游戏结束的值。
有:
根据这个方程我们建反图,从没有出度的点往上跑。
所以设有出度的点 的 为极大值, 为 。
然后考虑两个人该怎么更新他们的方程:
-
希望跑小值的
Takahashi
就可以直接跑最小值,和我们正常的 Dijkstra 相同。 -
希望跑大值甚至死循环的
Aoki
就要等到其所在的点的子节点全部更新完在走,所以每走到他的一次子节点,我们给他的出度减 ,直到其出度为 ,我们再放入小根堆中。
所以这样就解决了环的问题。
最终答案是 ,如果是极大值那么输出 INFINITY
。
Miku's Code
#include<bits/stdc++.h> using namespace std; #define il inline #define rg register int #define next Hatsune typedef long double llf; typedef long long ll; typedef pair<int,int> PII; const double eps=1e-8; namespace mystd{ il int Max(int a,int b)<%if(a<b) return b;return a; %> il int Min(int a,int b)<%if(a>b) return b;return a; %> il int Abs(int a)<% if(a<0) return a*(-1);return a; %> il double fMax(double a,double b)<%if(a<b) return b;return a; %> il double fMin(double a,double b)<%if(a>b) return b;return a; %> il double fAbs(double a)<% if(a<0) return a*(-1);return a; %> il int dcmp(double a){ if(a<-eps) return -1; if(a>eps) return 1; return 0; } }const int maxn=2e5+50,maxm=2e5+50; const ll MYMAX=(ll)0x3f3f3f3f3f3f3f3f; int n,m,s,du[maxn]; bool vis[maxn][2]; ll f[maxn][2]; int head[maxm<<1],t; struct edge{ int u,v,w; int next; };edge e[maxm<<1]; il void add_edge(int u,int v,int w){ e[++t].u=u; e[t].v=v; e[t].w=w; e[t].next=head[u]; head[u]=t; } struct Node{ int v; char type; ll w; }; bool operator>(Node n1,Node n2){ return n1.w>n2.w; } priority_queue<Node,vector<Node>,greater<Node> >q; void dijkstra(){ while(!q.empty()){ Node s=q.top(); q.pop(); int u=s.v,type=s.type-'0'; if(vis[u][type]==true) continue; vis[u][type]=true; if(type==1){ for(rg i=head[u];i;i=e[i].next){ int to=e[i].v; if(f[to][0]>f[u][1]+e[i].w){ f[to][0]=f[u][1]+e[i].w; q.push({to,'0',f[to][0]}); } } } else{ for(rg i=head[u];i;i=e[i].next){ int to=e[i].v; --du[to]; if(f[to][1]<f[u][0]+e[i].w) f[to][1]=f[u][0]+e[i].w; if(du[to]==0){ q.push({to,'1',f[to][1]}); } } } } } il void pre(){ for(rg i=1;i<=n;++i){ if(du[i]==0){ Node s; s.v=i; s.type='0'; s.w=0; q.push(s); s.type='1'; q.push(s); } else f[i][0]=MYMAX; } } il void input(){ scanf("%d %d %d",&n,&m,&s); int u,v,w; for(rg i=1;i<=m;++i){ scanf("%d %d %d",&u,&v,&w); ++du[u]; add_edge(v,u,w); } } int main(){ input(); pre(); dijkstra(); if(f[s][0]>1e18) <% printf("INFINITY");return 0; %> else printf("%lld",f[s][0]); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现