基本原理
那就是若有多個人進行博弈,假設他們都足夠聰明(能力已經相當於計算機了),在他們都沒有失誤並採取最優策略後,一定有一個人勝出,在知道初狀態及規則的情況下,求解最終必勝的初狀態(即何人勝出)的一類問題的理論及方法。
基本知識
1.異或運算符( ^ ),異或是一種對於兩個數的二進制數進行運算的邏輯運算符
同真同假爲假,一真一假爲真,即1^1=0,0^0=0,1^0=1。
2.定義兩個狀態,分別爲N和P:
N代表Next-position,可以理解爲先手必勝狀態;
P代表Previous-position,可以理解爲後手必勝狀態(或爲先手必敗)。
3.N/P兩個狀態的關係:
(1)無法進行局面轉移的狀態爲P;
(2)只要有一種轉移方式能將局面變爲P,則當前狀態爲N;
(3)任何轉移方式都只能使局面變爲N,則當前狀態爲P。
4. 黃金比例 φ=(sqrt(5)+1)/ 2
奇異局勢(必敗局勢)
1.任何自然數都包含在一個且僅有一個奇異局勢中。
2.任意操作都可將奇異局勢變爲非奇異局勢。
3.採用適當的方法,可以將非奇異局勢變爲奇異局勢。
BashGame:同餘理論
-
巴什博奕:一堆n個物品,兩人輪流取,每次取1至m個,最後取完者勝
比如10個物品,每次只能取1到5個,則先手方必贏 1.面對[1...5]個局面,必勝 2.面對多餘5+1個局面,必輸 3.如果可以使對手面臨必輸局面,那麼是必贏局面 4.如果不能使對手面臨必輸局面,那麼是必輸局面 基礎: 1, 2, ..., m是必贏局面, m+1是必輸局面 遞推:m+2,m+3, ...,2m+1是必贏局面,2m+2是必輸局面 ... k(m+1)是必輸局面,應該允許k=0,因爲0顯然也是必輸局面 在必輸局和必贏局中,贏的一方的策略是:拿掉部分物品,使對方面臨k(m+1)的局面 例如上例中10個物品,只能拿1到5個,先手方拿4個即可,對手無論拿多少個,你下次總能拿完 如果物品數量隨機,那麼先手一方勝利的概率m/(m+1),後手方勝利的概率是1/(m+1)
-
模板
void BashGame(int n,int m){
if(n%(m+1)==0) {
System.out.println("敗");
}else {
System.out.println("勝");
}
- 例題:HDU2188
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int cc=sc.nextInt();
for(int c=0;c<cc;c++) {
int n=sc.nextInt();//目標值,需超過
int m=sc.nextInt();//每次最大捐款數
if(n%(m+1)==0) {
System.out.println("Rabbit");
}else {
System.out.println("Grass");
}
}
}
}
NimGame:異或理論
-
尼姆博弈: m堆n個物品,兩人輪流取,每次取某堆中不少於1個,最後取完者勝
有三堆各若干個物品,兩個人輪流從某一堆取任意多的物品,規定每次至少取一個,多者不限,最後取光者得勝。 這種情況最有意思,它與二進制有密切關係,我們用(a,b,c)表示某種局勢 首先(0,0,0)顯然是奇異局勢,無論誰面對奇異局勢,都必然失敗。 第二種奇異局勢是(0,n,n),只要與對手拿走一樣多的物品,最後都將導致(0,0,0)。 仔細分析一下,(1,2,3)也是奇異局勢,無論自己如何拿,接下來對手都可以將其變爲(0,n,n)的情形。 計算機算法裏面有一種叫做按位模2加,也叫做異或的運算,我們用符號⊕表示這種運算,先看(1,2,3)的按位模2加的結果: 1 =二進制01 2 =二進制10 3 =二進制11 ⊕ ——————— 0 =二進制00 (注意不進位) 對於奇異局勢(0,n,n)也一樣,結果也是0。 任何奇異局勢(a,b,c)都有a⊕b⊕c =0。 注意到異或運算的交換律和結合律,及a⊕a=0,: a⊕b⊕(a⊕b)=(a⊕a)⊕(b⊕b)=0⊕0=0。 所以從一個非奇異局勢向一個奇異局勢轉換的方式可以是: 1)使 a = c⊕b 2)使 b = a⊕c 3)使 c = a⊕b
-
結論:
對於nim遊戲的某個局面(a1,a2,…,an),當且僅當a1^ a2 ^ …^ an = 0,當前位於必敗點,當前局面稱爲奇異局面,所有非奇異局面都能轉化爲奇異局面。
所有非偏博弈都能轉化爲Nim博弈。 -
證明
(1)無法移動的局面爲所有石堆的石子數都爲0,0 ^ 0 ^ …^ 0 = 0,顯然已經輸了;
(2)若當前的局面不爲0,即a1 ^ a2 ^ a3 ^ … ^ an = k,則改變ai的值爲aci = ai ^ k,能使得a1 ^ a2 ^ a3 ^ … ^ aci ^ … ^ an=0。
證明很簡單,通過異或的性質,ai ^ k = a1 ^ a2 ^ a3 ^ … ^ ai ^ ai ^ … ^ an
ai ^ ai 將會被消去,即aci就是減去了ai這一堆的剩下的所有的異或,則此時
a1 ^ a2 ^ a3 ^ … ^ aci ^ … ^ an= a1 ^ a1^ a2 ^ a2 ^ a3 ^ a3 ^ … ^ an ^ an = 0
(3)若當前某個局面爲P,即a1 ^ a2 ^ a3 ^ … ^ an=0,一定不存在改變某個ai,使之變爲aci,使得a1 ^ a2 ^ a3 ^ … ^ aci ^ … ^ an = 0,因爲aci = ai ^ 0 = ai,由題目可得不能不取,即當前的ai值必須改變,所以此時aci一定不等於ai。 -
模板
void Nim(int x[]) {
int sum = 0;
for (int i = 0; i < x.length; i++)
sum ^= x[i];
if (sum == 0)
System.out.println("必輸");
for (int i = 0; i < x.length; i++) {
int k = sum ^ x[i];
if (k < x[i])
System.out.println("原堆個數爲" + x[i] + "的子堆變爲" + k);// 得出的結果爲所有的方案第一次移動的數
}
}
- 例題:HDU1907
/*
**題目讓求的是最後取光者爲失敗者,也就是說在取糖果的時候都要躲着最後一個糖果。
**首先來分析一下各種狀態:
**假設S態表示異或和非0,T態表示異或和爲0
**一堆糖果數量爲1的稱爲孤獨堆,否則稱爲充裕堆;Si或者Ti表示充裕堆的個數
** S0:表示有奇數個孤獨堆,則先手必敗;
** S1:表示有一個充裕堆,當孤獨堆的個數爲偶數時,先手只需要使得當前充裕堆只剩下一個即可
** 否則拿完即可,則先手必勝。
** T1:不存在這種狀態,即不存在異或和爲0且只有一個充裕堆的狀態
** T0:表示有偶數個孤獨堆。則先手必勝;
** S2:有兩個以及以上的充裕堆,異或和非0;
** T2:有兩個以上的充裕堆,異或和爲0;
** 現在主要來分析下S2和T2態,從上面可知,S0先手必敗,S1,T0先手必勝
** S2可以取一次變爲T2。T2取一次可變爲S2或者S1
** 因爲S1是先手必勝態,根據這兩個轉換規則,我們就能得知S2也是先手必勝,T2是先手必敗。
** 如果這道題是最後一個取的獲勝,即完全的Nim博弈,就不需要特判1的個數了。
*/
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();
for (int T = 0; T < t; T++) {
int n = sc.nextInt();
int ans = 0, x = 0, sum = 0;
for (int i = 0; i < n; i++) {
x = sc.nextInt();
if (x == 1) {
sum += 1;
}
ans ^= x;
}
if (sum == n) {
if (sum % 2 == 0) {// 偶數個全1
System.out.println("John");
} else {// 奇數個全1
System.out.println("Brother");
}
} else {
if (ans != 0)
System.out.println("John\n");
else
System.out.println("Brother\n");
}
}
sc.close();
}
}
WythoffGame:黃金分割
-
威佐夫博弈:兩堆(ak,bk)(ak<=bk)個物品,兩人輪流取,每次從一堆中取不少於1個或者從2堆中同時取k個,最後面對(0,0)局面的輸(設ak<=bk是爲了忽略順序的影響)
1.面對(0,0)局面必輸 2.面對(1,1)(2,2)...(n,n)局面必贏 (0,1)(0,2)...(0,n)局面必贏 3.如果可以使對手面臨必輸局面,那麼是必贏局面 4.如果不能使對手面臨必輸局面,那麼是必輸局面 基礎:(0,0)是必輸局面;(0,1)(0 ,2)...(0,n)是必贏局面, 遞推:(1,2)是必輸局面;(1,1)是必贏局面 (1,3)(1 ,4)...(1,n)是必贏局面 (2,2),(2,3)...(2,n)是必贏局面 (3,5)是必輸局面;(3,3)(3,4)是必贏局面 (3,6)(3,7)...(3,n)是必贏局面 (5,5)(5,6)...(5,n)是必贏局面 (4,7)是必輸局面;(4,4)(4,5)(4,6)是必贏局面 (4,8)(4,8)(4,9)...(4,n)是必贏局面 (7,7)(7,8)(7,9)...(7,n)是必贏局面 (6,10)是必輸局面;(6,6)(6,7)(6,8)(6,9)是必贏局面 (6,11)(6,12)(6,13)...(6,n)是必贏局面 (10,10)(10,11)(10,12)...(10,n)是必贏局面 規律:(必輸局面的規律比較容易找到) ak是前面必輸局未出現的數中最小者, bk=ak+k( k=0,1,2,3,...n) 下面介紹必輸局(奇異局)的最重要性質: 1,2,...,n中每一個自然數,出現且只出現在一個奇異局中。 推導:1.由於ak總是選擇未出現的數,所以每個數總能出現在奇異局中 且ak不會選擇到重複的數 2.bk=ak+k,所以bk總是比前面所有奇異局出現的數都大, 所以bk不會選擇到重複的數 必贏一方的策略是:始終讓對手面對必輸局(奇異局勢)
-
證明:這裏
-
給定任意局勢(a,b),判定(a,b)是否爲必輸局勢的方法是:
令 k=0,1…n
ak=[k * φ],bk=ak+k=[k * φ * φ]
如k=0,ak=0,bk=0
k=1,ak=1,bk=2
k=2,ak=3,bk=5
k=3,ak=4,bk=7 -
更好的一種判斷策略是 k = b-a ,如a=k*φ時,當前局勢爲奇異局勢
-
模板
void Wythoff(int a,double b){
double gold=(Math.sqrt(5)+1)/2;
int k=(a>b?a-b:b-a);
if(min==(int)(k*gold)) {
System.out.println("敗");
}else {
System.out.println("勝");
}
}
- 例題:POJ1067
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
double gold=(Math.sqrt(5)+1)/2;
int a,b;
while(sc.hasNextInt()) {
a=sc.nextInt();
b=sc.nextInt();
int min=(a<b?a:b);
int k=(a>b?a-b:b-a);
if(min==(int)(k*gold)) {
System.out.println(0);
}else {
System.out.println(1);
}
}
sc.close();
}
}
階梯博弈
- 階梯Nim
對奇數階的石頭進行 Nim,偶數階的石頭對結果不影響。
如上圖:Nim和 = 2 ^ 3 ^ 4 = 5 不爲 0,故先手必勝
把石頭從奇數階上移到偶數階上相同於 Nim 中取一次石子。
爲什麼是奇數階?
因爲最後石子都要到 0 上,從 1 到 0 就是最後一步,這是從奇數階到偶數階,
所以認爲從奇數階上移到偶數階上相當於取一次石子才能保證狀態一致。
若 Nim和 != 0,先手將非 0 的局面轉爲 0 的局面,即把部分石子從奇數階挪到下層偶數階。
此時對手只有兩種選擇:
① 挪動奇數階上的石頭,從而打破了 0 的局面,再一次把非 0
的局面留給先手方,先手方繼續調整奇數階的石子即可。
② 挪動偶數階上的時候到下層的奇數階上,先手只要將這些剛挪下來的石子繼續下挪,
這樣奇數階上的石子數量保持不變,又把 0 的局面留給對手。
這樣一來,就能保證先手必勝了。
- 模板
void Nim(int[] nim){
int res = 0;
for (int j = 0; j < n; j += 2) {
res ^= Nim[j];
}
if(res==0) {
System.out.println("敗");
}else {
System.out.println("勝");
}
}
- 例題:POJ1704
import java.util.Arrays;
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
int[] Nim,num;
for (int i = 0; i < T; i++) {
int n = sc.nextInt();
Nim = new int[n+1];
Nim[0]=0;
for (int j = 1; j <= n; j++) {
Nim[j] = sc.nextInt();
}
Arrays.sort(Nim);
int res = 0;
for (int j = n; j > 0; j -= 2) {
res ^= (Nim[j]-Nim[j-1]-1);
}
if(res==0) {
System.out.println("Bob will win");
}else {
System.out.println("Georgia will win");
}
}
sc.close();
}
}