mengxiaolong

 

博弈论算法总结

正在完善!

何为博弈论

博弈论 ,是经济学的一个分支,主要研究具有竞争或对抗性质的对象,在一定规则下产生的各种行为。博弈论考虑游戏中的个体的预测行为和实际行为,并研究它们的优化策略。

先来看一道小学就接触过的思维题

你和好基友在玩一个取石子游戏。面前有30颗石子,每次只能取一颗或两颗,你先取,取完的人为胜,问你是否有必胜策略

Q:什么?有必胜策略?能否胜利不应该随着我们选择而改变吗?
A:确实。但如果我们足够聪明呢?每次都做最优的选择,把取胜之路留给自己
Q:我一点也不聪明,那该如何做呢?

先从简单入手,
假如只有一个或两个石子,无疑先手必胜
只有三个石子,无疑先手必输

(我们约定先手必败状态为必败状态,先手必胜状态为必胜状态)
这就是我们的终止状态,即无论怎么拿,都会回到这几个状态
因为我们想赢,所以我们要让自己处于必胜状态,即剩下一个或两个石子的时候,我们是先手。不难发现,我们也许不能使自己处于必胜态,但我们可以让对方处于必败态。即剩下三个石子的时候,我们是后手。

不难发现,只要是三的倍数就一定是必败状态,否则就是必胜状态。
证明:
假设不是三的倍数,我们使它成为三的倍数,此时我们是后手。对方如果拿一个,我们就拿两个;如果拿两个,我们就拿一个。所以我们那完后剩下的一定永远是三的倍数,所以只剩下三个石子的时候我们一定是后手,此时对手必输,也就是我们必胜。
假设是三的倍数,因为两个人都足够聪明,所以对方一定会使我们永远处于三的倍数中。所以我们必败。
所以只要判断是不是三的倍数,就可以确定我们是否必胜了

至此,小学时代遗留的问题已经解决了可以拿去欺负同学,(这也是博弈论最基础的问题,Nim游戏)
可以说,你已经学会博弈论了

下面我主要讲一些关于算法比赛中用到的博弈类型:

首先你要理解必胜状态和必败状态:

  对下先手来说,

  一个状态是必败状态当且仅当它的所有后继都是必败状态。

  一个状态是必胜状态当且仅当它至少有一个后继是必败状态。

  就是说,博弈者,一旦捉住了胜利的把柄,必然最后胜利。

博弈中常常用到的:

  两个数,不用中间变量实现交换。
  a b;
  a = a^b;
  b = a^b;
  a = a^b;

博弈图和状态

把每个可到达的状态都看做结点,每次做出决策都是从旧的状态转移到新的状态,也就是在两个状态结点间连一条有向边。如果把所有状态转移都画出来,我们就得到了一张博弈图

就像这样

image

大多数博弈图会是一个DAG,否则游戏不可能结束

三个基本定理

  • 定理一:没有后继状态的状态是必败状态
  • 定理二:一个状态是必胜状态 当且仅当 存在至少一个必败状态为它的后继状态。
  • 定理三:一个状态是必败状态 当且仅当 它的所有后继状态均为必胜状态。

对于定理一,游戏进行不下去了,即这个玩家没有可操作的了,那么这个玩家就输掉了游戏

对于定理二,如果该状态至少有一个后继状态为必败状态,那么玩家可以通过操作到该必败状态;此时对手的状态为必败状态,即对手必定是失败的,而相反地,自己就获得了胜利。

对于定理三,如果不存在一个后继状态为必败状态,那么无论如何,玩家只能操作到必胜状态;此时对手的状态为必胜状态——对手必定是胜利的,自己就输掉了游戏。

SG函数

有向图游戏是一个经典的博弈游戏——实际上,大部分的公平组合游戏都可以转换为有向图游戏。

在一个有向无环图中,只有一个起点,上面有一个棋子,两个玩家轮流沿着有向边推动棋子,不能走的玩家判负。

定义 mex 函数的值为不属于集合 S 中的最小非负整数,即:

             mex(S) = min{x}  ( x ∉ S , x ∈ N )

例如 mex({0,1,3,4})=1,mex({1,2})=0,mex({})=0

对于状态 x 和它的所有 k 个后继状态 y1,y2,...,yk,定义 SG 函数:

          SG(x)=mex{SG(y1),SG(y2),...,SG(yk)}

SG定理:

而对于由 n 个有向图游戏组成的组合游戏,设它们的起点分别为 s1,s2,...,sn ,则有定理: 当且仅当
SG(s1)⊕SG(s2)⊕...⊕SG(sn)≠0 时,这个游戏是先手必胜的。

还是拿原来那个图开刀

image

用 SG[] 数组来存所有结点的 SG 函数值因为 9,3,8,10,4 这几个点都没有后继状态,所以它们 SG 值均为 0,同理推出 2,7,5这个点的 SG 值为 1,而

                      SG[6]=mex(SG[7],SG[8])=2

                  SG[1]=mex(SG[2],SG[5],SG[6],SG[4])=3

把 Nim游戏 转化为有向图游戏

