算法讲堂一:博弈论入门

Posted riotian

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法讲堂一:博弈论入门相关的知识,希望对你有一定的参考价值。

博弈论的题目有如下特点:

  • 1:博弈模型为两人轮流决策的博弈。并且两人都使用最优策略来取得胜利。

    • 两个玩家,都会采取最优的决策,那么如果存在一个局面为必胜局面,某玩家位于此局面。只要自己无失误,则必胜。那么同样又一个局面为必败局面,某玩家位于此局面。只要对手无失误,则必败。
    • 那也就是说,针对这样的游戏,我们关注点应该在局面上。
  • 2:博弈是有限的。即无论两人如何决策,都会在有限步决出胜负。

  • 3:博弈是公平的。即两人进行决策的规则相同。

  • 相关概念:
    • 先手必胜状态:先手可以从这个状态走到某一个必败状态。
    • 先手必败状态:先手走不到任何一个必败状态。
    • 也就是说先手必胜状态,那么先手一定能采取某些操作,让后手面对必败态。如果是先手必败态,无论先手怎么操作,都无法让后手面对必败态。

简单博弈的基本题型

1:bash博弈;2:nim博弈;3:威佐夫博弈;5:Fibonacci博弈;6:sg函数;

bash博弈 (巴什博奕)

  • 假设一堆石子有n个,每次最多取m个,甲乙两个玩家轮流取石子,最后把石子取完的人获胜,保证甲乙每一步的决策都是最优的,请问给定n和m,问甲胜还是乙胜。

    • 不妨假设刚刚开始

      n = m + 1
      

      ,那么后手必胜,有如下结论:

      • n = ( m + 1 ) * r + s 其中(r > 1,0 <= s < m + 1)。如果s=0的话,先手每次取k个,后手只要取(m+1-k)个即可,后手必赢。如果s!=0的话,先手者第一次取s个,后手第一次取k个,接下来先手只要取(m + 1 - k)个即可,先手必赢。
      • 所以只需考虑 是否为0就可以判定结果。余为0,先手必败,反之必胜。
  • 例题:

    • hdu_2188

      #include<bits/stdc++.h>
      using namespace std;
      int c, m, n;//总捐款数,每次最多m
      int main() {
          //freopen("in.txt","r",stdin);
      	cin >> c;
      	while (c--) {
      		cin >> n >> m;
      		if (n % (m + 1) == 0)
      			cout << "Rabbit
      ";
      		else
      			cout << "Grass
      ";
      	}
      	return 0;
      }
      
  • hdu_1846

  • hdu_1847

Nim游戏

  • 假设有n堆石子,每堆石子分别有(a_1,a_2,…,a_n个),每次可以选择任意一堆且至少取1枚石子, 甲乙两个玩家轮流取石子, 最后把石子取完的人获胜, 保证甲乙每一步的决策都是最优的, 甲为先手操作, 问甲胜还是乙胜。

  • 结论:

    • 设若(a_1^{ ∧ }a_2^{∧}…^{∧}a_n = 0)则先手必败, 反之必胜。
  • 证明

  • (a)不全为(0)时, 任意一个(res!=0)的局面, 先手可以通过一定的操作让后手面对(res=0)的局面。

  • 对于任意一个(res=0)的局面, 先手无法通过任何操作让后手面对(res=0)的局面。

  • 得出结论, 当(res=0)时先手必败, 反之必胜。

