博弈论

博弈论

记住:能转移到先手必败态的是先手必胜,只能转移到先手必胜的是先手必败

很多时候,它是与 DP/打表找规律相结合的

SG 函数也常见


常见模型

  • Nim 游戏:多堆石子,每次取一堆中任意个,无法行动者输,先手必败当且仅当 a1xora2xoran=0

    同时如果 aiss 为当前除 ai 外的异或和,则先手下一步可从 ai 中取

  • NimK:多堆石子,每次可从最多 k 堆中各取走任意个石子,无法行动者输,当且仅当石子数的二进制表示中每一位上 1 的个数和 0(modk+1) ,即 t[1,w],i=1naiand2t0(modk+1)

    Nim 游戏其实就是 NimK 游戏 k=1 的特殊形式

  • Bash 博弈:一堆石子共 n 个,每次只能取 1m 个,无法行动者输,当且仅当 n0(modm+1) 时先手必败

    此时后手只要和先手对称着取就必胜

  • Fibonacci Nim:一堆石子共 n 个,先手第一次取不能不取也不能取完,以后每次取的石子数不超过之前的 2 倍,无法行动者输,则当且仅当石子数为斐波那契数时先手必败

  • Wythoff’s game:2 堆石子个数为 a,b(a<b),每次只取一堆中任意个或两堆都取相同个,无法行动者输,则当且仅当 a=5+12k,b=a+k 时先手必胜,这种局势为奇异局势

  • Staircase Nim:n 级阶梯,每次可以把 1n 级上的某一级上任意个石子挪到下一级,第 0 级阶梯上石子无法移动,无法行动者输,则先手必败当前仅当奇数级阶梯上的石子数异或和为 0

    偶数级阶梯上石子个数不影响结果,可以通过调整保持必败必胜态的转移

  • Anti Nim:Nim 游戏,定义为取到最后一棵石子的输,则当且仅当所有石子数为 1SG0 或至少一堆石子数大于 1SG 不为 0 时先手必胜

    如果拓展至不同游戏的组合,则需 SJ 定理,必须满足所有 SG=0 的状态都无后继状态或有一个后继状态 SG=1


SG 函数

一个局面的 SG 值:它后继状态的 SG 值的 mex

一个游戏的 SG 值:它所有子游戏的 SG 值的异或和

注意区分 ”局面“ 和 ”游戏“

通常可以暴搜一下 SG 值,找找规律或 DP


博弈 DP

这是比较常见的考察形式

一般博弈双方都绝顶聪明,能预见未来,所以 DP 有后效性无前效性,从后往前 DP

CF1628D2 Game on Sum (Hard Version)

注意取实数,设 f(n,m) 表示当前还剩 n 次,其中有 m 次后手必须选加的 x

那么先手假设选 t,有两种情况:

  • 后手选择减,f(n,m)f(n1,m)t
  • 后手选择加,f(n,m)f(n1,m1)+t

此时后手一定会选让 x 小的那个,而先手想让最小值最大

因此先手要让 f(n1,m)t=f(n1,m1)+t,即 f(n,m)=f(n1,m)+f(n1,m1)2

边界是 f(i,0)=0,f(i,i)=ik

发现决策完全与 k 无关,可以直接当作 k=1 预处理出 k 的系数,最后答案为 f(n,m)×k

O(nm) 的 DP 可以通过 Easy Version

由于初始只有 f(i,i) 有值,考虑每个 f(i,i) 对答案的贡献

看作每次 (x,y) 只能走到 (x+1,y+1)(x+1,y),求 (i,i) 走到 (n,m) 的路径数,注意第一次只能走到 (i+1,i)

所以方案数为 (ni1mi)

由于每走一步系数除以 2,总答案为 i=1m(ni1mi)×ik2ni

注意特判 n=m

