fibgame 解题报告

题外话

赛时蒟蒻 SG 函数推导 2h 未果,看来我博弈论还得练。

题目描述

定义 tree(x)x - fib 树,它的递归定义如下:

  1. x=0,则 tree(0) 为空树;
  2. x=1,则 tree(1) 为单独一个节点;
  3. x>1,则 tree(x) 是一个根节点连接两个子树,分别为 tree(x1)tree(x2)
    小 Z 和小 W 在树上玩游戏,小 Z 先手。每次玩家可以选择一个节点 x ,然后将以 x 为根的子树全删去。规定取到根节点的人输。
    下面给出了 tree(1)tree(6) 的图例,其中红点表示小 Z 第一步的必胜点。
    image

现在小 Z 想让你帮他数一数:tree(n) 中有多少个节点是他第一步的必胜点?

对于 100% 的数据,1n10000

解题思路

显然这道题类似「公平组合游戏」。因此根据套路先考虑求 SG 函数。

策略分析

「公平组合游戏」的定义如下:

  • 游戏有两个人参与,二者轮流做出决策,双方均知道游戏的完整信息;
  • 任意一个游戏者在某一确定状态可以作出的决策集合只与当前的状态有关,而与游戏者无关;
  • 游戏中的同一个状态不可能多次抵达,游戏以玩家无法行动为结束,且游戏一定会在有限步后以非平局结束。

在这道题中,「玩家无法行动」指的是「整棵树只剩下了根节点」。

SG 函数

定义 SG(i) 表示局面为 tree(i) 时的 SG 函数值。显然 SG(1)=0,SG(2)=1。注意到可以在任何时刻将子树删光,但根不能删(否则会输),所以 SG(u)SG(u1)SG(u2) 。以下简称「形状为 x 的子树」为子树 x
考虑定义 f(i) 表示 SG(u) 从儿子 i(i{u1,u2}) 转移时的值。如果删除全部子树 i,则应当转移 0;若 SG(i)=0,则根据已知结论 f(i)=f(1)=SG(2)=1;若 SG(i)=1,则说明 f(i) 从两个地方转移:

  • 全部删除子树 i
  • 删除 i 的子树 p

因为 i 的内部只能有 SG(p)=0,所以根据上一种情况,f(p)=1,那么 f(i)=mex{0,1}=2。以此类推,若 SG(i)=x,则根据归纳,有 f(i)=x+1

因此,设节点 u(u3),则有 SG(u)=(SG(u1)+1)(SG(u2)+1)

点数计算

胜利点的数量计算有多种方式。这里采用 std 的实现。
dpi,j 表示满足「形状为 tree(i) 的树中,删除子树 uSG(i)=j」的 u 的数量。因为删除必胜点后 SG(n)=0(即对手必败),所以答案为 dpn,0
因为 u 可以在左子树,也可以在右子树,所以 dpi,j 的转移方程应当形如 di1,x+di2,y。因为两侧子树的 f 异或值为 j,有 (x+1)f(i2)=(y+1)f(i1)=j,即 x=(jf(i2))1y=(jf(i1))1
综上所述,dpi,j=dpi1,(j(SG(i2)+1))1+dpi2,(j(SG(i1)+1))1

打表发现 SG(x)8190。设 m=maxx=1n{SG(x)},则时间复杂度为 O(nm) 可以通过这道题。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int mod = 998244353;
int n, sg[10010], dp[10010][10010];
bitset<10010> vis[10010];
int pd(int i, int j) {
if (vis[i][j]) return dp[i][j];
if (i < 2) return 0;
if (i == 2 && j != 0) return 0;
vis[i][j] = 1;
return dp[i][j] = (((j == sg[i - 2] + 1) ? 1 : pd(i - 1, (j ^ (sg[i - 2] + 1)) - 1)) + ((j == sg[i - 1] + 1) ? 1 : pd(i - 2, (j ^ (sg[i - 1] + 1)) - 1))) % mod;
}
int main() {
#ifndef CWKAPN
freopen("fibgame.in", "r", stdin);
freopen("fibgame.out", "w", stdout);
#endif
scanf("%d", &n);
if (n == 1) {
printf("0\n");
return 0;
}
if (n == 2) {
printf("1\n");
return 0;
}
sg[1] = 0, sg[2] = 1;
for (int i = 3; i <= n; i++) sg[i] = (sg[i - 1] + 1) ^ (sg[i - 2] + 1);
vis[2][0] = 1; dp[2][0] = 1;
printf("%d\n", pd(n, 0));
return 0;
}
posted @   cwkapn  阅读(42)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示