Nim博弈拓展-台阶Nim

  • 问题描述: 有一个(n)级台阶的楼梯, 每级台阶上有若干个石子, 其中第i级台阶上有aiai个石子((i≥1))。两位玩家路轮流操作, 每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶上(不能不拿)。

  • 已经拿到地面的石子不能再拿, 最后无法进行操作的人视为失败。

  • 问如果两人都采取最优策略, 先手是否必胜.

  • 结论
    • (res=a_1∧a_3∧a_5∧,…,∧a_n=0)(当然这里的n是奇数)先手必败, 反之先手必胜。
  • 证明
  • 1): 考虑极端情况, 当(a1,a3,…,an)全为0时, (res=0), 此时先手只能将偶数级台阶往下搬, 后手只需要将先手从偶数级台阶上搬下来的石子全部搬到下一级偶数级台阶, 先手必败。

  • 2): 当(res=x≠0)时, 通过经典(Nim)游戏的证明, 我们知道一定有一种方法搬一定的石子到下一级让后手面对res为0的局面。

  • 3):当(res=x=0)(a)不全为(0)时, 我们无法通过任何操作让下一个状态的(res)也为(0)

  • 即对于(res)不为(0)的情况, 先手总能通过一定的操作让后手面对(res)(0)的情况,。

  • 然而(res)(0)时, 先手无论做什么操作都无法让后手面对(res)(0)的情况。

  • 那么此刻我们就将题目转化为在奇数台阶上的经典Nim游戏。

  • 思考题:

  • 为什么不用(res=a_2∧a_4∧a_6∧,…,∧a_n=0)(n为偶数)来判定胜负?

    • 因为当先手搬去一定的石子让后手面对res=0res=0的情况, 后手可以搬去一号台阶的石子到地面让先手重新面对res=0res=0的情况
