博弈论

简单博弈论学习记录

博弈论


简介

两人在公平规则下进行有限的对决,胜负明确

类似有向无环图,由一个状态转移到下一个状态

对先手来说,存在两种状态,即必胜态和必败态

必胜态

当前状态的后续为必败态,当前状态即为必胜态

必败态

不存在后续,或是所有后续都必胜


DP 解决博弈

利用状态转移的关系,来解决简单的博弈问题

移棋子问题

$n \times m$ 的棋盘,$(1,1)$ 在左上角,$(n,m)$ 在右下角,每格标明黑白两色
上面有一个棋子,Alice 和 Bob 轮流移动这个棋子,Alice 先手移动,每次可以向上或向左移动一格,一旦移动到第一行或第一列游戏结束,执行最后一步移动的人,如果将棋子移动到黑格,就获胜,反之,则失败。
现在给这个棋子的起始位置,问最后获胜的玩家是谁,对于所有的 $(i,j)$ 满足 $2 \leq i,j \leq n$ 输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10010;

int dp[MAXN][MAXN],n,m;
string a,b;

int main(){
    cin>>n>>m>>a>>b;
    for(int i=0;i<=m-2;i++){
        dp[1][i+2]=(a[i]=='W');
    }
    for(int i=0;i<=n-2;i++){
        dp[i+2][1]=(b[i]=='W');
    }
    for(int i=2;i<=n;i++){
        for(int j=2;j<=m;j++){
            dp[i][j]=!(dp[i-1][j]&&dp[i][j-1]);
            if(dp[i][j])cout<<"A";
            else cout<<"B";
        }
        puts("");
    }
}

取石子游戏

有一堆石子,大小为 $x$,Alice 和 Bob 轮流操作,Alice 先手,Alice 每次可以取 $a[i]$ 个石子,Bob 可以取 $b[i]$ 个,谁不能操作就输。
问谁能获胜,对 $x = 1 \sim m$ 都输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;

int n1, n2, m, a[N], b[N], A[N], B[N];

int main(){
    cin >> n1 >> n2 >> m;
    for (int i = 1; i <= n1; i++){
        cin >> a[i];
    }
    for (int i = 1; i <= n2; i++){
        cin >> b[i];
    }
    for (int i = 1; i <= m; i++){
        for (int j = 1; j <= n1; j++){
            if(i >= a[j] && B[i-a[j]] == 0){
                A[i] = 1;
                break;
            }
        }
        for (int j = 1; j <= n2; j++){
            if(i >= b[j] && A[i-b[j]] == 0){
                B[i] = 1;
                break;
            }
        }
        puts(A[i] ? "Alice" : "Bob");
    }
    return 0;
}

带和局的情况

存在先手必败,胜
存在和,和


经典模型

巴什博弈

有 $n$ 个石子,每次取 $1 \sim m$ 个,Alice 先取,谁取最后一颗谁胜

$n % (m + 1) = 0$,先手必败
$n % (m + 1) \neq 0$,先手必胜

威佐夫博弈

有两堆石子,分别有 $a, b$ 颗,每次可以选一堆取 $x$ 个,也可以选两堆都取 $x$ 个
所以 $(a, b)$ 有三种转移,$(a-x, b)$, $(a, b-x)$, $(a-x, b-x)$;

打表找规律

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;

int dp[N][N];

int main(){
    for (int i = 0; i <= 100; i++){
        for (int j = 0; j <= 100; j++){
            for (int x = 1; x <= i; x++)
                if (dp[i - x][j] == 0) dp[i][j] = 1;
            for (int x = 1; x <= j; x++)
                if (dp[i][j - x] == 0) dp[i][j] = 1;
            for (int x = 1; x <= i && x <= j; x++)
                if (dp[i - x][j - x] == 0) dp[i][j] = 1;
            if (dp[i][j] == 0) printf("%d %d\n", i, j);
        } 
    }
    return 0;
}

首先发现 $(a, b)$ 和 $(b, a)$,状态相同,即同为必败态或必胜态
再对 $a < b$ 的情况分析,发现 $a$ 是从 $1$ 到 $n$ 的,$b - a$ 逐渐递增,依次为 $1, 2, 3, 4, 5, …$

