博弈論
一. 抽象表示
將組合遊戲中的每一個狀態抽象成圖中的一個點,將每一步決策抽象爲圖中的一條邊。這樣,對於組合遊戲的每一次博弈,我們都可以將其抽象成遊戲圖中的一條從某一頂點到出度爲0的路徑
二. P, N狀態
一個先手勝狀態被認爲是一個N-狀態(因爲下一個玩家即將獲勝),一個後手勝狀態被認爲是一個P-狀態(因爲前一個玩家即將獲勝)。
P-和N-狀態歸納性地描述如下:
一個點v是P狀態當且僅當它的所有後繼爲N狀態
一個點v是N狀態當且僅當它的一些後繼爲P狀態
三. SG函數
給定一個有限子集SN,令mexS(最小排斥值)爲沒有出現在S中的最小自然數
現在給定一個遊戲圖G = (V, E),則有
性質:
對於任意的局面,若它的SG值爲0,則它任何一個後繼局面的SG值不爲0
對於任意的局面,若它的SG值不爲0,那麼它一定有一個後繼局面的SG值爲0
在我們每次只能進行一步操作的情況下,對於任何的遊戲的和,我們若將其中的任一單一 SG-組合遊戲換成數目爲它的SG 值的一堆石子,該單一 SG-組合遊戲的規則變成取石子游戲的規則(可以任意取,甚至取完),則遊戲的和的勝負情況不變。
四. Anti-SG遊戲
定義:決策集合爲空的遊戲者贏,即取走最後一個石子的人敗
SJ定理:對於任意一個 Anti-SG 遊戲,如果我們規定當局面中所有的單一遊戲的 SG 值爲 0 時,遊戲結束,則先手必勝當且僅當:
- 遊戲的 SG 函數不爲 0 且遊戲中某個單一遊戲的 SG 函數大於 1
- 遊戲的 SG 函數爲 0 且遊戲中沒有單一遊戲的 SG 函數大於 1。
五. Multi-SG遊戲
定義:在符合拓撲原則的前提下,一個單一遊戲的後繼可以爲多個單一遊戲,即可以將一堆石子分爲多堆石子
先從Multi-Nim開始:有n堆石子,兩個人可以從任意一堆石子中拿任意多個石子(不能不拿)或把一堆數量不少於2石子分爲兩堆不爲空的石子,沒法拿的人失敗。問誰會勝利。這有一神奇定義:
六. Every-SG遊戲
定義:對於還沒有結束的單一遊戲,遊戲者必須對該遊戲進行一步決策,即每一個可以移動的棋子都要移動
有如下解法:在通過拓撲關係計算某一個狀態點的 SG 函數時(事實上,我們只需要計算該狀態點的 SG 值是否爲 0)對於 SG 值爲 0 的點,我們需要知道最快幾步能將遊戲帶入終止狀態,對於 SG 值不爲 0 的點,我們需要知道最慢幾步遊戲會被帶入終止狀態,我們用step函數來表示這個值。
定理:對於Every-SG遊戲先手必勝當且僅當單一遊戲中最大的step爲奇數
七. 翻硬幣遊戲
一般規則如下:
- N 枚硬幣排成一排,有的正面朝上,有的反面朝上。我們從左開始對硬幣按 1 到 N 編號。
- 遊戲者根據某些約束翻硬幣(如:每次只能翻一或兩枚,或者每次只能翻連續的幾枚),但他所翻動的硬幣中,最右邊的必須是從正面翻到反面。
- 誰不能翻誰輸。
結論:局面的SG值爲局面中每個正面朝上的棋子單一存在時的SG值的異或和
八. 樹的刪邊遊戲
規則如下:
- 給出一個有 N 個點的樹,有一個點作爲樹的根節點。
- 遊戲者輪流從樹中刪去邊,刪去一條邊後,不與根節點相連的部分將被移走。
- 誰無路可走誰輸。
定理:葉子節點的SG值爲0;中間節點的SG值爲它的所有子節點的SG值加1後的異或和
九. 無向圖刪邊遊戲
1. Christmas Game
題目大意:
- 有 N 個局部聯通的圖。
- Harry 和 Sally 輪流從圖中刪邊,刪去一條邊後,不與根節點相連的部分將被移走。Sally 爲先手。
- 圖是通過從基礎樹中加一些邊得到的。
- 所有形成的環保證不共用邊,且只與基礎樹有一個公共點。
- 誰無路可走誰輸
有如下性質:
- 對於長度爲奇數的環,去掉其中任意一個邊之後,剩下的兩個鏈長度同奇偶,異或之後的 SG 值不可能爲奇數,所以它的 SG 值爲 1;
- 對於長度爲偶數的環,去掉其中任意一個邊之後,剩下的兩個鏈長度異奇偶,異或之後的 SG 值不可能爲 0,所以它的 SG 值爲 0;
所以我們可以去掉所有的偶環,將所有的奇環變爲長短爲1的鏈,這樣就改造成了上一節的模型,算出每一棵樹的SG值之後,再按Nim遊戲異或即可
2. 無向圖刪邊遊戲
對Christmas Game進行一步拓展——去掉對環的限制,規則如下:
- 一個無相聯通圖,有一個點作爲圖的根。
- 遊戲者輪流從圖中刪去邊,刪去一條邊後,不與根節點相連的部分將被移走。
- 誰無路可走誰輸。
對於這一模型,有一著名定理Fusion Principle :
我們可以對無向圖做如下改動:將圖中的任意一個偶環縮成一個新點,任意一個奇環縮成一個新點加一個新 邊;所有連到原先環上的邊全部改爲與新點相連。這樣的改動不會影響圖的 SG 值。
這樣,我們可以將任意一個無向圖改成樹結構,“無向圖刪邊遊戲” 就變成了 “樹的刪邊遊戲”
十.簡單博弈
1.斐波那契博弈
1堆石子有n個,兩人輪流取.先取者第1次可以取任意多個,但不能全部取完.以後每次取的石子數不能超過上次取子數的2倍。取完者勝.先取者負輸出"Second win".先取者勝輸出"First win".
Zeckendorf定理(齊肯多夫定理):任何正整數可以表示爲若干個不連續的Fibonacci數之和
#include<cstdio>
#include<iostream>
using namespace std;
int main()
{
int n, fib[50];
fib[0] = 2; fib[1] = 3;
for(int i = 2; i < 50; i++)
fib[i] = fib[i-1] + fib[i-2];
while(~scanf("%d", &n) && n){
int flag = 1;
for(int i = 0; i < 50; i++){
if(fib[i] == n){
flag = 0;
cout << "Second win" << endl;
}
if(fib[i] > n)
break;
}
if(flag){
cout << "First win" << endl;
}
}
return 0;
}
2.威佐夫博弈
有兩堆各若干個物品,兩個人輪流從某一堆或同時從兩堆中取同樣多的物品,規定每次至少取一個,多者不限,最後取光者得勝
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a, b, c;
while(~scanf("%d %d", &a, &b)){
if(a > b)
swap(a, b);
c = floor(b-a) *((sqrt(5.0)+1)/2);
if(a == c) cout << "0" << endl;
else cout << "1" << endl;
}
return 0;
}
3.Nimk博弈
有n堆各若干個物品,兩個人輪流從K堆取任意多的物品,規定每次至少取一個,多者不限,最後取光者得勝
const int MAXN = 10005;
int SG[MAXN];//需要處理的SG值數組
int XOR[MAXN];//儲存每一個二進制位上的和
int xxx;//儲存每一個SG值的臨時變量
int num;//儲存當前SG值有多少位的臨時變量
int maxn;//儲存最大的SG值位數
bool solve(int N,int M)//N表示SG數組的大小,從1到N,M表示每次可以取1到M堆
{
memset(XOR, 0, sizeof XOR);
maxn = -1;
for (int i = 1; i <= N; i++)
{
xxx = SG[i];
num = 0;
while (xxx) {
XOR[num] += xxx&1;
num++;
xxx >>= 1;
}
maxn = max(maxn, num);
}
for (int i = 0; i < maxn; i++)
if (XOR[i] % (M + 1))
return true;//返回true表示先手必勝
return false;//返回false表示先手必敗
}
十一.例題
1.HDU–1536
在最簡單的取石子情況中,有如下規律:
#include <bits/stdc++.h>
using namespace std;
const int N=10008;//N爲所有堆最多石子的數量
int f[108],sg[N];//f[]用來保存只能拿多少個,sg[]來保存SG值
bool hash[N];//mex{}
void sg_solve(int t,int N)
{
int i,j;
memset(sg,0,sizeof(sg));
for(i=1;i<=N;i++) {
memset(hash,0,sizeof(hash));
for(j=1;j<=t;j++)
if(i - f[j] >= 0)
hash[sg[i-f[j]]] = 1;
for(j=0;j<=N;j++)
if(!hash[j])
break;
sg[i] = j;
}
}
int main()
{
int k, m, l, num;
while(scanf("%d",&k), k) {
for(i= 1;i <= k;i++) scanf("%d",&f[i]);
sg_solve(k, N);
scanf("%d", &m);
string ans = "";
for (int i = 1; i <= m; i++) {
int sum = 0;
scanf("%d", &l);
for (int j = 1; j <= l; j++) {
scanf("%d", &num);
sum ^= sg[num];
}
if(sum == 0) ans+="L";
else ans+="W";
}
cout<<ans<<endl;
}
return 0;
}
2.HDU–1524
博弈的抽象表示,幫助理解SG
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1005;
int sg[maxn], pat[maxn];
vector<int> G[maxn];
int getSG(int u)
{
if (sg[u] != -1) return sg[u];
bool mex[10*maxn];
memset(mex, false, sizeof(mex));
for (int i = 0; i < G[u].size(); ++i) {
int v = G[u][i];
getSG(v);
mex[sg[v]] = true;
}
for (int i = 0; ;++i) {
if (!mex[i]) {
sg[u] = i;
break;
}
}
return sg[u];
}
int main()
{
int N, m, p, x;
while (~scanf("%d", &N)) {
for (int i = 0; i < N; ++i) {
G[i].clear();
pat[i] = 0;
}
for (int i = 0; i < N; ++i) {
scanf("%d", &p);
while (p--) {
scanf("%d", &x);
G[i].push_back(x);
pat[x]++;
}
}
memset(sg, -1, sizeof(sg));
for (int i = 0; i < N; ++i) {
if (pat[i] == 0) getSG(i);
}
while (~scanf("%d", &m), m) {
int ans = 0;
for (int i = 0; i < m; ++i) {
scanf("%d", &x);
getSG(x);
ans ^= sg[x];
}
if (ans != 0) cout << "WIN" << '\n';
else cout << "LOSE" << '\n';
}
}
return 0;
}
3.BZOJ–1299
對於一個博弈遊戲而言,重要的是通過操作留給對面一個的局面
#include<cstring>
#include<cstdio>
using namespace std;
int n,sg[2001],a[2001];
bool found;
int dfs(int x,int used,int now)
{
if(x==n+1)
{
if(!now&&used>0)found=1;
return 0;
}
dfs(x+1,used,now);
dfs(x+1,used+1,now^a[x]);
}
int main()
{
for(int t=1;t<=10;t++)
{
memset(sg,-1,sizeof(sg));
found=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
dfs(1,0,0);
if(found)puts("NO");
else puts("YES");
}
return 0;
}
類Green博弈
現有一棵樹,樹的每條邊都有長度,A和B輪流染色,每次可選擇一條邊進行染色,染色長度邊長,已染色的邊不能再染色,同時將子節點刪除。
green博弈變形,對於都是1的就是green博弈 ^
對於大於1的邊,偶數對其沒有貢獻,奇數有貢獻, ^$= SG[v] KaTeX parse error: Expected group after '^' at position 1: ^̲(val[v] % 2)$
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2005;
int cnt;
struct Node {
int to, val, next;
}edge[maxn];
int sg[maxn], head[maxn];
void add(int u, int v, int w)
{
edge[cnt].to = v;
edge[cnt].val = w;
edge[cnt].next = head[u];
head[u] = cnt++;
}
void dfs(int u, int fa)
{
sg[u] = 0;
for (int i = head[u]; ~i; i = edge[i].next) {
int v = edge[i].to;
if (v == fa) continue;
dfs(v, u);
if (edge[i].val == 1) sg[u] ^= (sg[v]+1);
else sg[u] ^= (sg[v] ^ (edge[i].val % 2));
}
}
int main()
{
int T;
cin >> T;
for (int j = 1; j <= T; ++j) {
memset(head, -1, sizeof(head));
cnt = 0;
int n, u, v, w;
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
scanf("%d %d %d", &u, &v, &w);
add(u, v, w);
add(v, u, w);
}
dfs(0, 0);
printf("Case %d: ", j);
if (sg[0]) cout << "Emily" << '\n';
else cout << "Jolly" << '\n';
}
return 0;
}
階梯博弈
n個人隨機的站在若干級臺階上,最高一級臺階必須站人,A和B分別移動某個人向上走任意走多級臺階,不能越過更高級臺階上的人,且同一臺階上只能站一人,無法繼續移動者輸。給出n個人位置,計算先手必勝策略。
#include <stdio.h>
#include <string.h>
const int N = 100 + 10;
int a[N],b[N];
int main()
{
int n = 0,i,j,k,sum = 0;
while(scanf("%d",&a[n])!=EOF)
n++;
for(i=1; i<n; ++i)
b[i-1] = a[i] - a[i-1] - 1;
for(i=0; i<n-1; i+=2)
sum ^= b[i];
if(sum==0)
printf("-1\n");
else
{
//枚舉第i個人移動j步,使得剩下的局面異或等於0,
for(i=0; i<n-1; ++i)
for(j=1; a[i]+j<a[i+1]; ++j)
{
b[i] -= j;
if(i!=0)
b[i-1] += j;
sum = 0;
for(k=0; k<n-1; k+=2)
sum ^= b[k];
if(sum==0)
{
printf("%d %d\n",a[i],a[i]+j);
break;
}
b[i] += j;
if(i!=0)
b[i-1] -= j;
}
}
return 0;
}