博弈论

博弈论

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

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

\(SG\) 函数也常见


常见模型

  • Nim 游戏:多堆石子,每次取一堆中任意个,无法行动者输,先手必败当且仅当 \(a_1\operatorname{xor}a_2\dots\operatorname{xor}a_n=0\)

    同时如果 \(a_i\ge s\)\(s\) 为当前除 \(a_i\) 外的异或和,则先手下一步可从 \(a_i\) 中取

  • NimK:多堆石子,每次可从最多 \(k\) 堆中各取走任意个石子,无法行动者输,当且仅当石子数的二进制表示中每一位上 \(1\) 的个数和 \(\equiv0\pmod{k+1}\) ,即 \(\forall t\in[1,w],\sum_{i=1}^n a_i\operatorname{and} 2^t\equiv0\pmod{k+1}\)

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

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

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

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

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

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

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

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

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


SG 函数

一个局面的 SG 值:它后继状态的 SG 值的 \(\operatorname{mex}\)

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

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

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


博弈 DP

这是比较常见的考察形式

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

CF1628D2 Game on Sum (Hard Version)

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

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

  • 后手选择减,\(f(n,m)\gets f(n-1,m)-t\)
  • 后手选择加,\(f(n,m)\gets f(n-1,m-1)+t\)

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

因此先手要让 \(f(n-1,m)-t=f(n-1,m-1)+t\),即 \(f(n,m)=\frac{f(n-1,m)+f(n-1,m-1)}{2}\)

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

发现决策完全与 \(k\) 无关,可以直接当作 \(k=1\) 预处理出 \(k\) 的系数,最后答案为 \(f(n,m)\times 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)\)

所以方案数为 \(n-i-1\choose m-i\)

由于每走一步系数除以 \(2\),总答案为 \(\sum_{i=1}^m {n-i-1\choose m-i}\times \frac{i\cdot k}{2^{n-i}}\)

注意特判 \(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)=\max_{k=st_{i,j}}\{cost(i+1,k,j)+\min_{l=st2_{i,j}} f(k,l)\}\),其中 \(cost(i,k,j)\) 为后手选到 \(j\) 时先手选 \(i\sim k\) 获得的收益

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

可以预处理出 \(s(i,j)\) 表示后手选到 \(j\) 时先手从 \(1\sim i\) 的收益,由于先手选的内部不重复,因此 \(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,st2_{i,j})\}\)

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

这样就能 \(O(n^2)\) 完成转移,细节有一点多

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\) 走到 \(y\)\(y\) 一定有匹配边,否则 \((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 @ 2024-02-15 10:35  KellyWLJ  阅读(6)  评论(0编辑  收藏  举报