我们可以将一个有 x 个物品的堆视为节点 x ,拿掉若干个石子后剩下 y个,则当且仅当 0<y<x 时,节点 x 可以到达 y 。

那么,由 n 个堆组成的 Nim 游戏,就可以视为 n 个有向图游戏了。根据上面的推论,可以得出 SG(x)=x 。

再根据 SG 定理,就可以得出 Nim 和的结论了。

博弈论DP

不得不说,博弈论DP就是个神仙做法,能用博弈论DP做的都是神仙题!

并没有什么固定的做法,但基本原理还是照着那三个定理来。能用DP的一般是因为想不出来如何用 SG 定理。状态的设计都比较神仙,主要是根据题目要求来设计。

习题

P2197 【模板】Nim 游戏

题目本意是给出 n 堆石子,轮流在其中一堆去任意个,谁不能去谁就输。

思路:证明所有石子异或和为0则先手必输

证明:

  1. 反正最终情况就是每堆都为0,先手必输,所以我们考虑怎么把情况转换到这里。

  2. 如果异或和的最高位为i,则有一堆石子第i为为1(不然怎么会有i位)

  3. 设A1就为那堆石子,其他堆石子异或和设为x,总异或和设为k,则 A1 xor x=k,把A1变成A1 xor k,那么后手面对的则是(A1 xor k)xor x=0,

    举个例子:11001 xor 11100=101,则有(11001 xor 101)xor 11100=0

  4. 如果现在的异或和已经为0了(不为最终情况),那么怎么转换异或和都不能为0

  5. 好,我们根据3 4点得出:如果先手异或和不为0,可以一步让后手的情况为异或和为0;如果先手异或和为0,那么后手异或和就不为0

  6. 终于开始进行游戏了,如果现在先手面对的情况异或和不为0,则一直让后手异或和为0,最后面对最终情况,后手输,则先手赢;如果先手面对的情况异或和为0,后手则赢

代码如下:

#include<bits/stdc++.h>
using namespace std;

int main(){
	int t;
	cin>>t;
	while(t--){
		int s=0;
		int n;
		cin>>n;
		for(int i=1;i<=n;i++){
			int a;
			cin>>a;
			s^=a;
		}
		if(s==0)cout<<"No"<<endl;
		else cout<<"Yes"<<endl;
	}
	return 0;
}

P1247 取火柴游戏

这个游戏的SG值就是各堆数量的异或和,当SG为0时先手必败,否则先手就要把SG变为0

先检验SG值,如果是0输出lose,否则我们按照以下原则行动

有n个数的异或值不为0 现在要减少一个数使异或值为0

假设n个数:a1 ,a2,a3...an

a1 ^ a2 ^ a3 ^ .. ^ an=k

那么我们可以对一个数进行操作,假设这个数是a1,设a1 ^ k = a',a' ^ a2 ^ a3 ^ ... ^ an = a1 ^ a2 ^ a3 ^ ... ^ an ^ k = k ^ k = 0;

所以我们只需要从头到尾检验每个数异或k的值是否比它小(因为是要减少),遇到小的直接输出ai-ai ^ k即可

代码如下:


#include<bits/stdc++.h>
using namespace std;
int k,n[500010];
int main(){
	cin>>k;
	int x=0;
	for(int i=1;i<=k;i++){
		cin>>n[i];
		x^=n[i];
	}
	if(!x){
		cout<<"lose"<<endl;
		return 0;
	}
	for(int i=1;i<=k;i++){
		if((n[i]^x)>=n[i])continue;
		cout<<n[i]-(n[i]^x)<<" "<<i<<endl;
		n[i]=n[i]^x;
		break;
	}
	for(int i=1;i<=k;i++)cout<<n[i]<<" ";
	return 0;
}

P4101 [HEOI2014] 人人尽说江南好

首先,合并次数为奇数先手必胜,偶数后手必胜,那么两个人都会尽可能向着自己想要的方向去发展(即拉到总合并次数为奇数/偶数)

当n ≤ m时,这种情况最后肯定能合成一堆,我们称这个较大的堆为【大堆】。

假如现在轮到先手操作,先手还没动,这个时候最长合并次数为偶数次,那么先手有两种可能性,把后面一个堆丢进大堆里面,这样后手再丢一个小堆进去,或者把后面两个堆合成一个,那么后手就可以把这个合成的直接丢进去。

无论怎么做,后手都能保证每轮完了之后,大堆的石子会增加两个,那么合并次数也会-2,一直保持为偶数。直到最后先手合无可合。

因此就可以保证剩下的合并次数为原始奇/偶,同时这种方式也会把局数拉到最多。

想办法弄到对自己有利

如果拉到最长的操作次数我们必胜的话,那么不管对面怎么操作,我们都能用上述方法拉回来

同样如果最长的操作是对面必胜,不管我们怎么合并,对面也能用上述方法拉回来

以及

我们已经发现最长的情况我们必胜,无论对手怎么操作我们都能拖到最长,我们必胜

对手发现最长他必胜,无论我们怎么操作,他也肯定能往下拖。

所以对最长必胜的那一方来说,一直拖到最长就是最优策略