Nim 模型

有 $n$ 堆石子,每堆有 $a[i]$ 个石子,Alice 和 Bob 轮流取,Alice 先取,每次可以从选一堆任取 $x$ 个石子,可以拿光,但不能不拿,谁最后把所有的拿光谁获胜

先打表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;

int dp[N][N];

int main(){
    for (int i = 0; i <= 100; i++){
        for (int j = 0; j <= 100; j++){
            for (int x = 1; x <= i; x++)
                if (dp[i - x][j] == 0) dp[i][j] = 1;
            for (int x = 1; x <= j; x++)
                if (dp[i][j - x] == 0) dp[i][j] = 1;
            // for (int x = 1; x <= i && x <= j; x++)
            //     if (dp[i - x][j - x] == 0) dp[i][j] = 1;
            if (dp[i][j] == 0) printf("%d %d\n", i, j);
        } 
    }
    return 0;
}

发现 $i == j$,经推广更多维,可以总结出必败态:$A_1 \oplus A_2 \oplus A_3 \oplus … \oplus A_n = 0$
要证明此结论,可从三个定理入手
1:没有后续状态的状态是必败态
2:对于 $A_1 \oplus A_2 \oplus A_3 \oplus … \oplus A_n \neq 0$ 的局面一定存在某种移动使 $A_1 \oplus A_2 \oplus A_3 \oplus … \oplus A_n = 0$
3:对于 $A_1 \oplus A_2 \oplus A_3 \oplus … \oplus A_n = 0$,不存在一种移动使 $A_1 \oplus A_2 \oplus A_3 \oplus … \oplus A_n = 0$


例题练习

石子游戏 2

有 $n$ 堆石子,每堆有 $a[i]$ 个石子,Alice 和 Bob 轮流操作,每次可以把一堆个数为奇数的石子分为两堆,两堆都不能为空,或把两堆为偶数的石子和为一堆

可以注意到两种操作都是对堆数改变,那么堆数的奇偶性就是突破点,同时根据操作的实质,可以发现只有 $1$ 个石子的堆是无法继续操作的,最后石堆将变成全是 $1$ 或者 $1$ 个偶数剩下的全是 $1$

所以结论就是:如果石堆全是 $1$,则先手必败,如果不是全为 $1$ 那么就检测结束时堆数的奇偶性是否改变

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;

int n, odd, one;
int x;

int main(){
    cin >> n;
    for (int i = 1;i <= n; i++){
        cin >> x;
        if (x % 2 == 1) odd++;
        if (x == 1) one++;
    }
    if (one != n) odd++;
    if ((odd + n) % 2 != 0)cout << "Alice\n";
    else cout << "Bob";
    return 0;
}

石子游戏 3

有 $n$ 堆石子,每堆有 $a[i]$ 个石子,Alice 和 Bob 轮流操作,选择 $n/2$ 堆非空石子,每堆移除掉正数个(可以不同)的石子,从 Alice 开始。

倒推:
必败:超过 $n/2$ 个堆已经为 $0$
必胜:有 $1 \sim n/2$ 个堆为 $0$
必败:有 $> n/2$ 个堆石子数量为 $1$
必胜:有 $1 \sim n/2$ 个堆的数量为 $1$
必败:有 $> n/2$ 个堆的数量为 $2$
……
有大于 $n/2$ 个堆的数量 = min,必败

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;

int n, mx = 1e9, cnt;

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++){
        int x;
        cin >> x;
        if (x < mx) mx = x, cnt = 0;
        if (x == mx) cnt++;
    }
    if (cnt > n/2) cout << "Alice\n";
    else cout << "Bob\n";
    return 0;
}

SG 板子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <bits/stdc++.h>
using namespace std;

int sg[1010];

int main() {
    int n, p;
    cin >> n >> p;
    for (int i = 1; i <= n; i++) {
        set<int> s;
        for (int j = 1; j <= i; j *= p) {
            s.insert(sg[i - j]);
        }
        while (s.count(sg[i])) sg[i]++;
        printf("%d %d\n", i, sg[i]);
    }
    return 0;
}
发表了95篇文章 · 总计15万2千字
永远相信美好的事情即将发生。
使用 Hugo 构建
主题 StackJimmy 设计