必胜点和必败点的概念:
P点:必败点,换而言之,就是谁处于此位置,则在双方操作正确的情况下必败。
N点:必胜点,处于此情况下,双方操作均正确的情况下必胜。
必胜点和必败点的性质:
1、所有终结点是 必败点 P 。(我们以此为基本前提进行推理,换句话说,我们以此为假设)
2、从任何必胜点N 操作,至少有一种方式可以进入必败点 P。
3、无论如何操作,必败点P 都只能进入 必胜点 N。
我们研究必胜点和必败点的目的时间为题进行简化,有助于我们的分析。通常我们分析必胜点和必败点都是以终结点进行逆序分析。
【硬币游戏】
A与B玩游戏,给定k个数字a1…ak,有x枚硬币,A与B轮流取,每次所取的硬币数要在a1…ak中。A先取,取走最后一枚硬币的一方获胜,双方都采取最优策略,问谁获胜。
我们从终点开始逆序推,设win[x]数组表示轮到我取的时候剩下x枚硬币,我能赢与否。
那么win[0]=0;
代码如下:
void solve(){
win[0]=0;
for(int j=1;j<=X;j++){//枚举还剩几枚硬币
win[j]=0;
for(int i=0;i
相关题:HDU - Good Luck in CET-4 Everybody!
【推理】
POJ2484 - A Funny Game(对称性)
硬币排成一个圈,A与B轮流从中取一枚或两枚,不过取两枚的时候,索取的必须是连续的,硬币取走留下空位,相隔空位的硬币视为不连续。A先取,取走最后一枚硬币的一方获胜。
n==1||n==2时,A胜。
其他情况,B胜。
B只要保证A取完,B取完使有硬币分成两个完全相同的状态,那么最后一个取完硬币的一定是B。
POJ1082 - Calendar Game
日期找规律,这个好难找啊qwq
给你一个日期(y-m-d),AB轮流往后数,A先,每次可以往后数1日或者往后数1月,问A能否先数到2001年11月4日。
我们发现的规律:不管往后数1日还是1月,m+d的奇偶性都会发生改变,11+4是奇数,所以开始日期是偶数一定可以赢
若开始日期是奇数,有可能会翻盘哦~
两个特殊日期:9.30->10.1,11.30->12.1,所以开始日期是这两个,我们也可以保证胜
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main(){
int t,y,m,d;
scanf("%d",&t);
while(t--){
scanf("%d%d%d",&y,&m,&d);
if((m+d)%2==0){printf("YES\n");continue;}
if(d==30&&(m==9||m==11)){printf("YES\n");continue;}
printf("NO\n");
}
}
【动态规划】
POJ2068 - Nim
还是取石子,不同的是一共2*n个人,1、3、5…n-1是一拨,2、4、6…n是一拨,按1,2,3,4…这个顺序轮流取,每个人都取1~ai个,谁取完最后一个石子谁输,你先取,问能否胜
dp[i][j]:=轮到第i个人取时还剩j个石子的状态(1胜0败)
dp[i][0]=1,记忆化搜索求出dp[0][s]的值,注意:这里可能两拨人都轮一遍石子还没取完,那么我们从第1个人再开始取,需要求余。(最好从0开始,不用特判)
代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int a[25],dp[25][10000];
int n,s;
int solve(int i,int j){//该第i个人取了,此时剩下了j个石头
if(j==0)return dp[i][j]=1;
if(dp[i][j]!=-1)return dp[i][j];
dp[i][j]=0;
for(int k=1;k<=a[i]&&k<=j;k++){
if(!solve((i+1)%(2*n),j-k))dp[i][j]=1;
}
return dp[i][j];
}
int main(){
while(scanf("%d",&n)==1&&n){
scanf("%d",&s);
memset(dp,-1,sizeof(dp));
for(int i=0;i<2*n;i++){
scanf("%d",&a[i]);
}
printf("%d\n",solve(0,s));
}
}
有n堆物品,每堆有ai个石子,A和B轮流取走至少1个,A先取,取光者胜。问谁胜。
int x=0;
for(int i=0;i
关于XOR运算的一些性质:
0^k=k;k^k=0;x^k^x=k;
若x!=0,a>0,b>0,(a-b)^x=0,那么a>x。(a-b=x)
异或性质的应用: POJ2975
代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int a[1005];
int main(){
int n;
while(scanf("%d",&n)!=EOF){
if(n==0)break;
int x=0;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
x^=a[i];
}
if(!x)printf("0\n");
else{
int tmp,ans=0;
for(int i=1;i<=n;i++){
tmp=x^a[i];
if(a[i]>tmp)ans++;
}
printf("%d\n",ans);
}
}
}
将组合游戏抽象为有向图,每个位置为有向图的一个节点,每种可行操作为有向图的一条路径,我们就在有向图的顶点上定义了SG函数。
首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。对于任意状态 x , 定义 SG(x) = mex(S),其中 S 是 x 后继状态(比如当前石子x=5,规定能取{1,3,4},那么后继状态就是4,2,1)的SG函数值的集合。如 x 有三个后继状态分别为 SG(a),SG(b),SG(c),那么SG(x) = mex{SG(a),SG(b),SG(c)}。 这样 集合S 的终态必然是空集,所以SG函数的终态为 SG(x) = 0,当且仅当 x 为必败点P时。
【例子】取石子问题
有1堆n个的石子,每次只能取{ 1, 3, 4 }个石子,先取完石子者胜利,那么各个数的SG值为多少?
SG[0]=0,f[]={1,3,4},
x=1 时,可以取走1 - f{1}个石子,剩余{0}个,所以 SG[1] = mex{ SG[0] }= mex{0} = 1;
x=2 时,可以取走2 - f{1}个石子,剩余{1}个,所以 SG[2] = mex{ SG[1] }= mex{1} = 0;
x=3 时,可以取走3 - f{1,3}个石子,剩余{2,0}个,所以 SG[3] = mex{SG[2],SG[0]} = mex{0,0} =1;
x=4 时,可以取走4- f{1,3,4}个石子,剩余{3,1,0}个,所以 SG[4] = mex{SG[3],SG[1],SG[0]} = mex{1,1,0} = 2;
x=5 时,可以取走5 - f{1,3,4}个石子,剩余{4,2,1}个,所以SG[5] = mex{SG[4],SG[2],SG[1]} =mex{2,0,1} = 3;
以此类推.....
所有终结点 SG 值为 0(因为它的后继集合是空集) 。
SG 为 0 的顶点,它的所有后继 y 都满足 SG 不为 0,该点为P(必败点)。
对于一个 SG 不为 0 的顶点,必定存在一个后继满足SG 为 0 ,后继点为必败,该点为N(必胜点)。
满足组合游戏性质 所有 SG 为 0 定点对应 P 点,SG大于 0 顶点对应 N 点。
设G1,G2…Gn是n个有向图游戏,
定义游戏G是G1,G2…Gn的和,也就是说总游戏G可以分成子游戏G1,G2…Gn。
Sprague-Grundy Theorem就是:
G的sg函数是所有子游戏的异或。
sg[G]=sg[G1]^sg[G2]^…^sg[Gn]。
【模板 - 硬币游戏2】
给定k个数字a1…ak,有n堆硬币,每堆有xi枚,A和B轮流取,A先,取出的枚数一定在a1…ak当中,取光者胜,问谁胜。
思路:把n堆硬币看成是n个子游戏,总游戏的sg值(也就是最后的结果)等于子游戏sg值的异或
代码如下:
sg[0]=0;
int max_x=*max_element(X,X+n);
for(int j=1;j<=max_x;j++){//max_x代表堆中最多的石子数,求它的sg值顺带其他堆的都求出来
int vis[N]={0};
for(int i=0;i
【SG应用 - 一维数组】
POJ3537
给你1*n的网格,A和B轮流在网格内画X,谁先连到三个X谁胜,先手胜输出1,反之输出2.
思路:这个游戏一看就是可以分解成子游戏的,怎样分解是我们要考虑的。
当该A画X时,是X_X或是_XX_这样个状态,那么A肯定就胜了,但B又不傻,肯定会尽力阻止这两种情况发生的。
例如当A画了个X, 1 2 X 3 4
若B在1、4位置上画X,那么会造成X_X这种情况,在2、3上画,会造成_XX_,那么A肯定胜了
所以B不能再1234上画,反之A也是
(即当对方画了一个X,我们不要在X的1234位置画,不然我们必败,除非无处可画)
那么长度为L的问题就分成了两个长度为posX-3和L-posX-2的问题。
疑惑:道理是明白,当时我就在疑惑怎么求sg[n],难道还像上题那样求出所有子问题的sg值后,再一起异或?答案是否定的啦,因为从不同的位置画X我们得到的后继状态是不同的,每个后继状态又对应两个子问题,一共能得到的不同的子问题灰常之多,这时候就要用到深搜了,每次对应长度n,我们有:在位置1…n画X这么多后继状态,对于每一个后继状态,分成2个子问题,(画X后分成posX-3和n-posX-2)。后继状态的sg值等于子问题sg值的异或,求出后用vis数组标记,再像模板上那样,把每个长度为n的问题的sg值求出,保存到sg数组里。(备忘录方法~)
代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int sg[2005];
int solve(int n){
if(n<0)return 0;
if(sg[n]!=-1)return sg[n];
bool g[2001]={0};
for(int i=1;i<=n;i++){
int t=solve(i-3)^solve(n-i-2);
g[t]=1;
}
for(int i=0;;i++){
if(g[i]==0)return sg[n]=i;
}
}
int main(){
int n;
memset(sg,-1,sizeof(sg));
scanf("%d",&n);
if(solve(n))printf("1\n");
else printf("2\n");
}
【SG应用 - 二维数组】
POJ2311
这题和上题非常像啦:给一个w*h的格子的长方形纸张,两人轮流剪纸,(沿着格子的分界线,水平或竖直地剪),每次都选择切得的某一张纸再进行切割,首先切出1*1的获胜,问先手必胜还是必败。
思路:
切割纸张时一旦切割出了长或宽为1的纸那么必败,所以我们切割时控制在长宽至少为2,当然2*3,2*2,3*2这些状态都是必败态,再切一刀,长或宽就出现1了。
代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int sg[205][205];
int solve(int w,int h){
if(sg[w][h]!=-1)return sg[w][h];
if(w==2&&(h==2||h==3))return sg[w][h]=0;
if(h==2&&w==3)return sg[w][h]=0;
int vis[205]={0};
for(int i=2;w-i>=2;i++){
int t=solve(i,h)^solve(w-i,h);
vis[t]=1;
}
for(int i=2;h-i>=2;i++){
int t=solve(w,i)^solve(w,h-i);
vis[t]=1;
}
for(int i=0;;i++){
if(!vis[i])return sg[w][h]=i;
}
}
int main(){
int w,h;
memset(sg,-1,sizeof(sg));
while(scanf("%d%d",&w,&h)!=EOF){
int res=solve(w,h);
if(res)printf("WIN\n");
else printf("LOSE\n");
}
}
大概这就是sg的套路吧~
按照“不败”的思想把原问题分成若干后继状态,一个后继状态分成2个子问题,用dfs求子问题的sg值,异或得到后继状态的sg值,标记sg值,然后求出最小的不属于S的非负整数。(S是后继状态sg值的集合)。
学习链接~
SG函数定理
ACM博弈模板
各种博弈