由於圖論專題考試考到了博弈論,而後就跑過來通了一遍
至於圖論考試爲何會扯到博弈論?我不知道,就很奇怪html
博弈論 ,是經濟學的一個分支,主要研究具備競爭或對抗性質的對象,在必定規則下產生的各類行爲。博弈論考慮遊戲中的個體的預測行爲和實際行爲,並研究它們的優化策略。ios
詳細解釋能夠請自行百度百科git
先來看一道小學就接觸過的思惟題數組
你和好基友在玩一個取石子游戲。面前有30顆石子,每次只能取一顆或兩顆,你先取,取完的人爲勝,問你是否有必勝策略函數
Q:什麼?有必勝策略?可否勝利不該該隨着咱們選擇而改變嗎?
A:確實。但若是咱們足夠聰明呢?每次都作最優的選擇,把取勝之路留給本身
Q:我一點也不聰明,那該如何作呢?優化
先從簡單入手,
假如只有一個或兩個石子,無疑先手必勝
只有三個石子,無疑先手必輸spa
(咱們約定先手必敗狀態爲必敗狀態,先手必勝狀態爲必勝狀態)
這就是咱們的終止狀態,即不管怎麼拿,都會回到這幾個狀態
由於咱們想贏,因此咱們要讓本身處於必勝狀態,即剩下一個或兩個石子的時候,咱們是先手。不難發現,咱們也許不能使本身處於必勝態,但咱們可讓對方處於必敗態。即剩下三個石子的時候,咱們是後手。設計
不難發現,只要是三的倍數就必定是必敗狀態,不然就是必勝狀態。
證實:
假設不是三的倍數,咱們使它成爲三的倍數,此時咱們是後手。對方若是拿一個,咱們就拿兩個;若是拿兩個,咱們就拿一個。因此咱們那完後剩下的必定永遠是三的倍數,因此只剩下三個石子的時候咱們必定是後手,此時對手必輸,也就是咱們必勝。
假設是三的倍數,由於兩我的都足夠聰明,因此對方必定會使咱們永遠處於三的倍數中。因此咱們必敗。
因此只要判斷是否是三的倍數,就能夠肯定咱們是否必勝了code
至此,小學時代遺留的問題已經解決了能夠拿去欺負同窗,(這也是博弈論最基礎的問題,Nim遊戲)
能夠說,你已經學會博弈論了htm
如今,讓咱們對本身的思考作一下規範
把每一個可到達的狀態都看作結點,每次作出決策都是從舊的狀態轉移到新的狀態,也就是在兩個狀態結點間連一條有向邊。若是把全部狀態轉移都畫出來,咱們就獲得了一張博弈圖
就像這樣
大多數博弈圖會是一個DAG,不然遊戲不可能結束
經過推理不可貴到這幾個定理
對於定理一,遊戲進行不下去了,即這個玩家沒有可操做的了,那麼這個玩家就輸掉了遊戲
對於定理二,若是該狀態至少有一個後繼狀態爲必敗狀態,那麼玩家能夠經過操做到該必敗狀態;此時對手的狀態爲必敗狀態,即對手一定是失敗的,而相反地,本身就得到了勝利。
對於定理三,若是不存在一個後繼狀態爲必敗狀態,那麼不管如何,玩家只能操做到必勝狀態;此時對手的狀態爲必勝狀態——對手一定是勝利的,本身就輸掉了遊戲。
若是博弈圖是一個有向無環圖,則經過這三個定理,咱們能夠在繪出博弈圖的狀況下用 \(O(n + m)\) 的時間(其中 \(n\) 爲狀態種數, \(m\) 爲邊數)得出每一個狀態是必勝狀態仍是必敗狀態。(利用拓撲排序
讓咱們回顧Nim遊戲,顯然咱們能夠經過構建博弈圖得到是否必勝
但這樣的複雜度是 \(O(\begin{matrix} \prod_{i=1}^n a_i \end{matrix})\),顯然不能接受。
有沒有什麼快速簡單的方法呢?
定義 Nim 和 = \(a_1 \oplus a_2 \oplus a_3 \oplus ... \oplus a_n\)
當且僅當 Nim 和 爲 \(0\) 時,該狀態爲必敗狀態;不然該狀態爲必勝狀態。
證實過程詳見Oi-wiki
實際上是我沒看懂
後面內容也必定程度上會證實這個定理
有向圖遊戲是一個經典的博弈遊戲——實際上,大部分的公平組合遊戲均可以轉換爲有向圖遊戲。
在一個有向無環圖中,只有一個起點,上面有一個棋子,兩個玩家輪流沿着有向邊推進棋子,不能走的玩家判負。
定義 \(mex\) 函數的值爲不屬於集合 \(S\) 中的最小非負整數,即:
例如 \(mex(\{ 0, 1, 3, 4\}) = 1\),\(mex(\{1,2 \}) = 0,mex(\{\}) = 0\)
對於狀態 \(x\) 和它的全部 \(k\) 個後繼狀態 \(y_1,y_2,...,y_k\),定義 \(SG\) 函數:
SG定理:
而對於由 \(n\) 個有向圖遊戲組成的組合遊戲,設它們的起點分別爲 \(s_1,s_2,...,s_n\) ,則有定理: 當且僅當 \(SG(s_1) \oplus SG(s_2) \oplus ... \oplus SG(s_n) \ne 0\) 時,這個遊戲是先手必勝的。
仍是拿原來那個圖開刀
用 \(SG[]\) 數組來存全部結點的 \(SG\) 函數值
由於 \(9,3,8,10,4\) 這幾個點都沒有後繼狀態,因此它們 \(SG\) 值均爲 \(0\),同理推出 2,7,5這個點的 \(SG\) 值爲 \(1\),而
咱們能夠將一個有 \(x\) 個物品的堆視爲節點 \(x\) ,拿掉若干個石子後剩下 \(y\)個,則當且僅當 \(0 < y < x\) 時,節點 \(x\) 能夠到達 \(y\) 。
那麼,由 \(n\) 個堆組成的 Nim 遊戲,就能夠視爲 \(n\) 個有向圖遊戲了。
根據上面的推論,能夠得出 \(SG(x) = x\) 。再根據 SG 定理,就能夠得出 Nim 和的結論了。
不得不說,博弈論DP就是個神仙作法,能有博弈論DP作的都是神仙題!
並無什麼固定的作法,但基本原理仍是照着那三個定理來。能用DP的通常是由於想不出來如何用 \(SG\) 定理。狀態的設計都比較神仙,主要是根據題目要求來設計。
能夠參考一下下面兩個博弈論DP習題找找感受,我也不是很會,主要是學會如何去設計狀態。
其實這三道題大致思路上面都講過了,比較基礎
四、取石子游戲
Describe
一樣是n堆石子,只不過可取的石子數只有m個數,求先手必勝仍是先手必敗,並輸出第一次取的方案
Solution
現根據 \(m\) 個數預處理出 \(1000\) 之內的數的 \(SG\) 值,再將 \(n\) 堆石子的數量異或,若是是 \(0\) 先手必敗,反之先手必勝。
尋找一個方案:在從第一堆石子開始,一次拿取全部能拿取的狀況,並判斷可否達成必勝條件。必勝條件是拿去枚舉的拿取石子數量後,剩下的石子數異或起來爲 \(0\) ,由於你拿了一次石子後你就變成後手了
Code
/* Work by: Suzt_ilymics Knowledge: ?? Time: O(??) */ #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long #define orz cout<<"lkp AK IOI!"<<endl using namespace std; const int MAXN = 1e5+5; const int INF = 1e9+7; const int mod = 1e9+7; int n, m; int a[15], SG[MAXN], val[15]; int pos[15]; bool vis[MAXN]; int read(){ int s = 0, f = 0; char ch = getchar(); while(!isdigit(ch)) f |= (ch == '-'), ch = getchar(); while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar(); return f ? -s : s; } void init_SG(){ SG[0] = 0; for(int i = 1; i <= 1000; ++i){ // int maxm = -1; memset(vis, false, sizeof vis); for(int j = 1; j <= m && (i - val[j]) >= 0; ++j){ vis[SG[i - val[j]]] = true; // maxm = max(maxm, SG[i - val[j]]); } int j = 0; while(vis[j]) j++; SG[i] = j; } } bool check(int x, int y){ for(int i = 1; i <= n; ++i){ pos[i] = a[i]; } pos[x] -= y; int ans = 0; for(int i = 1; i <= n; ++i) ans ^= SG[pos[i]]; if(ans) return false; return true; } int main() { n = read(); for(int i = 1; i <= n; ++i) a[i] = read(); m = read(); for(int i = 1; i <= m; ++i) val[i] = read(); init_SG(); int ans = 0; for(int i = 1; i <= n; ++i) ans ^= SG[a[i]]; if(ans) { printf("YES\n"); for(int i = 1; i <= n; ++i){ for(int j = 1; j <= m && (a[i] - val[j]) >= 0; ++j){ if(check(i, val[j])) { printf("%d %d", i, val[j]); return 0; } } } } else printf("NO"); return 0; }
五、S-Nim
Describe
和第二題同樣,就是多了 \(T\) 組數據,每組數據又有多輪遊戲,每輪遊戲若是存在先手必勝輸出 \(W\) 不然輸出 \(L\)
Solution
直接根據Nim和來作就行了,須要注意的是每次都要預處理一遍 \(SG\) 函數,每次預處理以前都要拍一遍序
Code
/* Work by: Suzt_ilymics Knowledge: ?? Time: O(??) */ #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long #define orz cout<<"lkp AK IOI!"<<endl using namespace std; const int MAXN = 1e5+5; const int INF = 1e9+7; const int mod = 1e9+7; int k, m, n; int SG[10010], val[110]; int vis[10010]; int read(){ int s = 0, f = 0; char ch = getchar(); while(!isdigit(ch)) f |= (ch == '-'), ch = getchar(); while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar(); return f ? -s : s; } void init_SG(){ SG[0] = 0; for(int i = 1; i <= 10000; ++i){ memset(vis, false, sizeof vis); for(int j = 1; j <= k && (i - val[j]) >= 0; ++j){ vis[SG[i - val[j]]] = true; } int j = 0; while(vis[j]) ++j; SG[i] = j; } } int main() { // freopen("test1.in","r",stdin); // freopen("test.out","w",stdout); while(true){ memset(SG, 0, sizeof SG); k = read(); if(!k) break; for(int i = 1; i <= k; ++i) val[i] = read(); sort(val + 1, val + k + 1); init_SG(); m = read(); for(int j = 1; j <= m; ++j){ n = read(); int ans = 0; for(int i = 1; i <= n; ++i) ans ^= SG[read()]; if(ans) printf("W"); else printf("L"); } printf("\n"); } return 0; }
六、巧克力棒
Describe
一共10輪,每次一人能夠從盒子裏取出若干條巧克力棒,或是將一根取出的巧克力棒吃掉正整數長度。TBL 先手兩人輪流,沒法操做的人輸。若是勝輸出 \(NO\) ,負輸出 \(YES\)
Solution
須要對Nim博弈有深刻的瞭解,這題若是不用取巧克力,就是典型的Nim博弈。
咱們知道,Nim博弈,若是異或和爲0則是必敗狀態,因此,若是先手拿出幾根巧克力異或和不爲0,後手就可使異或和變爲0,此時先手再拿,後手又能夠經過操做使異或和變爲0。
因此,先手要想取勝,必須先拿出最大的異或和爲0的集合,此時後手不管怎麼操做,都會使異或和變爲不等於0。因此,若是有異或和爲0的集合,先手必勝。若是沒有,先手必輸。由於n很小,因此直接暴搜判斷便可。
Code
/* Work by: Suzt_ilymics Knowledge: ?? Time: O(??) */ #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long #define orz cout<<"lkp AK IOI!"<<endl using namespace std; const int MAXN = 1e5+5; const int INF = 1e9+7; const int mod = 1e9+7; int n; int a[22]; int read(){ int s = 0, f = 0; char ch = getchar(); while(!isdigit(ch)) f |= (ch == '-'), ch = getchar(); while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar(); return f ? -s : s; } bool dfs(int pos, int val, int cnt){ if(cnt && !val) return true; if(pos > n) return false; if(dfs(pos + 1, val, cnt)) return true; if(dfs(pos + 1, val ^ a[pos], cnt + 1)) return true; return false; } int main() { for(int i = 1; i <= 10; ++i){ n = read(); for(int j = 1; j <= n; ++j) a[j] = read(); dfs(1, 0, 0) ? printf("NO\n") : printf("YES\n"); } return 0; }
七、取石子
Describe
一樣n堆石子,兩種操做,拿一個或者合併其中兩堆,不能操做的人輸
Solution
參考的這篇博客
把一個石子的堆的數量做爲一個狀態,將多個石子的堆的數量做爲一個狀態跑搜索,同時用f數組來記錄答案減小搜索量
把本身的理解放到了註釋裏,看代碼吧
Code
/* Work by: Suzt_ilymics Knowledge: ?? Time: O(??) */ #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long #define orz cout<<"lkp AK IOI!"<<endl using namespace std; const int MAXN = 1010; const int INF = 1e9+7; const int mod = 1e9+7; int T, n; int f[55][55 * MAXN]; int read(){ int s = 0, f = 0; char ch = getchar(); while(!isdigit(ch)) f |= (ch == '-'), ch = getchar(); while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar(); return f ? -s : s; } int dfs(int cnt, int stp){ if(cnt <= 0 && stp <= 0) return 0;//若是沒有石子了,遊戲結束 if(f[cnt][stp] != -1) return f[cnt][stp];//若是之前搜到過,直接返回存儲的值,減小搜索複雜度 if(cnt <= 0) return f[cnt][stp] = (stp & 1);//只剩下熱鬧堆值,只須要判斷熱鬧堆裏石子的奇偶性就行了 if(stp == 1) return f[cnt][stp] = dfs(cnt + 1, 0);//若是熱鬧堆還剩下一個石子,就變成了一個寂寞堆 f[cnt][stp] = 0;//先賦爲0,後面再看看是否有使這個狀態必勝的後續狀態 if(cnt && !dfs(cnt - 1, stp)) return f[cnt][stp] = 1;//從寂寞堆裏拿一顆石子 if(stp && !dfs(cnt, stp - 1)) return f[cnt][stp] = 1;//從熱鬧堆裏拿一顆石子 if(cnt && stp && !dfs(cnt - 1, stp + 1)) return f[cnt][stp] = 1;//將寂寞堆合併到熱鬧堆裏 if(cnt > 1 && !dfs(cnt - 2, stp + 2 + (stp ? 1 : 0))) return f[cnt][stp] = 1;//將兩個寂寞堆合併,至於後面爲啥多加個1?還不是很懂 return f[cnt][stp]; } int main() { T = read(); memset(f, -1, sizeof f); while(T--){ int cnt = 0, stp = 0; n = read(); for(int i = 1, x; i <= n; ++i) x = read(), (x == 1) ? ++cnt : stp = (stp + x + 1); //經過題解的論證,發現熱鬧堆的合併並不影響結果,因此直接合並起來 if(stp) stp--;//少合併一次要減一(感受這裏有問題?) dfs(cnt, stp) ? puts("YES") : puts("NO"); } return 0; }
八、取石子游戲
Describe
n堆石子,一次取任意個,可是隻能從第一堆或者最後一堆取,求是否先手必勝
Solution
yyb神仙%%%!
設的神仙狀態,建議親自觀摩;還有一個很神奇的作法也能過,惋惜正確性不能保證
Code
DP:
/* Work by: Suzt_ilymics Knowledge: ?? Time: O(??) */ #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long #define orz cout<<"lkp AK IOI!"<<endl using namespace std; const int MAXN = 1e3+5; const int INF = 1e9+7; const int mod = 1e9+7; int T, n; int a[MAXN], L[MAXN][MAXN], R[MAXN][MAXN]; int read(){ int s = 0, f = 0; char ch = getchar(); while(!isdigit(ch)) f |= (ch == '-'), ch = getchar(); while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar(); return f ? -s : s; } int main() { T = read(); while(T--){ n = read(); for(int i = 1; i <= n; ++i) L[i][i] = R[i][i] = a[i] = read(); for(int len = 2; len <= n; ++len){ for(int i = 1, j = i + len - 1; j <= n; ++i, ++j){ int x = a[j], l = L[i][j - 1], r = R[i][j - 1]; if(x == r) L[i][j] = 0; else if((x < l && x < r) || (x > l && x > r)) L[i][j] = x; else if(r < x && x < l) L[i][j] = x - 1; else L[i][j] = x + 1; x = a[i], l = L[i + 1][j], r = R[i + 1][j]; if(x == l) R[i][j] = 0; else if((x < l && x < r) || (x > l && x > r)) R[i][j] = x; else if(r < x && x < l) R[i][j] = x + 1; else R[i][j] = x - 1; } } printf("%d\n", (L[2][n] == a[1]) ? 0 : 1); } return 0; }
奇技淫巧:(雖然過了但已被Hack)
主要是判斷最外邊兩個堆的關係,看能不能讓對手先拿裏面的堆
/* Work by: Suzt_ilymics Knowledge: ?? Time: O(??) */ #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long #define orz cout<<"lkp AK IOI!"<<endl using namespace std; const int MAXN = 1e5+5; const int INF = 1e9+7; const int mod = 1e9+7; int T, n; int a[MAXN]; int read(){ int s = 0, f = 0; char ch = getchar(); while(!isdigit(ch)) f |= (ch == '-'), ch = getchar(); while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar(); return f ? -s : s; } int main() { T = read(); while(T--){ n = read(); int ans = 0; for(int i = 1; i <= n; ++i) a[i] = read(); if(abs(a[1] - a[n]) <= 1){ if(a[1] != 1 && a[n] != 1) printf("0\n"); else printf("1\n"); } else printf("1\n"); } return 0; }
若是本文有什麼錯誤,或者您有什麼問題,請在評論區提出。