int main()
{
	fact[0] = invf[0] = 1;
	for(ll i = 1; i <= V; ++i)	fact[i] = fact[i - 1] * i % mod;
	invf[V] = qmi(fact[V], mod - 2), inv2[V] = qmi(qmi(2ll, V), mod - 2);
	for(ll i = V - 1; i > 0; --i)	invf[i] = invf[i + 1] * (i + 1) % mod, inv2[i] = inv2[i + 1] * 2ll % mod;
	read(t);
	while(t--)
	{
		read(n, m, k), ans = 0;
		if(n == m)	{print(k * m % mod), putchar('\n');	continue;}
		for(ll i = 1; i <= m; ++i)	ans = (ans + c(n - i - 1, m - i) * i % mod * k % mod * inv2[n - i] % mod) % mod;
		print(ans), putchar('\n');
	}
	return 0;
}

CF536D Tavas in Kansas

预处理出 s,t 到每个点的距离,然后分别从小到大排序,距离相同的放在一起,则两人当前取的点一定是某个前缀

所以状态就方便表示了:设 f(i,j) 表示先手已经选了前 i 个,后手已选前 j 个时先手还能获得的最大分数

枚举先手下一步选到 k,后手一定会选 l 使 f(k,l) 最小

因此 f(i,j)=maxk=sti,j{cost(i+1,k,j)+minl=st2i,jf(k,l)},其中 cost(i,k,j) 为后手选到 j 时先手选 ik 获得的收益

sti,j 为已选 i,j,先手下一步能选的最近位置(保证有新点加入),同理 st2i,j 为后手下一步能选的最近位置

可以预处理出 s(i,j) 表示后手选到 j 时先手从 1i 的收益,由于先手选的内部不重复,因此 cost(i+1,k,j)=s(k,j)s(i,j)

然后边 DP 边记录后缀最小值 mn(i,j)=min{mn(i,j+1),f(i,j)},更新最大值 mx(i,j)=max{mx(i+1,j),s(i,j)+mn(i,st2i,j)}

最终转移为 f(i,j)=mx(st(i,j),j)s(i,j),边界为覆盖了所有点的 f(i,j)=0

这样就能 O(n2) 完成转移,细节有一点多

