zoukankan      html  css  js  c++  java
  • 博弈论小结

    最近学了一点博弈论。

    来写写blog造福人民(怕自己忘了)希望可以写出一篇比较好懂的文章,这里没有一些绕口的定义,也没有什么麻烦的代码,只有思维上的火花:


    先来讨论讨论什么是博弈论:

    博弈论就是指有若干个人进行一些对弈,并且窝们默认每个人都是最聪明的,不会失误,都可以找到当前的最优解,然后来寻找有没有哪个人有必胜/必败的的策略。


    前置芝士:

    窝们可以把一场博弈看成一颗树,以第一步为根,引出多叉。

    感谢 (Mital) 的指出,这里并不可以当成一颗树,需要当成一张有向无环图,但是为了方便在文字中区分它连向的边和连向它的边,所以还是用颗树来表示(其实是我懒

    对于每个节点,它的子树表示它可以如何递归下去;

    比如:

    0这个状态可以向1这个状态递推过去,也可以向2这个状态递推过去。

    窝们考虑什么时候0这个状态是必胜的?

    必胜是对于当前这个状态是必胜的,与是谁无关,赢的人只是处于一个胜的状态而已;

    这句话,我觉得是博弈论的灵魂,窝们每个必胜的状态都是从对手的上个必败状态推来的,必败也是同理。

    如上图,窝们当前是出于0这个状态,窝们想必胜,那么在下一个状态(对手选择)时,别人时必败的,这样窝们才能必胜。

    所以,窝们只要在当前这个节点的子树中寻找它的子节点是否存在子节点必败。

    如果,它的子节点中不存在必败的,那么这个状态就没有必胜的对策。

    反过来一样是成立的:如果它的子节点都是必胜的,那么当前这个状态就显然是必败的。

    结论:

    如果当前状态必输,那么这个人可以转换到的局面一定必赢;

    如果一种状态必赢,那么一定可以转换到一种必输的状态。


    最基础的博弈——巴什博弈:

    先讲讲什么是巴什博弈,有n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取 (m) 个。最后取光者得胜。

    窝们先不看这道题,窝们先考虑 (m = 2) 的情况,每次只能取(1/2),那么应该怎么做?这个应该还是很好想;

    窝们先考虑什么情况是必胜:

    显然,当你取完后还剩 (0) 个是必胜的,这个总不会有问题吧。这个时候在你取之前还剩 (1/2) 个。

    然后,什么情况是必败:

    当你取完还剩 (1/2) 的时候,别人是必胜的,你是必败的。这个时候在你取之前还剩 (3) 个。

    必胜:

    当你取完后还剩 (3) 个是必胜的,这个可以由前面的必败状态可以推出来(就换了个对象,我前面说过必胜是对于当前这个状态是必胜的,与是谁无关,赢的人只是处于一个胜的状态而已),这个时候在你取之前还剩 (4/5) 个。

    必败:

    当你取完还剩 (4/5) 的时候,别人是必胜的,你是必败的。这个时候在你取之前还剩 (6) 个,这也可以从前面那个必胜状态推出来。

    到这里窝们就可以找到规律了,这个结果是不断递归的。窝们每个必胜的状态都是从对手的上个必败状态推来的,必败也是同理。

    所以,当取前的数值为 (3 * k) 的时候为先手必败。

    其余的都是先手必胜。

    窝们再来分析分析 (m = 3) 的情况,只能取 (1/2/3) 的情况,同理我们还是来分析什么时候先手必胜,什么时候先手必败。

    必胜:

    当你取完后还剩 (0) 个是必胜的,这个总不会有问题吧。这个时候在你取之前还剩 (1/2/3) 个。

    必败:

    当你取完还剩 (3/2/1) 的时候,别人是必胜的,你是必败的。这个时候在你取之前还剩 (4) 个。

    必胜:

    当你取完后还剩(4)个是必胜的,这个不多BB。这个时候在你取之前还剩 (5 / 6 / 7) 个。

    必败:

    当你取完还剩(7/6/5)的时候,别人是必胜的,你是必败的。这个时候在你取之前还剩(8)个,这也可以从前面那个必胜状态推出来。

    所以,当取前的数值为 (4 * k) 的时候为先手必败。

    其余的都是先手必胜。

    再来看原题,窝们是不是可以大胆猜测:

    当取前的数值为 ((m + 1) * k) 的时候为先手必败。

    其余的都是先手必胜。

    这个就是正确的结论,接下来是理性的证明:

    窝们考虑对于每个状态,可以推向那里?

    显然,当你取完是 (0) 的时候是必胜的,此时你取之前的 (1)~(m)

    当你取完是 (1,2,3……m) 的时候是必败的,此时你取之前的 (m + 1)

    当你取完是 (m + 1) 的时候是必胜的,此时你取之前的 ((m + 2)) ~ ((2 * m + 3))

    当你取完是 (m + 2,m + 3, m + 4 …… 2 * m + 3) 的时候是必败的,此时你取之前的 (2 * m + 4)

    到这里,窝们就可以找到规律了;

    (n = (m + 1) * k)的时候先手是必败的

    其他的都是先手必胜状态


    nim游戏

    这是个比较经典的博弈论的板子题,题目大概的意思是:有 (n) 堆数,每堆有 (s_i) 个,每次可以且仅可以取一堆中的若干个数,求问先手有没有必胜策略。

    这是窝做博弈论的题的一些小技巧:先研究一些比较显然的必胜/必败策略,比如说,我们要得到(0)这个数,那么当你取完时还剩0个,你就显然是胜的。

    然后再通过最后的这个显然的必胜状态,往前递推找出其余的必胜状态。

    显然博弈论是必然会出现循环节的,因为它是一种不断递归求解的过程,每次都可以取到当前这个循环节上的必胜状态,并且让你的对手能达到的下个状态全部都是必败状态,那样你就可以稳了。

    窝们来分析nim游戏这道题,窝们还是先考虑什么状态下是必胜的:

    先讲结论,如果 (s_1) ^ (s_2) ^ (s_3) ^ …… ^ (s_n) == (0) 就是先手必败;其余都是先手必胜。

    还是先考虑他的子问题:当 (n == 1) 的时候:这个显然是先手必胜,因为直接全部取掉就好。

    这里是不是 (s_1) != 0,也可以理解为 (s_1) ^ 0 = s;

    (n == 2) 的情况:窝们就考虑一堆的石头和另一堆相不相等

    如果一堆的石头和另一堆相等,那么不论先手取什么,后手都只需要跟着先手取相同的个数就行了。这是先手必输。

    此时窝们可以考虑为: (s_1) ^ (s_2) = (0) ;

    如果一堆的石头和另一堆不相等,那么先手就在多的那一堆里面取出两堆直接的差值;

    然后就转移到了上面那个状态,不过对象换了,这也符合窝的想法:必胜是对于当前这个状态是必胜的,与是谁无关,赢的人只是处于一个胜的状态而已。这是先手必胜。

    此时可以理解为 (s_1) ^ (s_2) = (s) ( (s) 为非零数);

    现在,窝们再来把这个问题扩展到 (n)

    显然,当我取完后每堆石头的数量都为 (0) 的时候,我是胜的。

    窝们可以得出最终获胜式子为: (0) ^ (0) ^ (0) ^ (0) ^ (0) ^ (0) = (0) ;

    窝们再来看看它的最初的状态: (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (s) ;

    窝们下一步期望达到的状态应该是 (news_1) ^ (news_2) ^ (news_3) ^ (news_4) = (0) ;

    窝们再来想想,如果任意的一个状态 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (s) 可以转移成 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (0) ;

    那么它的下一个状态一定会是 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (ss)

    因为取一堆数的异或和为0,并且窝们修改且只修改其中的一个元素,是无法维持 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (0) 的。

    所以你的对手(下一个状态)肯定又会变成一个 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (news) 的形式;

    如果你可以继续把 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (news) 变成这个 (news_1) ^ (news_2) ^ (news_3) ^ (news_4) = (0)

    那是不是就可以把这一次一次操作当一个以2为循环周期的循环节,每次前者取到的值为 (x) (注意这里的 (x) 并不是一个固定的数,(x) 是一个抽象的概念,你们可以理解为非零数),后者取到的值为 (0)

    最后取到 (0) ^ (0) ^ (0) ^ (0) ^ (0) ^ (0) = (0) 这个式子的一定就是你,因为你的对手根本就没有取到过异或和为0的状态,所以你是必胜的。

    接下来,来讲一下为什么对于任意的一个状态 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (s) 可以转移成 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (0) ;

    窝们再会过来分析这个等式:(s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (s)

    窝们来分析 (s) 这个数的由来,窝们肯定可以找到 (s) 这个数的最高位(第 (k) 位)为 (1) 是吧, (0) 是做不了第一位的。

    既然窝们可以找到数 (s) 的第 (k) 位为 (1) ,那么这一位是怎么来的?在异或运算中是不存在进位的,所以这个 (1) 肯定是第 (k) 位这一位上面算出来的,又因为它是异或,所以在前面的 (s_1)(s_n) 中必然存在奇数个 (1) ;

    而我们只需要找到一个数这一位(第 (k) 位)为 (1) 就行了,多个的话只需要其中一个就行了,另外几个也是可行的方案。

    现在,窝们找到了 (s_i) 的第 (k)(1) ,窝们的目的是要使 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (s) 转移成 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (0) ;那么在异或的意义下如何把 (s) 变成 (0) 嘞?

    窝们都知道一个数异或上它自己,答案肯定是 (0) ;那么窝们就把等式两边同时异或上 (s) ,等式右边不就是 (0) 了吗?

    等式化成了这个亚子: (s) ^ (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (s) ^ (s) = (0).

    我接下来要做的是不是把等式左边的 (s) 与一个 (s_i) 相融合,因为这个问题窝一次只可以变小一堆,所以 (s) ^ (s_i) 要得到一个小于 (s_i) 的新数。

    那么,窝们在这之前是不是找到了一个数在 (s) 的最高位(第 (k) 位)等于 (1) ,可以设它位 (s_k) ,它的第 (k) 位为 (1)

    如果 (s) ^ (s_k) 是不是会对他们的每一位进行异或运算?

    如果 (s_k) 的位数要高于 (s) 那么它前面高出的那几位是不会变的,在第 (k) 位上,(s_k) ^ (s) 得到的答案应该是 (0)

    又因为 (s_k) 的前几位不变,第 (k) 位从 (1) 变成了 (0) ,所以它是变小了的,这满足了窝们每次从一堆数中取出部分(将原数减小)的要求。也满足了将任意 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (s) 转移成 (s_1) ^ (s_2) ^ (s_3) ^ (s_4) = (0)

    如何,你不断的将异或和变成 (0) ,别人只能被动的把异或和变成 (s) ,又因为最后获胜的时异或和要为 (0) ,所以不断取 (0) 的时必胜的。

    如果将每堆数相互异或得到的和为 (0) ,则先手必败。

    如果将每堆数相互异或得到的和不为 (0) ,则先手必胜。

    这个代码也很好写:

    #include<bits/stdc++.h>
    
    using namespace std;
    
    const int dx[5] = {0, 1, -1, 0, 0};
    const int dy[5] = {0, 0, 0, 1, -1};
    
    //#define XRZ
    //#define int long long
    #define maxn 10010
    #define maxm
    #define ll long long
    #define mian main
    #define inf 0x3f3f3f3f
    #define debug(x) printf("now here is %d
    ", x);
    #define file(a) freopen(#a".in", "r", stdin), freopen(#a".out", "w", stdout);
    #define Rep(x, a, b) for(int x = a; x <= b; ++ x)
    #define Dep(x, a, b) for(int x = a; x >= b; -- x)
    #define Next(x, u) for(int i = head[u]; i ; i = e[i].nxt)
    int t, n, a[maxn];
    signed mian(){
    scanf("%d", &t);
    while(t --){
    scanf("%d", &n); int ans = 0;
    Rep(i, 1, n) scanf("%d", &a[i]), ans ^= a[i];//维护其异或和
    if(ans == 0) puts("No"); else puts("Yes");
    }
    return 0;
    }
    

    ps:都是一些最近学的时候的理解。蒟蒻刚开始学如有错误,请各位大佬指出。还会在自己学的同时放一些自己的理解上来。如有什么不懂,可以私信我,我在对其进行更改。

  • 相关阅读:
    JAVA WEBSERVICE服务端&客户端的配置及调用(基于JDK)
    An internal error occurred during: "Launching New_configuration"
    Android 创建虚拟机时“提示no system images installed for this target”
    [转] 传说中的WCF(2):服务协定的那些事儿
    [转] 传说中的WCF
    python的包管理
    python入门常用方法(转json,模拟浏览器请求头,写入文件)
    python读写数据篇
    python跳坑手记
    python爬虫入门篇
  • 原文地址:https://www.cnblogs.com/Flash-plus/p/12028298.html
Copyright © 2011-2022 走看看