公平组合游戏(Impartial Game)的定义如下:
-
游戏有两个人参与,二者轮流做出决策,双方均知道游戏的完整信息;
-
任意一个游戏者在某一确定状态可以作出的决策集合只与当前的状态有关,而与游戏者无关;
-
游戏中的同一个状态不可能多次抵达,游戏以玩家无法行动为结束,且游戏一定会在有限步后以非平局结束。
博弈图:
如果将每个状态视为一个节点,再从每个状态向它的后继状态连边,我们就可以得到一个博弈状态图。
比如巴什博奕游戏(后面会介绍),我们初始状态设为1堆,3个石头,那么初始状态的博弈图就是:
3->2 3->1 3->0
博弈状态关系:
定义 必胜状态 为 先手必胜的状态,必败状态 为 先手必败的状态。
从博弈图上可知,状态3一个后继状态为0,而0是必败状态(没有石头,拿不了当然输了),那么3就是一个必胜状态。换句话说,如果当前状态可以到达一个必败状态(不用所有方式,只要有一种),那么当前状态就是必胜状态。反之,如果当前状态只能到达必胜状态(一个必败状态都到不了),那么当前状态就是必败状态。
上述的必胜状态和必败状态的关系十分重要!!!
博弈状态关系例题:
假设我们当前处理的值为x0,位置为p,且在p后面还有至少一个x,我们取后面的一个x1。如果x1是必败状态,那么x0就是必胜状态,因为x0可以转移到x1。如果x1是必胜状态,就说明x1可以转移到一个必败状态,且x0也可以转移到该必败状态(因为x0=x1),所以x0是必胜状态。所以只要x0后面仍然有x,那么就是必胜状态。
如果x后面没有x了。因为只有255个数,所以我们可以遍历找,且我们找的时候也是找每个数(可以转移的数)的最后一个位置(该位置至少要在x的后面),因为不是最后的位置一定是必胜状态。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
//#define int long long
typedef pair<int,int> pii;
const int inf=0x3f3f3f3f;
const int N=2e5+10;
const int mod=1e9+7;
#define fi first
#define se second
int a[N*2];
int pos[400],res[400],vis[400];
int dfs(int x){
if(vis[x])
return res[x];
vis[x]=1;
int ans=0;
for(int i=0;i<8;i++){
int t=x^(1<<i);
if(vis[t]&&pos[t]>pos[x]) ans|=!res[t];
else if(pos[t]>pos[x]) ans|=!(dfs(t));
}
res[x]=ans;
return ans;
}
void solve(){
int n,m,op,k;
cin>>n>>m;
for(int i=1;i<=n;i++) {
cin>>a[i];
pos[a[i]]=i;
}
while(m--){
cin>>op>>k;
if(op==1){
a[++n]=k;
pos[k]=n;
}
else {
if(pos[a[k]]>k){
cout<<"Grammy"<<endl;
}
else {
memset(vis,0,sizeof res);
memset(res,0,sizeof vis);
if(dfs(a[k])==0) cout<<"Alice"<<endl;
else cout<<"Grammy"<<endl;
}
}
}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t=1;
//cin>>t;
while(t--){
solve();
}
return 0;
}
巴什博弈:
只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个。最后取光者得胜。
我们用上述的博弈图来解决这个问题。
假设一开始我们有4个物品,每次最多取3个,有关系如下:
4->3,4->2,4->1(第一次操作可转移状态),3->2,3->1,3->0;2->1,2->0;1->0;(第二次操作可转移状态)。容易知道0个物品的时候是必败状态,而1,2,3都能一次转移到0这个必败状态,所以他们都是必胜状态。但是对于4来说,它只能转移到1,2,3状态,也就是说4只能转移到必胜状态,所以4是必败状态。
n个物品,每次最多取m个的时候同样是这样推理的,然后可以发现m+1 l n(n是m+1的倍数)情况输其他情况都赢,n是石子数,m是每次可取石子数。
威佐夫博弈:
每次每个人可以从其中一堆石子当中取走任意数量的石子,或者是从两堆当中同时取走相同数量的石子。无法取石子的人落败,请问,在告知两堆石子数量的情况下,这两个人当中哪一方会获胜?
推理仍然可以用博弈图来推理,但是太繁琐了...
我们令first的x小于second的y(必败状态是对称的,(x,y)是必败状态,那么(y,x)也是),找一些必败状态(0, 0), (1, 2), (3, 5), (4, 7)...
容易发现他们y-x的差是递增且不相等的,公差为1,也就是y【i】-x【i】=i。
证明不相等:
假设有两个必败状态的差值一样,比如(1,2)和(2,3),他们的差值都是1,那么很明显我们可以让(2,3)的两坐标都-1就变成了(1,2),也就是说(2,3)可以到达必败状态,所以他应该是必胜的,所以不可能存在差值相等的两个必败状态。
证明差递增,且公差为1:
发现x【i】=,y【i】=
(
是黄金分割率,
,后面会求)。
有个性质,
+1=
,那么两边同乘i,再移项得
。因为i是整数,这说明i*
的小数部分是和i*
的小数部分是完全一样的,所以
也是成立的。
并且每个必败态的x和y坐标会不重不漏的包括所有正整数(0会出现两次)。
证明不重:
令(a,b)为必败状态(b>a),令一个状态为(a,c)。
c>b时。显然直接让c变成b,就可以到达必败状态,(a,b)。
c<b时。此时c-a<b-a,也就是说在(a,b)状态之前一定有一个差为(c-a)的必败状态,且(a,c)可以同时减去一个数到达差为c-a的那个必败状态,(x,y)(y-x=c-a,x<a,y<c)。
令一个状态为(c,b)。
c>a时。此时b-c<b-a,同样可以在(a,b)前面找到一个必败状态的差为b-c,(x,y)(y-x=b-c,x<c,y<b)。
c<a时。因为c<a,有两种情况:
1.如果有以c为前项的必败状态,那么它的y-x值一定是小于b-a的,更小于b-c,所以这种情况只需要让c不动,b减到该值即可,(c,x)(x>c)。
2.如果是以c作为后项的必败状态,也就是(x,c)(x<c,因为c不在前项就在后项),且(x,c)一定是在(a,b)的前面的,也就是说c-x一定是小于b-a的,同时每个必败状态都是对称的,所以一定存在必败状态(c,x)(x<c),直接让b减到x即可。
证明不漏要用到Beatty定理。
Beatty 定理:
若两个正无理数的倒数之和是1,则任何正整数都可刚好(恰好出现一次)以一种形式表示为不大于其中一个无理数的正整数倍的最大整数(向下取整)。
也就是,且
(n,m是正整数),也就是每个数被其中之一唯一表示。
证明:
假设不成立,那么存在且
。将下取整去掉,等式转化不等式,k<nx<k+1,k<my<k+1(没有等于号因为x,y是无理数).将不等式改变一下,k/n<x<(k+1)/n,都取倒数,因为都是正数,所以不等号取反,即n/k>1/x>n/(k+1),同理可得,m/k>1/y>m/(k+1),将两个不等式相加,得,(n+m)/k>1/x+1/y>(n+m)/(k+1),又1/x+1/y=1,所以不等式又变成(n+m)/k>1>(n+m)/(k+1),再取倒数,同乘(n+m)得,k<n+m<k+1。又因为n+m是整数,所以不等式不成立,即不存在
且
,所以
。
证明无重复:
对于,因为x和y都是大于1的数,当n+1的时候,(n+1)x一定比x多1以上,下取整后也会比nx下取整多1以上,不会有重复,my同理,且nx和my下取整无交集。
证明:
假设不成立,那么存在且
。转换成不等式,nx<k<(n+1)x-1,my<k<(m+1)y-1(因为nx下取整,原式子应该是
,因为k是整数,n
x是无理数,举个例子,1<k<3,那么1.5<k<3.5-1=2.5)。和上面一样的变化,n<k/x<n+1-1/x,m<k/y<m+1-1/y,两不等式相加,得,n+m<k<n+m+1。因为k是整数所以不等式不成立,所以。
介绍完Beatty定理回到本题。
证明不漏(包括所有正整数),并求
:
由α、β两个无理数构成1/α + 1/β = 1,取n为任意正整数且an= ⌊α*n⌋,bn= ⌊β*n⌋(⌊x⌋代表向下取整),序列an和bn就称为Beatty序列。和Beatty序列对比,发现我们的an和bn完全符合Beatty序列,所以an和bn包括了所有正整数。
计算:已知Beatty序列:an= ⌊α*n⌋,bn= ⌊β*n⌋,而bn = an + n = ⌊α*n⌋ + n = ⌊(α+1)*n⌋,于是β = (α+1),代入1/α + 1/β = 1,解得α =(1+√5)/ 2 ≈ 1.618,这个数字就是黄金比例数。
判断结果:
因为x=i*,y=i*
*
,所以只要判断(y-x)*
==x(i*
*
-i*
=i)。
拓展威佐夫博弈:
两堆拿的值需满足∣ x − y ∣ ≤ d ,可以发现x=n,y=x+(d+1)*n,通过Beatty定理推出
Nim游戏:
有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
结论:
当且仅当所有堆的石子个数异或和为0时先手必败,其他时候先手必胜。
证明:
如果当前异或和值为x,比如1010101。我们设y是x的最高位1。因为y是1,就说明一定有某一堆的石子数2进制上y为1,我们设其为z。我们将z的y位以下的2进制数1都取走,我们设这个值是p。那么现在的异或和值为x^p,且x^p的最高位1一定是小于y的,也就是说p>x^p。然后现在我们考虑将p填补石子回z。因为p>x^p,所以我们只需要补回x^p,之后的异或值就为0了。也就是说我们只需要从z堆拿走p-x^p个石子就可以让当前的石子异或为0。
所以我们总有办法让异或值不为0的状态变成异或值为0的状态,但是如果一开始异或值为0,我们不能让它仍然为0。所以说只要一开始的异或值不为0,先手总可以让异或值变成0,然后后手会让异或值不为0,先手再让异或值为0。一直这样最后先手就会拿光石子。如果一开始异或值为0,后手就可以一直让异或值变成0,最后后手胜。
例题:
容易知道先手必胜条件是异或和不为0,且证明的时候是因为每次都可以取走异或值的石子堆,如果先手不能第一次把石子堆异或和变成0,那么到后手的局面就是必胜的(异或和不为0,且每次都可以把石子变成异或和为0)。所以有两种情况先手不能必胜:
情况1:
先手面对的异或和为0的局面,那么无论取哪个堆作为先手的第一次操作堆,先手都是必败的。
情况2:
先手不能取完异或和的石子,也就是假设当前石子异或值为x,那么先手第一次操作石子堆是y,y<x时先手必败。
所以我们考虑枚举每一堆当做第一次操作堆,我们要找的其余堆条件是他们的异或和大于等于当前第一次操作堆的石子数(等于是情况1,大于是情况2)。
我们考虑用dp来找不包括当前枚举堆的其余堆所对应的异或和方案数。
注意点:x^y可能大于x并且也大于y,所以数组范围和for循环遍历范围要特别注意,比如256^255就是511。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
typedef unsigned long long ull;
typedef pair<ll,ll> pii;
const int inf=0x3f3f3f3f;
const int N=2e5+10;
const int mod=1e9+7;
const ll INF=2e9+10;
mt19937_64 rd(23333);
uniform_real_distribution<double> drd(0.000001,0.99999);
void solve(){
int n;
cin>>n;
vector<int> a(n+1);
for(int i=1;i<=n;i++)
cin>>a[i];
int ans=0;
vector<vector<int> > dp(1300,vector<int>(1300));
dp[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
for(int k=0;k<=520;k++){
if(i==j) dp[j][k]=dp[j-1][k];
else dp[j][k]=(dp[j-1][k]+dp[j-1][k^a[j]])%mod;
}
}
for(int j=a[i];j<=520;j++)
ans=(ans+dp[n][j])%mod;
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t=1;
//cin>>t;
while(t--){
solve();
}
return 0;
}
SG函数:
含义:
这个函数的参数是游戏的状态,并且返回值是一个非负整数,当函数值为 0 时,先手必败,否则先手必胜。
mex运算:
mex表示当前为出现的最小值,比如mex{1 2 3 4 }=0,mex{0 1 2 4 5}=3。
sg函数计算:
设当前状态为a,设集合A为a的后继状态集合。那么sg【a】=mex{sg【x1】,sg【x2】....}(x属于集合A),注意一定是由后继状态的sg函数值转移,而不是后继状态的编号。
sg定理:
一个游戏的sg值等于各个子游戏的sg值异或和
用sg函数理解Nim和:
上面证明了Nim和的结论(用结论来证明结论),其实就是利用了sg定理。Nim的n堆石子可以看成n个小游戏(互不影响,比如取了1堆2个石头,并不会影响到2堆和3堆),然后每个小游戏的sg值就是每堆的石子数(举个例子,如果这堆有3个石头,0个石头的sg值为0,1为mex{sg【0】}=1,2为mex{sg【0】,sg【1】}=2,3=mex{sg【0】,sg【1】,sg【2】}),然后把每个小游戏的sg值都异或起来就是Nim游戏的sg值了。
sg函数例题1:
首先可以发现每个质数是互不影响的,所以每个质数都是一个小游戏,这是一层。对于一个质数x,我们令x的倍数的数为1,其余为0,那么我们就可以得到一个01序列。然后我们每次只能对1的连续序列进行操作,这又是一个小游戏,第二层。对于一个连续序列1,我们发现我们可以从中间取,然后又划分层两侧的连续区间,所以这又是一层小游戏,这是第三层。我们从第三层一直递推到第一层,每次利用sg定理即可。
注意x^y是可能大于x也大于y的,数组内层要开的大一点,然后要用快速质因数分解。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
typedef pair<int,int> pii;
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
#define fi first
#define se second
inline int read(){
int res = 0, ch, flag = 0;
ch = getchar();
if(ch==EOF)
exit(0);
if(ch == '-') //判断正负
flag = 1;
else if(ch >= '0' && ch <= '9') //得到完整的数
res = ch - '0';
while((ch = getchar()) >= '0' && ch <= '9' )
res = res * 10 + ch - '0';
return flag ? -res : res;
}
inline void print(__int128 x){
if(x<0){
putchar('-');
x=-x;
}
if(x>9) print(x/10);
putchar(x%10+'0');
}
ll gcd(ll a,ll b){
return b==0?a:gcd(b,a%b);
}
ll q_pow(__int128 a,ll b,ll mo){
ll res=1;
while(b){
if(b&1) res=((__int128)res*a)%mo;
b>>=1;
a=((__int128)a*a)%mo;
}
return res%mo;
}
bool MillerRabin(ll n){
if(n==2) return 1;
if(n<=1||n%2==0) return 0;
ll base[]={2,325,9375,28178,450775,9780504,1795265022};//或前12个素数
ll u=n-1,k=0;
while(u%2==0) u/=2,k++;
for(auto &x:base){
if(x%n==0) continue;//n的倍数不可以用
ll v=q_pow(x,u,n);//x^u
if(v==1||v==n-1) continue;//-1 1 1 1 1,1前面只能是-1
for(int j=1;j<=k;j++){
ll last=v;
v=((__int128)v*v)%n;//x^2u->(x^u)^2,x^4u->((x^u)^2)^2,x^8u->(((x^u)^2)^2)^2
if(v==1){
if(last!=n-1) return 0;
break;
}
}
if(v!=1) return 0;//x^(n-1)%n=1
}
return 1;
}
ll Pollard_Rho(ll n){//找一个n的约数
static mt19937_64 sj(chrono::steady_clock::now().time_since_epoch().count());
uniform_int_distribution<ll> u0(1,n-1);
ll c=u0(sj);
auto f=[&](ll x){
return ((__int128)x*x+c)%n;
};
ll x=0,y=0,s=1;
for(int k=1;;k<<=1,y=x,s=1){
for(int i=1;i<=k;i++){
x=f(x);
s=(__int128)s*abs(x-y)%n;
if(i%127==0){
ll d=gcd(s,n);
if(d>1) return d;
}
}
ll d=gcd(s,n);
if(d>1) return d;
}
return n;
}
vector<ll> factor;//每次用factor要清空
void get_factor(ll n){//25会放入5*5,因子有重复
if(n==1) return;//1不是质数
if(MillerRabin(n)){
factor.push_back(n);//放入质数因子
return;
}
ll x=n;
while(x==n) x=Pollard_Rho(n);//n不是质数,一直找到一个约数
get_factor(x),get_factor(n/x);//把n分解成约数x和n/x
}
map<ll,vector<ll> > pos;
set<ll> s;
ll n,a[N];
ll sg[N];
void init_sg(int n){
sg[0]=0,sg[1]=1;
for(int i=2;i<=n;i++){
vector<bool> vis(n*5);
for(int j=0;j<=i;j++){
for(int k=0;k+j<=i;k++)
vis[sg[j]^sg[k]]=1;
}
for(int j=0;;j++){
if(!vis[j]){
sg[i]=j;
break;
}
}
}
}
void work(int i){
factor.clear();
get_factor(a[i]);
sort(factor.begin(),factor.end());
factor.erase(unique(factor.begin(),factor.end()),factor.end());
for(auto &x:factor){
s.insert(x);
pos[x].push_back(i);
}
}
void solve(){
cin>>n;
init_sg(n);
for(int i=1;i<=n;i++){
cin>>a[i];
work(i);
}
ll ans=0;
for(auto &x:s){
int sum=0,last=-1,tempsg=0;
for(auto &y:pos[x]){
if(last==-1||y==last+1){
sum++;
}
else{
tempsg^=sg[sum];
sum=1;
}
last=y;
}
tempsg^=sum;
ans^=tempsg;
}
cout<<(ans?"First":"Second")<<endl;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t=1;
//cin>>t;
while(t--){
solve();
}
return 0;
}
sg函数例题2:
假设x是棋子,y是空格,初始棋局是x1x2x3yyx4x5yx6(不到20个简单举例一下)。x6右边没有空格了,所以x6动不了。该棋局的后继棋局可以是:x1x2yx3yx4x5yx6,x1yx2x3yx4x5yx6,yx1x2x3yx4x5yx6。从这三个后继状态可以看出:
x2和x3右边有连续的棋子,但他们可以跳到这段连续棋子右边最靠近的空格位置,然后空格跳到当前位置,也就是说棋子和空格可以交换位置。也就是说对于一个连续段棋子,其中的任意一个棋子都可以和这段区间右边最靠近空格交换位置,然后交换完等价于该棋子和该棋子的右连续棋子都右移若干位,然后左边由空格插入。
我们将每一行二进制存储(反着存,向右移变向左移),然后每次我们只需要从左到右去遍历一次,每碰到一个二进制位上的1就让它和左边最近的0交换,就是一种后继状态了。
然后每行跑一次mex{sg后继}求出当前的sg值,最后将每一行的sg值异或起来即可。
注意点:map会超时,可以用unordered-map或者直接用数组。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
typedef unsigned long long ull;
typedef pair<ll,ll> pii;
const int inf=0x3f3f3f3f;
const int N=2e5+10;
const int mod=1e9+7;
const ll INF=2e9+10;
mt19937_64 rd(23333);
uniform_real_distribution<double> drd(0.000001,0.99999);
unordered_map<int,int> sg;
int dfs(int x){
if(sg.find(x)!=sg.end())
return sg[x];
vector<bool> vis(21);
int last0=-1;
for(int i=19;i>=0;i--){
if((x>>i)&1){
if(last0!=-1){
vis[dfs(x^(1<<i)^(1<<last0))]=1;
}
}
else
last0=i;
}
for(int i=0;i<=20;i++)
if(!vis[i])
return sg[x]=i;
}
void solve(){
int n;
cin>>n;
vector<int> a(n+10);
vector<vector<int> > b(n+10,vector<int>(30));
int ans=0;
for(int i=1;i<=n;i++){
cin>>a[i];
int x=0;
for(int j=1;j<=a[i];j++)
cin>>b[i][j],x|=(1<<(b[i][j]-1));
ans^=dfs(x);
}
cout<<(ans==0?"NO":"YES")<<endl;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t=1;
cin>>t;
while(t--){
solve();
}
return 0;
}
Anti-Nim游戏(反Nim游戏):
n堆物品,每堆 ai 个,两个玩家轮流取走任意一堆的任意个物品,但不能不取,取走最后一个物品的人 失败。
结论:
先手必胜当且仅当
- 所有堆的石子数都为1且游戏的SG值为0
- 有些堆的石子数大于1且游戏的SG值不为0
证明:
情况1:
所以石子数都是1,显然sg值为0,即偶数个石子堆的时候先手必胜。
情况2:
只有一堆石子数大于1,其他石子都是1,显然先手可以控制唯一一堆大于1石子数的石子堆是否取完或者留一个石子,这样就能变成情况1,且先手可以控制石子堆奇偶,所以先手必胜。
情况3:
不止一堆石子数大于1。显然从情况3一定可以到情况2,因为每次只能取一堆石子。所以谁先到情况2谁就必胜。
由情况2可知,此时的异或值必然不为0。如果情况3的异或值不为0,那么一定可以拿走一堆的一些石子使得异或值变成0。理由同Nim游戏。所以先手一定可以到情况2,先手必胜。
例题:
板题。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
typedef unsigned long long ull;
typedef pair<ll,ll> pii;
const int inf=0x3f3f3f3f;
const int N=2e5+10;
const int mod=1e9+7;
const ll INF=2e9+10;
mt19937_64 rd(23333);
uniform_real_distribution<double> drd(0.000001,0.99999);
void solve(){
int n;
cin>>n;
vector<int> a(n+1);
int flag=0,sg=0;
for(int i=1;i<=n;i++){
cin>>a[i];
sg^=a[i];
if(a[i]>1)
flag=1;
}
if(flag==0) cout<<(sg?"Brother":"John")<<endl;
else cout<<(sg?"John":"Brother")<<endl;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t=1;
cin>>t;
while(t--){
solve();
}
return 0;
}
Anti-SG游戏:
走完最后一步者输,即决策集合为空者赢(因为无法操作),与sg游戏相反。
结论:
对于任意一个Anti-SG游戏,如果我们规定当局面中所有的单一游戏的SG值为0时,游戏结束,则先手必胜当且仅当:
1,游戏的SG函数不为0且游戏中某个单一游戏的SG函数大于1
2,游戏的SG函数为0且游戏中没有单一游戏的SG函数大于1
Nim-K游戏:
n堆石子轮流取,每次可以任选k(1-k个堆都可以)堆取任意个,无法操作者输,求是否先手必胜。
和Nim游戏的关系:
Nim游戏是k=1的Nim-K游戏,因为异或就相当于把每一位1的个数加起来对2取模.
结论:
将每堆石子数用二进制表示,二进制为1表示对该位有1的贡献。如果所有二进制位的贡献都是(k+1)的倍数,也就是mod (k+1)等于0,那么先手必败,否则先手必胜。
证明:
如果当前局面所有位的贡献都是(k+1)的倍数:
对于一次操作,设我们拿走的最高位1是x,那么x位的贡献必然减1,但小于x位的贡献可能+1也可能-1也可能不变。
如果我们操作k次,那么每次的最高位1都会-1,其他低位贡献可以自选。可以发现这样对于任何一位的最高贡献变化就是k或-k,并不是(k+1)的倍数,所以一定会使异或全0变成不全为0.
果然当前局面所有位不全为0:
假设x是当前不为0的最高位的贡献,那么我们一定可以拿走x次最高位的石子,且对于低位我们可以任意操作(和Nim游戏证明类似),可以发现我们对每一位最多可以操作k次,所以一定可以使所有位的贡献变成(k+1)的倍数。
又因为全拿完的状态就是所有位的异或值为0,所以只要先手不是所有位全为0,他就一定可以让后手的局面一直都是异或值全为0,先手必胜。
例题:
因为题目说了每两个相邻棋子颜色相异且黑白棋子方向固定,又左第一颗为白,所以每两相邻的异色的棋子都是相互独立不影响,这等于分出了k/2个石子堆,每个堆的石子数就是两石子中间空格位置个数。又每次可以操作1-d堆,这就是Nim-K游戏。
Nim-K游戏的先手必胜条件是异或值不为0,显然没有显示必败的条件好求,且总的方案数也好求,所以我们考虑用总的方案数减去先手必败的方案数就是先手必胜的方案数了。
假设用0表示空格,1表示白棋,2表示黑棋。初始棋局可能是00102。可以发现1左边两个0实际上是没有用的,也就是说实际上k/2堆石子的总石子数其实是不确定的,且有用总石子数不同的方案一定是不同的。所以我们可以先枚举总的石子,这是第一层循环。
假设我们枚举到总的石子数为x,那么现在的问题是x个石子数怎么分成k/2堆,且二进制每位贡献都是(d+1)的倍数。
可以发现每位二进制位取的(d+1)的倍数也是不确定的,可能取1倍,可能取2倍...然后总的石子数又是x,容易发现这是一个多重背包,二进制位就是物品,每个物品可以取(d+1)的倍数,只是对于背包容量的x要全部取完不能有空余的方案数。
注意枚举(d+1)倍数的时候不能超过k/2,因为最多只有k/2堆,也不能超过容量x。设我们枚举到(d+1)*y,也就是说我们要从k/2堆里选出(d+1)*y堆来提供该位的贡献,也就是还要乘个组合数。
当我们处理完每个容量下的方案数,最后我们就要把所有容量的方案数都加起来。但是这里要注意,因为实际上我们算的时候并没有算出这些堆的位置。比如3个空,一堆石子,0个有用石子,可能的方案就有120,012。可以发现我们还没有计算每个堆不同位置的方案数。
假设我们算总石子数为x时候的方案数。因为n个空格里有k个位置和x个位置是被石子堆占用了,那么还有n-k-x个位置是可以自由分配的,也就是将n-k-x插入k/2+1个空位,采用隔板法。
因为隔板法的前提条件是给每个小朋友至少分一个,但是这里是至少分0个,所以我们先从每个小朋友借1,然后石子数变成n-k-x+k/2+1=n-x-k/2+1,需要分成k/2+1份,每份至少一个,用隔板法就是c【n-x-k/2】【k/2】,也就是n-x-k/2个空插入k/2个板。
总的方案数就是n个格子里选k个位置的方案数。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
typedef unsigned long long ull;
typedef pair<ll,ll> pii;
const int inf=0x3f3f3f3f;
const int N=2e5+10;
const int mod=1e9+7;
const ll INF=2e9+10;
mt19937_64 rd(23333);
uniform_real_distribution<double> drd(0.000001,0.99999);
int n,k,d,C[10010][210],dp[18][100010];
void solve(){
cin>>n>>k>>d;
C[0][0]=1;
for(int i=1;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=200;j++) C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
dp[0][0]=1;
/*for(int i=0;i<=16;i++){
for(int j=0;j<=n-k;j++){
for(int x=0;(1ll<<i)*x*(d+1)<=n-k&&x*(d+1)<=k/2;x++){
dp[i+1][j+(1ll<<i)*x*(d+1)]=(dp[i+1][j+(1ll<<i)*x*(d+1)]+1ll*dp[i][j]*C[k/2][x*(d+1)])%mod;
}
}
}*/
for(int j=0;j<=n-k;j++){
for(int i=1;i<=17;i++){
for(int x=0;(1ll<<(i-1))*x*(d+1)<=n-k&&x*(d+1)<=k/2;x++){
if(j>=(1ll<<(i-1))*x*(d+1))//对于每次的背包问题,容量j是固定的,比普通背包少了枚举体积
dp[i][j]=(dp[i][j]+dp[i-1][j-(1ll<<(i-1))*x*(d+1)]*C[k/2][x*(d+1)])%mod;
}
}
}
int ans=0;
for(int i=0;i<=n-k;i++)
ans=(ans+1ll*dp[17][i]*C[n-i-k/2][k/2])%mod;
ans=C[n][k]-ans;
ans=(ans+mod)%mod;
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t=1;
//cin>>t;
while(t--){
solve();
}
return 0;
}
斐波那契博弈
有一堆n个石子,2人轮流取,先取者可以取走任意多个,但不能全取完,以后每人取的石子数不能超过上个人的2倍,无法操作者输。
结论:
先手必败当且仅当石子数为斐波那契数。
证明:
例题:
如果n是斐波那契数,那么结果就是n,否则是齐肯多夫定理的最小划分出来的斐波那契数。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
typedef unsigned long long ull;
typedef pair<ll,ll> pii;
const int inf=0x3f3f3f3f;
const int N=2e5+10;
const int mod=1e9+7;
const ll INF=2e9+10;
mt19937_64 rd(23333);
uniform_real_distribution<double> drd(0.000001,0.99999);
int n;
void solve(){
cin>>n;
vector<int> a(1e4);
int p=0;
a[++p]=1,a[++p]=2;
while(a[p]+a[p-1]<=n){
p++;
a[p]=a[p-1]+a[p-2];
}
while(n){
while(a[p]>n) p--;
n-=a[p];
if(!n) cout<<a[p]<<endl;
}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t=1;
//cin>>t;
while(t--){
solve();
}
return 0;
}
对称操作博弈:
硬币问题:
5个硬币围成一圈,每次只能取连续的1-2个(如果一开始是123,那么12连续,23连续,13不连续,把2取掉变成1 3,13还是不连续的),取完获胜,谁赢?
只要后手一直模仿先手的操作,每次都留下对称的部分,那么后手一定胜,就算一开始5并不对称,但可以通过拿1个(先手第一次拿2个,后手拿那两个的对面的一个),或者拿2个(先手第一次拿1个,后手拿那一个的对面的两个),来保证剩下两个硬币不连续(也就是对称的),不让先手一次拿完就赢了。
扩大范围n个硬币,每次只能取连续1-k个,如果k>=n先手胜,如果k==1看n的奇偶,其余只要后手都执行对称操作,后手一定胜。
台阶问题:
现在有3个台阶,从低到高每个台阶都有一些石子,个数分别为{1,2,3},可以从某一台阶挑选任意个石子扔到下一层台阶,扔到地上的台阶不能再操作,不能操作的人输,两个人轮流操作,谁赢?
可以发现,对于先手操作偶数层的情况,后手一定可以对称操作,比如先手从x层扔y个石子到x-1层,那么后手一定可以从x-1层扔y个石子到x-2层,最后后手会将石子扔到地面上。也就是说无论偶数层有多少石子,哪些偶数层有石子,哪些偶数层没有石子,实际上并不会影响结果,因为后手一定可以采取对称操作。
所以只需要考虑奇数层石子数。假如x层有y个石子(x是奇数),那么先手可以取任意个扔到x-1层(x-1是偶数),设扔的个数是z(z<=y)。那么会发现这z个石子到偶数层以后双方又可以采用对称操作,也就是说这z个石子一定会被扔到地上,且扔到地上以后是由后手开始下一轮操作。也就是说实际上问题就转化成每个奇数层阶梯有一些石子,然后两个人轮流从奇数阶梯挑一些石子扔到地上(扔到下一偶数层等价扔到地上),这就变成奇数层上的Nim游戏,所以只需要对奇数层取异或和即可。
例题1:
如果从x阶梯拿走y个石子,那么x+1阶梯就可以多拿y个,且其他阶梯没有变化,等价于从x拿y个石子给x+1。容易发现这是反着的阶梯Nim游戏,我们只需要反着来一遍阶梯Nim即可。
注意点:地面不是0了,是n+1层。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
typedef unsigned long long ull;
typedef pair<ll,ll> pii;
const int inf=0x3f3f3f3f;
const int N=2e5+10;
const int mod=1e9+7;
const ll INF=2e9+10;
mt19937_64 rd(23333);
uniform_real_distribution<double> drd(0.000001,0.99999);
void solve(){
int n;
cin>>n;
vector<int> a(n+10);
for(int i=1;i<=n;i++)
cin>>a[i];
vector<int> d(n+10);
for(int i=1;i<=n;i++)
d[i]=a[i]-a[i-1];
int ans=0;
for(int i=n;i>=1;i-=2)
ans^=d[i];
cout<<(ans==0?"NIE":"TAK")<<endl;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t=1;
cin>>t;
while(t--){
solve();
}
return 0;
}
例题2:
还是高手过招。
假设x是棋子,y是空格,初始棋局是x1x2x3yyx4x5yx6(不到20个简单举例一下)。x6右边没有空格了,所以x6动不了。该棋局的后继棋局可以是:x1x2yx3yx4x5yx6,x1yx2x3yx4x5yx6,yx1x2x3yx4x5yx6。从这三个后继状态可以看出:
x2和x3右边有连续的棋子,但他们可以跳到这段连续棋子右边最靠近的空格位置,然后空格跳到当前位置,也就是说棋子和空格可以交换位置。也就是说对于一个连续段棋子,其中的任意一个棋子都可以和这段区间右边最靠近空格交换位置,然后交换完等价于该棋子和该棋子的右连续棋子都右移若干位,然后左边由空格插入。
可以发现每次都是从两个空格直接的连续棋子拿出任意个,然后放到右边相邻空格中间。
如果我们令两个空格作为一个台阶,他们中间的连续棋子作为石子个数,可以发现就是从左台阶拿任意个石子放到右台阶,然后最右边是地面。发现这是一个阶梯Nim游戏。
注意点:地面可能已经有石子了,得找到第一层台阶再开始。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
typedef unsigned long long ull;
typedef pair<ll,ll> pii;
const int inf=0x3f3f3f3f;
const int N=2e5+10;
const int mod=1e9+7;
const ll INF=2e9+10;
mt19937_64 rd(23333);
uniform_real_distribution<double> drd(0.000001,0.99999);
unordered_map<int,int> sg;
int dfs(int x){
if(sg.find(x)!=sg.end())
return sg[x];
vector<bool> vis(21);
int last0=-1;
for(int i=19;i>=0;i--){
if((x>>i)&1){
if(last0!=-1){
vis[dfs(x^(1<<i)^(1<<last0))]=1;
}//等价于交换位置,棋子位置变0,最近空格位置变1
}
else
last0=i;//记录最近空格,因为棋子只能跳到最近的空格
}//等价于n阶梯只能跳到下一层阶梯n-1
for(int i=0;i<=20;i++)//最右边一个空格,其他都是棋子
if(!vis[i])//这些棋子都可以跳到最右边的空格
return sg[x]=i;
}
void solve(){
int n;
cin>>n;
vector<int> a(n+10);
vector<vector<int> > b(n+10,vector<int>(30));
int ans=0;
for(int i=1;i<=n;i++){
cin>>a[i];
int x=0;
for(int j=1;j<=a[i];j++)
cin>>b[i][j],x|=(1<<(b[i][j]-1));//转化二进制 最低位变成0
//ans^=dfs(x);
}
for(int i=1;i<=n;i++){
vector<int> v(30);
for(auto &x:b[i]){
v[x]=1;
}
vector<int> game;
int sum=0;
for(int j=20;j>=1;j--){//从地面已经有几个棋子开始算
if(v[j]==0){
game.push_back(sum);
sum=0;
}
else
sum++;
}
if(v[1])//可能1有棋子特判一下
game.push_back(sum);
int temp=0;
for(int j=1;j<game.size();j+=2)//0是地面然后j=几就是第几层阶梯,异或奇层
temp^=game[j];
ans^=temp;
}
cout<<(ans==0?"NO":"YES")<<endl;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t=1;
cin>>t;
while(t--){
solve();
}
return 0;
}
图的博弈:
树上删边博弈:
给出一个有根树,游戏者轮流从树上删去一条边,删去一条边后,不与根连通的部分被删去,谁不能操作谁输。
与Nim游戏的关系:
当一颗树退化成一条链的时候,容易发现这就是一个只有一堆石子的Nim游戏。如果有多条链,那么就是一个普通的Nim游戏。但是当两条链连接后又有所不同(不一定两端相连),这里要用到克朗原理。
克朗原理:
对于树上的某一个点,它的子树可以转化成以这个点为根的一条链,这个链的长度就是它各个链里边(一定是先变成链之后的边)的数量的异或和。
sg函数和链边个数的关系:
也就是说对于一个根节点,它和它的子树可以看成一条链,链长就是以根的每个直接儿子及其子树所构成的链里边的数量的异或和(就是一直递归下去,让每个子树最后合成一整条链),但是实际上一个点以及它子树所构成链的边个数就是这个点的sg值。
叶子节点sg值是0,两个节点的链sg值是1,3个节点的链的sg值是2....就是Nim游戏的sg值。设根节点为x,它有3个儿子,s1,s2,s3,那么sg【x】=(sg【s1】+1)^(sg【s2】+1)^(sg【s3】+1)。sg【s】的时候已经是把儿子节点及其子树变成链了,且链里边的个数就是sg【s】,因为根节点x与儿子也有一条边,所以是sg【x】+1才是以x为根的链里边的个数。
一般图的删边博弈:
就是可能有环的树上删边博弈。
对于环的处理:
对于偶数边的环我们可以对称操作,和上面说的硬币一样,只是每次只能删一个边,最后会发现偶数边的环对结果没有影响,所以我们把偶数环缩成一个点。
对于奇数边的环我们仍然对称操作,最后对结果等价多一条边的影响,所以将奇数环缩成一条边加一个点。
处理完环就变成树上博弈了。