例题:
  • hdu_1850(经典Nim)
  • #include<bits/stdc++.h>
    using namespace std;
    const int maxn = 100 + 10;
    int n, a[maxn], res;
    int main() {
        //freopen("in.txt","r",stdin);
    	while (cin >> n,n)
    	{
    		res = 0;
    		for (int i = 1; i <= n; i++){
    			cin >> a[i];
    			res ^= a[i];
    		}
    		if (res == 0) puts("0");
    		else{
    			int ans = 0;
    			for (int i = 1; i <= n; i++)
    				if ((res ^ a[i]) < a[i]) ans++;
    			cout << ans << endl;
    		}
    	}
    	return 0;
    }
    
  • hdu_1730(经典Nim)

  • #include<bits/stdc++.h>
    using namespace std;
    int main() {
    	//freopen("in.txt","r",stdin);
    	int n, m;
    	while (cin >> n >> m) {
    		int res = 0;
    		for (int i = 1; i <= n; ++i) {
    			int a, b; cin >> a >> b;
    			res = res ^ (abs(a - b) - 1);
    		}
    		if (res == 0) puts("BAD LUCK!");
    		else puts("I WIN!");
    	}
    	return 0;
    }
    
  • poj_1704(台阶Nim)

  • #include<iostream>
    #include<cstdio>
    #include<algorithm>
    using namespace std;
    const int N = 1000 + 10;
    int a[N];
    int main() {
    	//freopen("in.txt", "r", stdin);
    	int t, n;
    	cin >> t;
    	while (t--) {
    		int ans = 0;
    		cin >> n;
    		for (int i = 1; i <= n; ++i) cin >> a[i];
    		sort(a + 1, a + n + 1);
    		if (n % 2)
    		{
    			ans ^= (a[1] - 1);
    			for (int i = 3; i <= n; i += 2)
    				ans ^= (a[i] - a[i - 1] - 1);
    		}
    		else for (int i = 2; i <= n; i += 2)
    			ans ^= (a[i] - a[i - 1] - 1);
    		if (ans) printf("Georgia will win
    ");
    		else printf("Bob will win
    ");
    	}
    return 0;
    }
    
  • hdu_4315(台阶Nim)

  • #include<bits/stdc++.h> 
    using namespace std;
    const int maxn = 1e3 + 10;
    int a[maxn];
    int n, k;
    int main() {
        //freopen("in.txt","r",stdin);
        while(cin >> n >> k)
        {
            memset(a, 0, sizeof a);
            for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
            if(k == 1) puts("Alice");
            else
            {
                int res = 0;
                if(n & 1)
                {
                    if(k == 2) res ^= a[1] - 1;
                    else res ^= a[1];
                    for(int i = 3; i <= n; i += 2)
                        res ^= a[i] - a[i - 1] - 1;
                }
                else
                {
                    for(int i = 2; i <= n; i += 2)
                        res ^= a[i] - a[i - 1] - 1;
                }
                if(res) puts("Alice");
                else puts("Bob");
            }
        }
        return 0;
    }
    

Wythoff 游戏 (威佐夫博弈)

  • 两堆石子各有若干个, 两人轮流从一堆取至少一个石子或从两堆取同样多的物品, 最后一名取完石子者胜利。

  • 结论:

    • 当两堆石子各有(n)(m)个且不妨设(n<m)
    • 当(m?n)(√5+1)/2=n时, 先手必败。
  • 证明
  • ?首先考虑最(zhao)极(gui)端(lv)的情况, (0, 0), (1, 2), (3, 5)局面为先手必败局面。而且这样的数字对被称为奇异局势。

  • 奇异局势的定义如下:

    • 设数字对为(a[(i),b(i)])
    • 1:(a(0)=b(0)=0);
    • 2: (a(k))是前面数字对中未出现的最小的自然数, 且(a(k)+k=b(k))
  • 接下来我们看奇异局势的几个性质:

    • 性质1: 任何自然数都包含在一个且仅有一个奇异局势中。

    • 性质2: 任意操作都能将奇异局势转变为非奇异局势.

    • 性质3: 采取适当的方法, 可将非奇异局势转变为奇异局势。

      证明略

  • 结论:奇异局势必败

技术图片

例题
  • hdu_2177

  • #include<bits/stdc++.h> using namespace std;
    int n, m;
        
    bool check(int n, int m) {
        int x = min(n, m), y = max(n, m);
        double c = (sqrt(5.00000) + 1) / 2;
        double d = (double)(y - x);
        if(x == int(c*d)) return 1; // 必败     return 0;
    }
        
    void work() {
        if(n > m) swap(n, m); // (n, m)     //第一个模块 我们能一起减去让他成为必败态     {
            int tem = m - n;
            double c = (sqrt(5.00000) + 1) / 2;
            int a = double(tem) * c;
            int b = a + tem;
            if(n - a == m - b) cout << a << " " << b << endl;
        }
        //第二个模块 我们求出当前n的奇异局势, 如果m比他大 拿走就行     //如果m比他小我们求出(x, n) 然后拿走m     {
            double c = (sqrt(5.00000) + 1) / 2;
            int x = n;
            double d = x / c;
            int y = n + d;
            if(m > y) cout << x << " " << y << endl;
            else
            {
                double x = double(n) * 2 / (sqrt(5.000000) + 1);
                cout << int(x) << " " << n << endl;
            }
        }
    }
        
    int main() {
        while(cin >> n >> m)
        {
            if(!(n + m)) break;
            if(check(n, m)) puts("0");
            else
            {
                puts("1");
                work();
            }
        }
        return 0;
    }
        
    

斐波那契博弈(Fibonacci Nim Game)

  • 一堆石子有nn个,两人轮流取.先取者第1次可以取任意多个,但不能全部取完。以后每次取的石子数不能超过上次取子数的22倍。取完者胜。给定nn,问先手必胜还是必败。

  • 结论:
    • (n)(fibonacci)数的时候,先手必败
  • 证明:

    技术图片

