ZR 集训 Day17 - 博弈论
必胜态与必败态
-
先手必胜 = “存在”一个后继局面是后手必败。
-
先手必败 = “对于所有”后继局面,都是后手必胜。
SG 函数
-
对于多个独立的游戏,可以分别计算它们的 SG 函数值,再求 Nim 和(即异或和);
-
对于单个游戏,每个状态的 SG 函数值都是它的所有后继状态的 SG 函数值的 值;
-
特别地,终止状态(即没有后继状态的状态)的 SG 函数值为 。
题单
P17128 [ICPC 2025 Shanghai R] AGI
如果同时存在两个相同的数,那先手如果取走其中一个,后手肯定会把另一个取走。我们考虑找出所有这样的数对,让先手取走数对其中一个数,并对剩下的数进行处理。
如果剩下的数的数量不少于 个,那么先手必败,因为任何能取胜的策略都能被后手破坏。
如果剩下的数的数量为 个,则判断这两个数中有没有数异或上先手目前的数为 。
若数量为 ,则判断先手手上的数是否为 。
代码:
C++
#include
using namespace std;
const int N=2e5+5;
int T,n;
int a[N],tag[N];
void solve(){
fill(tag,tag+(n mp;
int now=0,sum=0,y=0;
scanf("%d",&n);
for(int i=1;i>T;
while(T--) solve();
return 0;
}
`#include
using namespace std;
const int N=2e5+5;
int T,n;
int a[N],tag[N];
void solve(){
fill(tag,tag+(n1)+1,0);
mapint,int> mp;
int now=0,sum=0,y=0;
scanf("%d",&n);
for(int i=1;i(n1);i++){
scanf("%d",a+i);
if(mp[a[i]]!=0) now^=a[i],tag[mp[a[i]]]=1,tag[i]=1,mp[a[i]]=0;
else mp[a[i]]=i;
}
for(int i=1;i(n1);i++){
if(!tag[i]){
sum++;
if((now^a[i])==0) y=1;
}
}
if(!sum){
if(now) printf("Bot\n");
else printf("Menji\n");
}
else if(sum==2) {
if(!y) printf("Bot\n");
else printf("Menji\n");
}
else printf("Bot\n");
}
signed main(){
cin>>T;
while(T--) solve();
return 0;
}`
AT_arc168_b [ARC168B] Arbitrary Nim
不难发现,对于一个堆的 函数应该是 ,
先排除掉那些石子个数相等的两个堆,因为无论怎么取模它们异或的贡献还是 。
如果剩余堆的异或值不为 ,说明我们不需要取模,也就是说 可以取无穷大,则输出 -1。
若没有剩余的堆,那说明先手必败,输出 0。
否则,我们输出剩余堆石子数最大值减 ,这样只有最大值变成了 ,其余值不变,异或值自然不会是 。
代码:
C++
#include
using namespace std;
int n,mx,sum;
set s;
int main(){
cin>>n;
for(int i=1,x;i
`#include
using namespace std;
int n,mx,sum;
setint> s;
int main(){
cin>>n;
for(int i=1,x;in;i++){
scanf("%d",&x);
sum^=x;
if(s.find(x)!=s.end()) s.erase(x);
else s.insert(x);
}
if(sum) cout"-1";
else if(s.empty()) cout"0";
else couts.end()-1;
}
`
CF388C Fox and Card Game
分两种情况讨论。
对于一个偶数堆,先发起取这个堆的肯定是有一个较大的值靠近自己,那对方就会为了先发起的人拿不到这个值而去取另一边的值,所以最终的状态肯定是双方各取两边。
对于一个奇数堆,原理还是一样的,但是先手会多拿到最中间的那个值。
所以对于偶数堆,直接计算答案;对于奇数堆,我们按照堆中间的值从大到小排序,两人交替地取中间的值即可。
代码:
C++
#include
using namespace std;
const int N=1005;
int n,idx,ans1,ans2;
struct node{
int mid,l,r;
bool operatorb.mid;
}
}a[N];
int main(){
cin>>n;
for(int i=1;i>s;
if(s&1){
idx++;
for(int j=1,x;j>x;
if(j>x;
if(j
`#include
using namespace std;
const int N=1005;
int n,idx,ans1,ans2;
struct node{
int mid,l,r;
bool operatorconst node b)const{
return mid>b.mid;
}
}a[N];
int main(){
cin>>n;
for(int i=1;in;i++){
int s;
cin>>s;
if(s&1){
idx++;
for(int j=1,x;js;j++){
cin>>x;
if(js/2) a[idx].l+=x;
else if(j==s/2+1) a[idx].mid=x;
else a[idx].r+=x;
}
}
else{
for(int j=1,x;js;j++){
cin>>x;
if(js/2) ans1+=x;
else ans2+=x;
}
}
}
sort(a+1,a+1+idx);
for(int i=1;iidx;i++){
if(i&1) ans1+=a[i].l+a[i].mid,ans2+=a[i].r;
else ans1+=a[i].l,ans2+=a[i].mid+a[i].r;
}
coutans1" "ans2;
return 0;
}`
CF1704F Colouring Game
不难发现,对于先手 Alice,肯定优先删除 RB,其次才考虑 RW;对于 Bob 也是如此。
所以,问题的关键就在于字符串中的颜色交替段,毕竟把颜色交替段取完后答案就是唯一的了。
因为每次删除 RB,两个颜色数量之差是不会变的,那就能先想到一个特判:如果两个颜色数量不等,一定是数量多的那个人赢。
否则我们来考虑计算颜色交替段的 SG 函数。
枚举颜色交替段的大小 与删除的位置 ,有:
SGi=mexj=1ji{SGj−1⊕︎SGi−j−1}SG_i=\text{mex}_{j=1}^{j
这个计算是 的,显然对于此题不优。
这时我们打个表:
C++
#include
using namespace std;
int sg[1005],tag[1005];
signed main(){
freopen("sg.out","w",stdout);
sg[2]=1;
cout
`#include
using namespace std;
int sg[1005],tag[1005];
signed main(){
freopen("sg.out","w",stdout);
sg[2]=1;
cout"0\n1\n";
for(int i=3;i1005;i++){
memset(tag,0,sizeof tag);
for(int j=1;ji;j++) tag[sg[j-1]^sg[i-j-1]]=1;
for(int j=0;j1005;j++) if(tag[j]==0){
sg[i]=j;
break;
}
coutsg[i]"\n";
}
return 0;
}`
得到 sg.out 如下(为了美观,每 34 个元素换一次行,省略其余项):
C++
0 1 1 2 0 3 1 1 0 3 3 2 2 4 0 5 2 2 3 3 0 1 1 3 0 2 1 1 0 4 5 2 7 4
0 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 2 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
...
`0 1 1 2 0 3 1 1 0 3 3 2 2 4 0 5 2 2 3 3 0 1 1 3 0 2 1 1 0 4 5 2 7 4
0 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 2 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
8 1 1 2 0 3 1 1 0 3 3 2 2 4 4 5 5 9 3 3 0 1 1 3 0 2 1 1 0 4 5 3 7 4
...`
就会惊喜地发现,除了前 68 项以外,后面的项都是以 34 为周期,那就可以只计算前 102 项了;并且值域是 10 以内的,所以求 时也可以把枚举的范围缩小。
代码:
C++
#include
using namespace std;
int T,n;
int sg[105],tag[105];
char s[500005];
void get_sg(){
sg[2]=1;
for(int i=3;i>T;
while(T--){
int sr=0,sb=0,l=1,ans=0;
cin>>n;
for(int i=1;i>s[i];
if(s[i]=='R') sr++;
else sb++;
}
if(sr!=sb){
coutsb?"Alice\n":"Bob\n");
continue;
}
for(int i=2;i
`#include
using namespace std;
int T,n;
int sg[105],tag[105];
char s[500005];
void get_sg(){
sg[2]=1;
for(int i=3;i102;i++){
memset(tag,0,sizeof tag);
for(int j=1;ji;j++) tag[sg[j-1]^sg[i-j-1]]=1;
for(int j=0;j10;j++) if(tag[j]==0){
sg[i]=j;
break;
}
}
}
int SG(int x){
if(x68) return sg[x];
else return sg[(x%34?x%34:34)+68];
}
int main(){
get_sg();
cin>>T;
while(T--){
int sr=0,sb=0,l=1,ans=0;
cin>>n;
for(int i=1;in;i++){
cin>>s[i];
if(s[i]=='R') sr++;
else sb++;
}
if(sr!=sb){
cout(sr>sb?"Alice\n":"Bob\n");
continue;
}
for(int i=2;in;i++){
if(s[i]==s[i-1]) ans^=SG(l),l=1;
else l++;
}
ans^=SG(l);
cout(ans?"Alice\n":"Bob\n");
}
return 0;
}`