博弈论
博弈论
记住:能转移到先手必败态的是先手必胜,只能转移到先手必胜的是先手必败
很多时候,它是与 DP/打表找规律相结合的
函数也常见
常见模型
-
Nim 游戏:多堆石子,每次取一堆中任意个,无法行动者输,先手必败当且仅当
同时如果 , 为当前除 外的异或和,则先手下一步可从 中取
-
NimK:多堆石子,每次可从最多 堆中各取走任意个石子,无法行动者输,当且仅当石子数的二进制表示中每一位上 的个数和 ,即
Nim 游戏其实就是 NimK 游戏 的特殊形式
-
Bash 博弈:一堆石子共 个,每次只能取 个,无法行动者输,当且仅当 时先手必败
此时后手只要和先手对称着取就必胜
-
Fibonacci Nim:一堆石子共 个,先手第一次取不能不取也不能取完,以后每次取的石子数不超过之前的 倍,无法行动者输,则当且仅当石子数为斐波那契数时先手必败
-
Wythoff’s game: 堆石子个数为 ,每次只取一堆中任意个或两堆都取相同个,无法行动者输,则当且仅当 时先手必胜,这种局势为奇异局势
-
Staircase Nim: 级阶梯,每次可以把 级上的某一级上任意个石子挪到下一级,第 级阶梯上石子无法移动,无法行动者输,则先手必败当前仅当奇数级阶梯上的石子数异或和为
偶数级阶梯上石子个数不影响结果,可以通过调整保持必败必胜态的转移
-
Anti Nim:Nim 游戏,定义为取到最后一棵石子的输,则当且仅当所有石子数为 且 为 或至少一堆石子数大于 且 不为 时先手必胜
如果拓展至不同游戏的组合,则需 定理,必须满足所有 的状态都无后继状态或有一个后继状态
SG 函数
一个局面的 SG 值:它后继状态的 SG 值的
一个游戏的 SG 值:它所有子游戏的 SG 值的异或和
注意区分 ”局面“ 和 ”游戏“
通常可以暴搜一下 SG 值,找找规律或 DP
博弈 DP
这是比较常见的考察形式
一般博弈双方都绝顶聪明,能预见未来,所以 DP 有后效性无前效性,从后往前 DP
CF1628D2 Game on Sum (Hard Version)
注意取实数,设 表示当前还剩 次,其中有 次后手必须选加的 值
那么先手假设选 ,有两种情况:
- 后手选择减,
- 后手选择加,
此时后手一定会选让 小的那个,而先手想让最小值最大
因此先手要让 ,即
边界是
发现决策完全与 无关,可以直接当作 预处理出 的系数,最后答案为
的 DP 可以通过 Easy Version
由于初始只有 有值,考虑每个 对答案的贡献
看作每次 只能走到 或 ,求 走到 的路径数,注意第一次只能走到
所以方案数为
由于每走一步系数除以 ,总答案为
注意特判
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;
}
预处理出 到每个点的距离,然后分别从小到大排序,距离相同的放在一起,则两人当前取的点一定是某个前缀
所以状态就方便表示了:设 表示先手已经选了前 个,后手已选前 个时先手还能获得的最大分数
枚举先手下一步选到 ,后手一定会选 使 最小
因此 ,其中 为后手选到 时先手选 获得的收益
为已选 ,先手下一步能选的最近位置(保证有新点加入),同理 为后手下一步能选的最近位置
可以预处理出 表示后手选到 时先手从 的收益,由于先手选的内部不重复,因此
然后边 DP 边记录后缀最小值 ,更新最大值
最终转移为 ,边界为覆盖了所有点的
这样就能 完成转移,细节有一点多
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;
}
二分图博弈
定义:有一张二分图,起始节点为左部点 ,先后手交替移动,每条边只能经过一次,不能移动的一方输掉游戏
先说结论,当 在图上的每一组最大匹配中时,先手必胜
证明:
若 在每一组最大匹配中,则先手先走一条匹配边到 ,后手从 走到 后 一定有匹配边,否则 所在匹配中把它换成 最大匹配不会变而 不在匹配中,同理之后若后手走到的点无匹配边,则相当于找到了增广路,能调整使得 不在最大匹配中,矛盾
因此先手每次能沿着匹配边走,先手必胜
若存在最大匹配使得 不在其中,则先手走到 后,之后后手每次肯定能走一条匹配边 ,因为如果某次后手走了非匹配边 ,之前先手走了 ,则可以调整,将 所在匹配中它换成 加入最大匹配,使得最大匹配增加,矛盾
因此后手每次能沿着匹配边走,后手必胜
那么如果只对一个点求,可以直接去掉这个点重跑最大匹配
找出所有的非必经点,则从每个非匹配点 ,找到 使得 为非匹配边, 为匹配边,将 连边后遍历新图,被遍历到的都是非必经点
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具