而我们已经分析了必胜的那一方总是能拖下去

即这是一种必然取胜的方法,且如果对手不按套路出牌我们也能拖回来。

因此最终答案就是最长能拖到的次数,如果为奇先手胜,如果为偶后手胜。

最长次数
在n<=m的情况下可以轻松判断出是n-1;

若n>m,则最后最大合并后堆数一定是{m,m,m,m,n%m}; (因为如果是形如{m,m,m,m-x,m-x}的形式,不好算,我觉得其实是没什么不一样的,可能仅仅只是不好算吧)

因此 ans=(n/m)×(m-1)+n%m?n%m-1:0;

代码如下:

#include<bits/stdc++.h>
using namespace std;
int T;
long long n,m;
int main(){
	cin>>T;
	while(T--){
		cin>>n>>m;
		long long ans=(n/m)*(m-1)+((n%m)?(n%m-1):0);
		if(ans&1)cout<<0<<endl;
		else cout<<1<<endl;
	}
	return 0;
}

P3480 [POI2009] KAM-Pebbles

设a[i]表示第i堆石子的个数,c[i]表示a[i]-a[i-1],则我们每堆可以拿的石子数即为c[i]。当我们在第i堆拿了x个时,c[i]变成了c[i]-x,c[i+1]变成了c[i+1]+x,相当于我们把第i堆中可拿的石子转移到了i+1堆,由此我们可以把此题转化为一道反着的阶梯nim游戏。

下面简单讲解一下阶梯nim,如果不懂的话可以去网上搜一下。

阶梯nim是指,有n堆石子,每次我们可以从第i堆的石子中拿走一部分放到第i-1堆中,或者把第1堆中的石子拿走一部分,无法操作的人算输。先说结论:阶梯nim的游戏结果与只看奇数堆的石子数的普通nim结果相同。

假设我先手,那么我可以按照必胜策略把奇数堆中的石子转移到偶数堆,当对方拿的时候我们分情况讨论:

  1. 对方拿奇数堆中的石子到偶数堆,相当于进行对于奇数堆的普通nim,我们继续按照必胜策略拿奇数堆中的石子;

  2. 对方把偶数堆的石子拿到奇数堆,则我们可以把这部分石子继续向下拿,对于奇数堆相当于局势没有变动。

代码如下:

#include<bits/stdc++.h>
using namespace std;
int T; 
const int N=1e3+10;
int a[N];
int b[N];
int n;
int main(){
	cin>>T;
	while(T--){
		cin>>n;
		for(int i=1;i<=n;i++){
			cin>>a[i];
			b[i]=a[i]-a[i-1];
		}
		int ans=0;
		for(int i=n;i>=1;i-=2){
			ans^=b[i];
		}
		if(ans)cout<<"TAK"<<endl;
		else cout<<"NIE"<<endl;
	}
	return 0;
}

最后来一道黑体吧!

P3235 [HNOI2014] 江南乐

我们在整体上使用 SG 函数的性质。

我们单独求出每一个石子堆的 SG 函数值,最后求异或和即可。

我们可以写记忆化搜索,因为有用的 SG 函数项不多,这样节省时间。

问题中的操作将石子分成了很多石子,不好处理。

但是,种类只有两类,同样的石子堆的 SG 函数值肯定相同。

分成多个石子堆后,注意到有很多相同的,根据 SG 函数的性质,可以异或抵消。

所以其实相同的石子堆 SG 函数值异或之后抵消了。

对于操作后产生的每种石子堆,我们可以根据石子数量奇偶性质思考讨论。

如果是奇数,那最后还有一个没抵消。

如果是偶数,那么全部抵消掉了。

也就是看起来很多堆石子,其实到头来,每个种类最多一个最少没有。

这时候还不够,需要剪枝。

最后要用到分块。

代码如下:

#include<bits/stdc++.h>
using namespace std;
int T,F;
int n;
const int N=110;
const int M=1e5+5;
int sg[M];
int dfs(int x){
	if(~sg[x])return sg[x];
	if(x<F)return sg[x]=0;
	int vis[M];
	memset(vis,0,sizeof vis);
	for(int i=2;i<=x;i++){
		int sum=0;
		if((x+i-1)/i<F){
			vis[sum]=1;
			break;
		}
		if(x%i&1)sum^=dfs(x/i+1);
		if((i-x%i)&1)sum^=dfs(x/i);
		vis[sum]=1;
	}
	for(int i=0;i<=100;i++){
		if(!vis[i]){
			sg[x]=i;
			return i;
		}
	}
}
int main(){
	cin>>T>>F;
	memset(sg,-1,sizeof sg);
	while(T--){
		cin>>n;
		int ans=0;
		for(int i=1;i<=n;i++){
			int x;
			scanf("%lld",&x);
			ans^=dfs(x);
		}
		if(!ans)printf("0 ");
		else printf("1 ");
	}
	return 0;
}

posted on   zsfzmxl  阅读(59)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· DeepSeek 解答了困扰我五年的技术问题。时代确实变了!
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 趁着过年的时候手搓了一个低代码框架
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!

导航

统计

点击右上角即可分享
微信分享提示