题目:硬币游戏1,Alice和Bob在玩这样一个游戏。给定k个数字a1,a2,···ak。 一开始,有x枚硬币,Alice和Bob轮流取硬币。每次所取硬币的枚数
一定要在a1,a2···,ak当中。Alice先取,取走最后一枚硬币的一方获胜。当双方都采取最有策略时,谁会获胜?假定a1a2···ak中一定有1
限制条件:1<=x<=10000 1<=k<=100 1<=ai<=k
样例:
输入
x=9
k=2
a={1,4}
输出
Alice
样例2
x=10
k=2
a={1,4}
输出
Bob
下面考虑轮到自己的时,还有j枚硬币的情况
1、题目规定取光所有硬币就获胜,这等价于轮到自己时如果没有了硬币就失败了。因此,j=0时是必败态
2、如果对于某个i(1<=i<=k),j-ai是必败态的话,j就是必胜态。(如果当前有j枚硬币,只要取走ai枚,对手就必败->自己必胜)
3、如果对于任意的i(1<=i<=k),j-ai都是必胜态的话,j就是必败态(不论怎么取,对手都必胜->自己必败)
根据上面这些规则,我们利用动态规划算法按照j从小到大的顺序计算必胜态必败态。只要看x是必胜态还是必败态,我们就知道谁会获胜了
像这样,通过考虑各个状态的胜负条件,判断必胜态和必败态,是有胜败的游戏的基础
看代码
#include<iostream> #include<stdio.h> #include<string.h> #include<cmath> #include<math.h> #include<algorithm> #include<set> #include<queue> typedef long long ll; using namespace std; const ll mod=1e9+7; #define INF 0x3f3f3f int main() { bool win[10005]; int x,k; int a[110]; cin>>x>>k; for(int i=0;i<k;i++) cin>>a[i]; win[0]=false;//0枚硬币必败 for(int i=1;i<=x;i++) { win[i]=false;//先初始化为为必败态 for(int j=0;j<k;j++) { win[i]|=a[j]<=i&&!win[i-a[j]];//异或运算,有一个为必败态则为必胜态 } } if(win[x]) cout<<"Alice"<<endl; else cout<<"Bob"<<endl; return 0; }
2、A Funny Game
题目:n枚硬币排成一个圈。Alice和Bob轮流从中取一枚或两枚硬币。不过,取两枚时,所取的两枚硬币必须是连续的。硬币取走之后留下空位
,相隔空位的硬币视为不连续。Alice开始先取,取走最后一枚硬币的一方获胜。当双方都采取最优策略时,谁会获胜
0<=n<=1000000
输入
n=1
输出
Alice
输入
n=3
输出
Bob
n高达1000000,考虑到还有将连续部分分裂成几段等的情况,状态数非常的多,搜索和动态规划都难以胜任。需要更加巧妙地判断胜败关系
首先,试想一下如下情况。能够把所有硬币分解成两个完全相同的状态,是必败态还是必胜态呢?
事实上,是必败态。不论自己采取何种策略,对手是要在另一组采取相同的策略,就又回到了分成两个相同的组的状态了
不断循环下去,总会轮到自己没有硬币了。也就是说,因为对手取走了最后一枚硬币而败北
接下来,回到正题。Alice在第一步取走了一枚或者两枚硬币之后,原本成圈的硬币变成了长度为n-1或者n-2的链。这样只要Bob在中间位子
根据链长的奇偶性,取走一枚或者两枚硬币,就可以把所有硬币正好分为两个长度相同的链
这也正如我们前面说的必败态。也就是说Alice必败,Bob必胜,只不过当n<=2时,Alice可以在第一次取光,所以胜利的是Alice。在这类游戏中
做出对称的状态再完全模仿对手的策略常常是有效的
看代码
#include<iostream> #include<stdio.h> #include<string.h> #include<cmath> #include<math.h> #include<algorithm> #include<set> #include<queue> typedef long long ll; using namespace std; const ll mod=1e9+7; #define INF 0x3f3f3f int main() { int n; cin>>n; if(n<=2) cout<<"Alice"<<endl; else cout<<"Bob"<<endl; return 0; }
3、Euclid's Game
让我们看一下这个以辗转相除法为基础的游戏
给定两个整数a,b。Stan和Ollie轮流从较大的数字中减去较小数字的倍数。这里的倍数是指1倍,2倍这样的正整数倍,并且相减后的结果不能
小于零。Stan先手,在自己的回合将其中一个数变为0的一方获胜。当双方都采取最优策略时,谁会获胜?
输入
a=64,b=12
输出
Stan wins
输入
a=15,b=24
输出
Ollie wins
让我们来找找看该问题中必胜态和必败态。首先,如果a>b则交换,假设a<b。另外,如果b已经是a的倍数了则必胜,所以假设b并非a的倍数
此时,a和b的关系按照自由度的观点。可以分为以下两类
b-a<a的情况
b-a>a的情况
对于第一种情况,只能从b中减去a,没有选择的余地。相对的,对于第二种情况,有从b中减去a,减去2a,或者更高的倍数的情况
对于第一种情况,要判断必胜还是必败并不难。因为没有选择的余地,如果b-a之后所得状态是必败态的话,他就是必胜态,如果得到的是必胜态的话,它就是必败态
例如,从(4,7)这个状态出发就完全没有选择的余地,按照
(4,7)->(4,3)->(1,3)的顺序,轮到(1,3)的一方将获胜
所以有必胜->必败->必胜 可见(4,7)是必胜态
接下来,我们来看第二种情况是必胜态还是必败态。假设x是使得b-ax<a的整数,考虑一下,从b中减去a(x-1)的情况。例如对于(4,19)则减去12
此时,接下来的状态成了前边讲过的没有选择的情况,如果改状态是必败的话,则当前状态就是必胜态。
那么,如果减去a(x-1)后的状态是必胜态的话,该如何是好? 此时,从b中减去ax后的状态就是减去a(x-1)后的状态唯一可以转移到的状态,根据假设,减去a(x-1)是必胜态,所以该状态是必败态。因此是必胜态
由此可知,对于第二种情况,总是必胜的。所以,从初始状态开始,最先到达有自由度的第二种状态的一方必胜
看代码
#include<iostream> #include<stdio.h> #include<string.h> #include<cmath> #include<math.h> #include<algorithm> #include<set> #include<queue> typedef long long ll; using namespace std; const ll mod=1e9+7; #define INF 0x3f3f3f int a,b; void solve(int a,int b) { bool f=true; while(1) { if(a>b) swap(a,b); if(b%a==0) break; if(b-a>a) break; b-=a; f=!f; } if(f) cout<<"Stan wins"<<endl; else cout<<"Ollie wins"<<endl; } int main() { cin>>a>>b; solve(a,b); return 0; }
4、Nim游戏
算法树上有点简略,没有看懂,参考资料:https://baike.baidu.com/item/Nim游戏/6737105?fr=aladdin
题目大意:有n堆石子,每堆石子有a[i]个。Alice和Bob轮流从非空的石子中取走至少一颗石子。Alice先取,取光所有石子的一方获胜。当双方都采取最优策略时
,谁会获胜?
限制条件:1<=n<=1000000 1<=ai<=10^9
样例:
输入
n=3
a={1,2,4}
输出:
Alice
让我们来看看这个游戏,该游戏的策略也成为了许多游戏的基础。要判断该游戏的胜负只要用异或运算就好了。有以下结论:
a1^a2^...^an!=0 必胜态
a1^a2^...^an==0 必败态
因此,只要计算异或值就知道谁胜了
分析一下为什么是这样的:
有三种情况:
1、无法进行移动 那么它就是必败态
2、可以移动到必败态 那么它就是必胜态
3、所有的移动都导致必胜态 那么它就是必败态
第一个命题显然,无法进行移动只有一个,就是全0,异或仍然是0。
第二个命题,对于某个局面(a1,a2,...,an),若a1^a2^...^an<>0,一定存在某个合法的移动,将ai改变成ai'后满足a1^a2^...^ai'^...^an=0。不妨设a1^a2^...^an=k,则一定存在某个ai,它的二进制表示在k的最高位上是1(否则k的最高位那个1是怎么得到的)。这时ai^k<ai一定成立。则我们可以将ai改变成ai'=ai^k,此时a1^a2^...^ai'^...^an=a1^a2^...^an^k=0。
第三个命题,对于某个局面(a1,a2,...,an),若a1^a2^...^an=0,一定不存在某个合法的移动,将ai改变成ai'后满足a1^a2^...^ai'^...^an=0。因为异或运算满足消去率,由a1^a2^...^an=a1^a2^...^ai'^...^an可以得到ai=ai'。所以将ai改变成ai'不是一个合法的移动。证毕。
看代码:
#include<iostream> #include<cstdio> #include<cstring> #include<stdio.h> #include<string.h> #include<cmath> #include<math.h> #include<algorithm> #include<set> #include<queue> typedef long long ll; using namespace std; const ll mod=1e9+7; const int maxn=1e6+10; const ll maxa=32050; #define INF 0x3f3f3f3f3f3f ll n; ll a[maxn]; void solve() { ll x=0; for(int i=0;i<n;i++) x^=a[i]; if(x!=0) cout<<"Alice"<<endl;//总存在一个变化使得x==0,使得它为必败态,所以它本身是必胜态 else cout<<"Bob"<<endl; } int main() { cin>>n; for(int i=0;i<n;i++) cin>>a[i]; solve(); return 0; }
题目大意:排成直线的格子上放有n个棋子。棋子i放在左数第a[i]个格子上。Georgia和Bob轮流选择一个棋子向左移动。每次可以移动一个或者任意多格】
但是不允许反超其他的棋子,也不允许将两个棋子放在同一个格子内。无法进行移动的失败。假设Georgia先移动,当双方都采取最优策略时,谁会获胜?
限制条件:1<=n<=1000 1<=a[i]<=10000
输入
3
1 2 3
输出
Bob
输入
8
1 5 6 7 9 12 14 17
输出
Georgia
思路:如果将棋子两两成对当做整体进行考虑,我们就可以把这个游戏转化为Nim游戏了。先按棋子个数分奇偶情况讨论。
我们可以将每对棋子看作Nim中的一堆石子。石子堆中石子的个数等于两个棋子的间隔、
让我们看一下为什么可以这样转化。考虑其中的某一对石子,将右边的棋子向左移动就相当于从Nim的石子堆中取走石子,另一方面,将左边的石子向左移动,就相当于增加石子。这就与Nim游戏不同了。但是,即便对手增加了石子数量,只要将所加部分减回去就回到原来的状态了。所以这个游戏的胜负和Nim游戏胜负是一样的
看代码
#include<iostream> #include<cstdio> #include<cstring> #include<stdio.h> #include<string.h> #include<cmath> #include<math.h> #include<algorithm> #include<set> #include<queue> #include<map> typedef long long ll; using namespace std; const ll mod=1e9+7; const int maxn=2e5+10; const ll maxa=32050; #define INF 0x3f3f3f3f3f3f int main() { int n,x=0; int a[1050]; cin>>n; for(int i=0;i<n;i++) { cin>>a[i]; } if(n%2==1) a[n++]=0; sort(a,a+n); for(int i=1;i<n;i++) { x^=(a[i]-a[i-1]-1); } if(x==0) cout<<"Bob"<<endl; else cout<<"Georgia"<<endl; return 0; }
5、硬币游戏2
Alice和Bob在玩这样一个游戏。给定k个数字q1,a1···ak。一开始,有n堆硬币,每堆各有xi枚硬币。Alice和Bob轮流选出其中一堆硬币,从中取出硬币。每次所取i硬币的枚数一定要在a1,a2
···ak当中。Alice先取,取光硬币的一方获胜。当双方都采取最优策略时谁会获胜?保证a1,a2···ak一定有个1
限制条件:1<=n<=1000000 1<=k<=100 1<=xi,ai<=10000
输入
n=3
k=3
a={1,3,4}
x={5,6,7}
输出
Alice
这和我们之前介绍的硬币问题1相似,只不过那道题中只有一堆硬币,而本题有n堆。如果依然用动态规划的话,状态数高达O(x1x2···xn)
在此,为了高效的求解该问题,引出Grundy值这一重要概念。利用它,不光这个游戏,其他许多游戏都可以转化成前面所介绍的Nim
让我们再来考虑一下只有一堆硬币的情况,qy硬币枚数x所对应的Grundy值的计算方法如下。
int grundy(x)
{
集合S={}
for(j=1...k)
if(a[j]<=x]) 将grundy(x-a[j])加入到S中
return 最小的不属于S的非负整数
}
也就是说这样的Grundy值就是除了自己走任意一步所能到达的状态的Grundy值以外的最小非负整数。这样的Grundy值,和Nim游戏中的一个石子堆类似,有如下性质
Nim中有x颗石子的石子堆,能够转移成有0,1,···x-1颗石子的石子堆
从Grundy值为x的状态出发,能够转移到Grundy值为0,1,x-1的状态
只不过,与Nim不同的是,转移后的Grundy值也有可能增加。不过,对手总能够选取合适的策略回到相同的Grundy值的状态。,所以对胜负没有影响。
了解了一堆硬币Grundy值的计算方法之后,就可以将它看作Nim中的一个石子堆。
下面用的动态规划的方法,复杂度为O(xk)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
const int maxn=1e6+10;
const int maxk=100+10;
const int maxx=1e4+10;
const ll maxa=43200;
#define INF 0x3f3f3f3f3f3f
int n,k,a[maxn],b[maxk];
int grundy[maxx];
void solve()
{
//轮到自己时剩下0枚必败
grundy[0]=0;
int max_a=*max_element(a,a+n);//求取最大元素
for(int i=1;i<=max_a;i++)
{
set<int> s;
for(int j=0;j<k;j++)
{
if(b[j]<=i)
s.insert(grundy[i-b[j]]);
}
int g=0;
while(s.count(g)!=0)//count用来判断g出现的次数,在这里只有0和1之分
g++;
grundy[i]=g;
}
//判断胜负
int x=0;
for(int i=0;i<n;i++)
x^=grundy[a[i]];
if(x!=0)
cout<<"Alice"<<endl;
else
cout<<"Bob"<<endl;
}
int main()
{
cin>>n>>k;
for(int i=0;i<n;i++)
cin>>a[i];
for(int i=0;i<k;i++)
cin>>b[i];
solve();
return 0;
}
6、两个人在玩如下游戏
准备一张分成w*h的格子的长方形纸张,两人轮流切割纸张。要沿着格子的边界切割,水平或者垂直的将纸张切成两部分。切割了n次之后就得到了n+1张纸,每次选择切得的某一张
再进行切割。首先切出只有一个格子的纸张的一方获胜。当双方都采取最优策略时,先手必胜还是必败
限制条件
2<=w,h<=200
这道题也能用Grundy值来计算。当w*h的纸张分成两张时,假设所分得的纸张的Grundy值分别为g1,g2,则这两张纸对应的状态的Grundy值可以表示为g1^g2
看代码
#include<iostream> #include<cstdio> #include<cstring> #include<stdio.h> #include<string.h> #include<cmath> #include<math.h> #include<algorithm> #include<set> #include<queue> #include<map> typedef long long ll; using namespace std; const ll mod=1e9+7; const int maxn=1e6+10; const int maxk=100+10; const int maxx=1e4+10; const ll maxa=43200; #define INF 0x3f3f3f3f3f3f int a[210][210]; int mem[210][210]; int grundy(int w,int h) { if(mem[w][h]!=-1) return mem[w][h]; set<int> s; for(int i=2;w-i>=2;i++) s.insert(grundy(i,h)^grundy(w-i,h)); for(int i=2;h-i>=2;i++) s.insert(grundy(w,i)^grundy(w,h-i)); int g=0; while(s.count(g)!=0) g++; return mem[w][h]=g; } void solve(int w,int h) { if(grundy(w,h)!=0) cout<<"WIN"<<endl; else cout<<"LOSE"<<endl; } int main() { int w,h; memset(mem,-1,sizeof(mem)); cin>>w>>h; solve(w,h); return 0; }