ll n, m, S, T, u, v, d, p[N], dis[2][N], f[N][N], ful[N][N], vis[N][N], st[N][N], st2[N][N], cnt[2], mx[N][N], mn[N][N], s[N][N], tot;
map<ll, vector<ll> > dist[2];
vector<ll> lin[2][N];
vector<pll> edge[N];	bitset<N> book;
void dij(ll id, ll st)
{
	priority_queue<pll, vector<pll>, greater<pll> > heap;
	memset(dis[id], 0x3f, sizeof(dis[id])), book.reset();
	dis[id][st] = 0, heap.push(mp(0, st));
	while(!heap.empty())
	{
		ll x = heap.top().se;	heap.pop();
		if(book[x])	continue;
		book[x] = 1;
		for(pll y : edge[x])
			if(dis[id][y.fi] > dis[id][x] + y.se)
			{
				dis[id][y.fi] = dis[id][x] + y.se;
				heap.push(mp(dis[id][y.fi], y.fi));
			}
	}
	for(ll i = 1; i <= n; ++i)	dist[id][dis[id][i]].pb(i);
}
int main()
{
	read(n, m, S, T);
	for(ll i = 1; i <= n; ++i)	read(p[i]), tot += p[i];
	for(ll i = 1; i <= m; ++i)
	{
		read(u, v, d);
		edge[u].pb(mp(v, d)), edge[v].pb(mp(u, d)); 
	}
	dij(0, S), dij(1, T);
	for(iter it = dist[0].begin(); it != dist[0].end(); ++it)	lin[0][++cnt[0]] = it -> se;
	for(iter it = dist[1].begin(); it != dist[1].end(); ++it)	lin[1][++cnt[1]] = it -> se;
	for(ll j = 0; j <= cnt[1]; ++j)
	{
		book.set();
		for(ll k = 1; k <= j; ++k)	
			for(ll u : lin[1][k])	book[u] = 0;
		for(ll i = 1; i <= cnt[0]; ++i)
		{
            ll flag = 0;
			for(ll k : lin[0][i])
				if(book[k])	s[i][j] += p[k], flag = 1;
			s[i][j] += s[i - 1][j], vis[i][j] = flag;
		}
        for(ll i = cnt[0], k = cnt[0] + 1; i >= 0; --i)
        {
            st[i][j] = k;
            if(vis[i][j])   k = i; 
        }
	}
    memset(vis, 0, sizeof(vis));
    for(ll i = 0; i <= cnt[0]; ++i)
    {
        book.set();
        for(ll j = 1; j <= i; ++j)
            for(ll k : lin[0][j])   book[k] = 0;
        for(ll j = 0; j <= cnt[1]; ++j)
        {
            ll flag = 0;
            for(ll k : lin[1][j])
                if(book[k]) {flag = 1;  break;}
            vis[i][j] = flag;
        }
        for(ll j = cnt[1], k = cnt[1] + 1; j >= 0; --j)
        {
            st2[i][j] = k;
            if(vis[i][j])  
            {
                if(k == cnt[1] + 1)
                    for(ll u = j; u <= cnt[1]; ++u) ful[i][u] = 1;
                k = j;
            }
        }
    }
    if(tot > 0) // 先手全选即胜利
    {
        printf("Break a heart");
        return 0;
    } // 注意初始化,mx[cnt[0]][j] 看作先手直接全部取完
	memset(f, -0x3f, sizeof(f)), memset(mx, -0x3f, sizeof(mx)), memset(mn, 0x3f, sizeof(mn));
    for(int j = 0; j <= cnt[1]; ++j)    f[cnt[0]][j] = 0, mx[cnt[0]][j] = s[cnt[0]][j], ful[cnt[0]][j] = 1;
    for(int i = 0; i <= cnt[0]; ++i)    f[i][cnt[1]] = mn[i][cnt[1]] = 0, mx[i][cnt[1]] = s[i][cnt[1]], ful[i][cnt[1]] = 1;
	for(int i = cnt[0] - 1; i >= 0; --i)
		for(int j = cnt[1] - 1; j >= 0; --j)
		{
            if(ful[i][j])   f[i][j] = 0;
			else    f[i][j] = mx[st[i][j]][j] - s[i][j];
            mn[i][j] = min(mn[i][j + 1], f[i][j]);
            if(mn[i][st2[i][j]] < inf) mx[i][j] = max(mx[i + 1][j], s[i][j] + mn[i][st2[i][j]]);
            else    mx[i][j] = max(mx[i + 1][j], s[i][j]); // 注意存在 f 才用它更新,否则为后手直接取完全部
        }
	if(f[0][0] == tot - f[0][0])	printf("Flowers");
	else if(f[0][0] < tot - f[0][0])	printf("Cry");
	else	printf("Break a heart");
	return 0;
}

二分图博弈

定义:有一张二分图,起始节点为左部点 S,先后手交替移动,每条边只能经过一次,不能移动的一方输掉游戏

先说结论,当 S 在图上的每一组最大匹配中时,先手必胜

证明:

S 在每一组最大匹配中,则先手先走一条匹配边到 x,后手从 x 走到 yy 一定有匹配边,否则 (S,x) 所在匹配中把它换成 (x,y) 最大匹配不会变而 S 不在匹配中,同理之后若后手走到的点无匹配边,则相当于找到了增广路,能调整使得 S 不在最大匹配中,矛盾

因此先手每次能沿着匹配边走,先手必胜

若存在最大匹配使得 S 不在其中,则先手走到 x 后,之后后手每次肯定能走一条匹配边 (x,y),因为如果某次后手走了非匹配边 (z,w),之前先手走了 (y,z),则可以调整,将 (x,y) 所在匹配中它换成 (S,x),(y,z) 加入最大匹配,使得最大匹配增加,矛盾

因此后手每次能沿着匹配边走,后手必胜

那么如果只对一个点求,可以直接去掉这个点重跑最大匹配

找出所有的非必经点,则从每个非匹配点 x,找到 z 使得 (x,y) 为非匹配边,(y,z) 为匹配边,将 x,z 连边后遍历新图,被遍历到的都是非必经点

posted @   KellyWLJ  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示