題目:硬幣游戲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; }