题意: 给出n个石子, 两个人轮流取, 每个人只能取连续的k个, 不能取即输, 问在最优策略时, 第一个人会赢吗?
思路: 首先很容易想到dfs暴力穷举, 枚举当前这个人取的所有方式, 如果存在一种方式使得对手先手必败, 那么就是必胜态.
枚举的方式很简单: 因为是从now个连续石子中取k个, 那么每次操作后, 会剩下两堆分离的石子堆, 他俩可能是:(0个, now-k个), (1个, now-k-1个), ... , ( (now-k)/2个, now-k-(now-k)/2个)...只要存在一种取法使得下一个人输, 那么此状态就赢; 如果所有取法下一个人都赢, 那么此状态就输. 然而哪怕加上记忆化也要T, 就要用到新算法了.
SG函数的思想:
P态: 这个状态下, 先操作的人会输. N态: 这个状态下, 先操作的人会赢.
考虑这个题: 如果当前剩下的石子个数<k个, 那么这个状态就是P态, 如果剩下石子刚好是[k,2k-1]个,因为这些状态可以转移到P态, 那么[k,2k-1]就是N态. 而因为[2k,3k-1]只能到达N态, 所以这是P态.
此时, 我们定义没有后继状态的点(边界的必败点)为"0级胜态", 相应的, 转移到"0级胜态"的点为"1级胜态", 转移到"1级胜态"的点为"2级胜态"....
每个点如果能同时到达"0级胜态"~"n-1级胜态", 他就是一个"n级胜态", 而如果他能到达"0级胜态", "2级胜态", 却不能到达"1级胜态", 他就只能是"1级胜态". 换句话说, 这个点是几级胜态取决于他第一级不能到达的胜态.
这样, 我们就会发现, 只要这个点是除"0级胜态"外所有的胜态, 他就是必胜态, 反之("0级胜态")就是必败态. 有了这个思想, 我们就可以记忆化递归求解这类问题了:
(当前状态的所有)前导状态是几级胜态→当前状态是几级胜态
实际上在SG中, 几级胜态就是sg值.
这大体就是SG定理(Sprague-Grundy)的思路, 具体(为什么是这样&怎么来的)看这里.
此外, 我们注意到, 这个题每次取完都会让一堆石子分成了两堆(其中1(2)堆可能是0), 相当于游戏的剩余时间是在2堆石子中继续(取k个连续石子)的游戏. 而SG定理告诉我们, 子状态的(按此类取法的)sg值就是所有新分出堆的sg值的异或.(为什么?看那里或这里)
总结以下怎么用: 求出每种取法下, 子状态的sg值, 放入一个集合, 当前状态的sg值就是不在这个集合中的最小的数.
代码:
#include<bits/stdc++.h>
#define fuck(x) std::cout<<"["<<#x<<"->"<<x<<"]"<<endl;
using namespace std;
typedef long long ll;
const int M=2e5+5;
const int inf=1e9+5;
const int mod=1e9+7;
int _,n,k,caz=0;
int SG[55];
int sg(int now) {
if(now<k)
return 0;
if(SG[now]!=-1)
return SG[now];
int vis[55]= {0};
for(int i=0; i<=(now-k)/2; i++) {
vis[ sg(i) ^ sg(now-k-i) ]=1;
}
for(int i=0;; i++) {
if(vis[i]==0)
return SG[now]=i;
}
}
int main() {
scanf("%d",&_);
while(_--) {
memset(SG,-1,sizeof(SG));
scanf("%d%d",&n,&k);
if(sg(n))
printf("Case %d: Winning\n",++caz);
else
printf("Case %d: Losing\n",++caz);
}
return 0;
}