例题:

  • hdu_2516

  • #include<bits/stdc++.h> using namespace std;
    typedef long long ll;
    unordered_map<int, int> mp;
    ll f[50];
    void fib() {
        f[0] = f[1] = 1;
        for(int i = 2; i <= 50; i++)
        {
            f[i] = f[i - 1] + f[i - 2];
            mp[f[i]]++;
        }
    }
        
    int main() {
        int n;fib();
        while(cin >> n)
        {
            if(n == 0)break;
            if(!mp[n]) puts("First win");
            else puts("Second win"); //如果是fibonacci数, 则先手必败
        return 0;
    }
    

SG函数

  • (mex)运算:
    • 定义(mex(S))为不属于集合S的最小非负整数运算。
    • ?举个栗子: (S=1,2,3,mex(s)=0);
  • SG函数:
    • ?(SG)函数: 设对于每个节点x, 设从x出发有k条有向边分别到达节点(y1,y2,…,yk), 定义SG(x)函数为后继节点(y1,y2,…,yk)的SG函数值构成的集合再执行mex运算的结果。
    • 特别的, 整个有向图GG的SGSG函数被定义为有向图起点sSG函数值, 即(SG(G)=SG(s))
    • 有向图终点的SG函数为0。
  • 结论:
    • ?先手必败, 则该局面对应(SG函数=0)。反之必胜。

技术图片

例题
  • hdu_1524

    #include<bits/stdc++.h> using namespace std;
    const int maxn = 1e3 + 10;
    int n, num;
    int sg[maxn];
        
    int head[maxn], ver[maxn], nex[maxn], tot;
    void add(int x, int y) {
        ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
    }
        
    int GetSg(int x) {
        if(sg[x] != -1) return sg[x];
        bool vis[maxn];
        memset(vis, 0, sizeof(vis));
        for(int i = head[x]; i; i = nex[i]) // 扫描所有出边     {
            int y = ver[i];
            sg[y] = GetSg(y);
            vis[sg[y]] = 1; //所有出边的sg函数值     }
        for(int i = 0; i < n; i++)
            if(!vis[i]) return sg[x] = i; // mex运算     return 0;
    }
        
    void init() {
        memset(head, 0, sizeof(head));
        memset(nex, 0, sizeof nex);
        memset(ver, 0, sizeof ver);
        memset(sg, -1, sizeof sg);
        tot = 0;
    }
        
    int main() {
        while(cin >> n)
        {
            init();
            for(int i = 0; i < n; i++)
            {
                cin >> num;
                while(num--)
                {
                    int x; scanf("%d", &x);
                    add(i, x);
                }
            }
            while(cin >> num)
            {
                if(!num) break;
                int res = 0;
                while(num--)
                {
                    int x; scanf("%d", &x);
                    res ^= GetSg(x);
                }
                if(res) puts("WIN");
                else puts("LOSE");
            }
        }
        return 0;
    }
    
  • hdu_1536

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn = 1e4 + 10;
    int s[maxn], sg[maxn];
    int k;
        
    void init() {
        memset(sg, -1, sizeof(sg));
    }
        
    int GetSg(int x) {
        if(sg[x] != -1) return sg[x];
        bool vis[maxn]; memset(vis, 0, sizeof(vis));
        for(int i = 1; i <= k; i++)
            if(x >= s[i])
            {
                sg[x - s[i]] = GetSg(x - s[i]);
                vis[sg[x - s[i]]] = 1;
            }
        for(int i = 0; ; i++)
            if(!vis[i]) return sg[x] = i;
        return 0;
        
    }
        
    int main() {
        ios::sync_with_stdio(false);
        while(cin >> k)
        {
            init();
            if(k == 0) break;
            for(int i = 1; i <= k; i++) cin >> s[i];
            int num; cin >> num;
            while(num--)
            {
                int x, res = 0; cin >> x;
                for(int i = 1; i <= x; i++)
                {
                    int y; cin >> y;
                    res ^= GetSg(y);
                }
                if(res) cout << "W";
                else cout << "L";
            }
            cout << endl;
        }
        return 0;
    }
    

参考

【博弈论】关于三姬分金(五海盗分赃)的博弈论问题分析

李永乐老师对三姬分金视频讲解

ACM集训队讲解

以上是关于算法讲堂一:博弈论入门的主要内容,如果未能解决你的问题,请参考以下文章

大讲堂—MyBatis简介与入门

算法小讲堂之拓扑排序

10.5 博弈入门

算法小讲堂之平衡二叉树|AVL树(超详细~)

讲堂 | WebGL 入门

只用一行代码就能搞定,博弈论究竟是什么神仙算法?