暴力sg入门--(牛客暑假多校A-Alice and Bob)
Posted ccsu_yuki.
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了暴力sg入门--(牛客暑假多校A-Alice and Bob)相关的知识,希望对你有一定的参考价值。
博弈,要么败要么胜.在我看来,博弈就是一个状态转移问题,由必胜态向必败态两者之间不断转换.常规博弈,比如bash博弈(两个人取m个物品,最多取n个,先取完胜利)这类,就是找到了必胜必败的规律来直接找到问题答案,一般要么记规律,要么暴力打表找规律......当然,像nim游戏这类求异或和再判断的博弈,也可以将问题化为二进制数来寻找规律.你也许会想:博弈就这?但是,这些都是特殊情况,那么在没有特殊情况的一般博弈里面,我们该如何求呢?为了解决这些,我们引入了sg函数.
必胜点,必败点
在引入sg函数之前,先引入必胜点(N点)和必败点(P点)的概念:
顾名思义,必败点(P)就是前一个选手必定取胜的点,必胜点(N)就是下一个选手将取胜的点,这个点是一个状态,而状态转移的规则:从任何必胜点(N)推,必可以转移到至少一个必败点(P),从必败点(P)转移,只能进入必胜点(N).游戏由必败点终结.
mex
另外需要介绍的一个就是mex(s),mex是最小非负数的意思,比如mex(1,3,4)=0,mex=(0,1)=2.mex里面的s是一个集合,他的意思是找出不包含在集合内的最小非负数.而sg函数中,保存的就是mex(该点的前驱节点的所有sg值),这些值都是由已知的终结点--必败点来进行推,暴力sg就是推出所有可能的结果,从而得到想要的答案.
下面,上题!
链接:https://ac.nowcoder.com/acm/contest/11166/A
来源:牛客网
时间限制:C/C++ 1秒,其他语言2秒
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld
题目描述
Alice and Bob like playing games. There are two piles of stones with numbers n{n}n and m{m}m. Alice and Bob take turns to operate, each operation can take away k(k>0){k}(k>0)k(k>0) stones from one pile and take away s×k(s≥0)s \\times k(s \\geq 0)s×k(s≥0) stones from another pile. Alice plays first. The person who cannot perform the operation loses the game.
Please determine who will win the game if both Alice and Bob play the game optimally.
输入描述:
The first line contains an integer T(1≤T≤104)T(1 \\le T \\le 10^4)T(1≤T≤104) denotes the total number of test cases. Each test case contains two integers n,m(1≤n,m≤5×103)n,m(1 \\le n,m \\leq 5 \\times 10^3)n,m(1≤n,m≤5×103) in a line, indicating the number of two piles of stones.
输出描述:
For each test case, print "Alice" if Alice will win the game, otherwise print "Bob".
示例1
输入
5 2 3 3 5 5 7 7 5 7 7
5 2 3 3 5 5 7 7 5 7 7
输出
Bob Alice Bob Bob Alice
Bob Alice Bob Bob Alice
题目大意是alice和bob玩游戏,有两堆石头,他们轮流取,不能继续操作的就输了,alice先取.规则是每次一堆取k个(k>0),另一堆取k*s个(s>=0).要输入一个n控制几组样例.输出就输出胜者名字.
这题是我打牛客多校遇到的,当时稍微学了一点点博弈,想挑战来着,结果到最后还是没过,sg没学明白(现在也没学明白).
我一开始就像,尝试纯暴力sg,把所有情况都列举了在算出来,按照规则,只从必败点推,可以剪枝优化一下,于是就有了以下代码:
#include<iostream>
using namespace std;
bool sg[5007][5007];
void init(){
int i,j,u,k,q,p;
sg[0][0]=0;
for(i=0;i<=5000;i++)
{
for(j=0;j<=5000;j++)
{
if(sg[i][j]!=0)continue;
for(u=1;u+i<=5000;u++)
{
for(k=0;k*u+j<=5000;k++)
{
sg[u+i][k*u+j]=true;
}
}
for(u=1;u+j<=5000;u++)
{
for(k=0;k*u+i<=5000;k++)
{
sg[k*u+i][u+j]=true;
}
}
}
}
return;
}
int main()
{
int t,x,y;
init();
cin>>t;
while(t--)
{
cin>>x>>y;
if(sg[x][y]==0)cout<<"Bob"<<endl;
else cout<<"Alice"<<endl;
}
return 0;
}
代码很好理解,就是遍历所有点,是必败点的话,就从这里出发,按照题意,一边取k张牌,另一边取k*s张牌,这些点就都是必胜点,如果遍历访问到必胜点就直接continue,因为从必胜点可以推出必胜必败两种情况,但是只要遍历所有必败点,并且按规则找必胜点,一定就可以找完所有的点的sg值.在按题意多次访问,就过了.(这里的必胜必败是相对于Alice来说的,因为一开始(0,0)状态是相对于Alice的必败点,只有必败点才能推出必胜点).
其实这个解法并不是最好的写法,代码中sg数组我本来开的是int类型,超时了,后来改为bool类型,1000ms用了800ms将近900ms,有点卡时间过的意思了.
之前我还有另一段代码,由于考虑了从必胜点转化,以及时间复杂度过高的问题,总之就是写了百行,没有过,然后去找学长请教,最终把它优化了,代码如下:
#include<iostream>
using namespace std;
int sg[5007][5007];
void init(){
for(int i=0;i<=5000;i++){
for(int j=i;j<=5000;j++){
if(sg[i][j]==0){
for(int h=1;i+h<=5000;h++){
for(int k=0;j+h*k<=5000;k++){
int a=h+i;
int b=j+h*k;
if(a>b)swap(a,b);
sg[a][b]=1;
}
}
for(int h=1;j+h<=5000;h++){
for(int k=0;i+h*k<=5000;k++){
int b=h+j;
int a=i+h*k;
if(a>b)swap(a,b);
sg[a][b]=1;
}
}
}
}
}
}
int main()
{
int tt,t,x,y;
init();
cin>>t;
while(t--){
cin>>x>>y;
if(x>y){
tt=x;
x=y;
y=tt;
}
if(sg[x][y]==0)cout<<"Bob"<<endl;
else cout<<"Alice"<<endl;
}
return 0;
}
该段暴力sg时,遍历就不像上一段代码,循环是j必定大于i,这样就导致main函数进行sg值访问时,就要按照左小右大的顺序才能进行访问.其实两段代码性质上都是暴力sg,只是下面的这段进行了剪枝.就可以不用bool类型,虽然使用了时间肯定会进一步缩短.
总结一下,这个题的思路就是从终结点(0,0)出发,寻找所有必败点,找到一个必败点就去逆推出所有必胜点,将所有的情况列举完,这样不论问你哪种状态的胜负情况,都可以直接求出来.
本人是蒟蒻一枚,sg函数也是初学,有什么问题望大佬指正!大家好好刷题!加油!有什么问题一起探讨.
以上是关于暴力sg入门--(牛客暑假多校A-Alice and Bob)的主要内容,如果未能解决你的问题,请参考以下文章
2021牛客暑期多校训练营1 Alice and Bob(结论,sg打表|优化)
2020牛客暑假多校第一场 J题 Easy Integration
牛客2021暑假多校10Train Wreck(出栈顺序,建树,优先队列维护)