算法基础课—数学知识(六)容斥原理、博弈论

容斥原理

集合数为奇数——正好
偶数——负号
在这里插入图片描述

判断集合个数

在本题中,判断1-n中包含质数p1的个数为 n / p1,向下取整。
同时有可能存在同时是p1 和p2 的倍数,出现重复相加的情况,要减去,于是 n / (p1*p2)。
以此类推,每次要求的就是包含不同集合的情况下的个数。

程序设计思路

由于可能项较多,我们枚举所有可能项——一般用位运算来做
这样可以对应所有的选法,
从1- 2的n次方-1,将他们转换成二进制表示,0表示不选,1表示选,这样可以包含其中的所有情况,唯一要做的就是判断包含的集合个数,如果集合个数为奇数,则为正号,如果为偶数,则为负号
在这里插入图片描述

判断第k位是不是1, i >> k & 1

题目

给定一个整数 n 和 m 个不同的质数 p1,p2,…,pm。

请你求出 1∼n 中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。

输入格式
第一行包含整数 n 和 m。

第二行包含 m 个质数。

输出格式
输出一个整数,表示满足条件的整数的个数。

数据范围
1≤m≤16,
1≤n,pi≤109
输入样例:
10 2
2 3
输出样例:
7

模板

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 20;

int p[N];


int main()
{
    int n, m;
    cin >> n >> m;

    for (int i = 0; i < m; i ++ ) cin >> p[i];

    int res = 0;
    for (int i = 1; i < 1 << m; i ++ )
    {
        int t = 1, s = 0;
        for (int j = 0; j < m; j ++ )
            if (i >> j & 1)
            {
                if ((LL)t * p[j] > n)
                {
                    t = -1;
                    break;
                }
                t *= p[j];
                s ++ ;
            }

        if (t != -1)
        {
            if (s % 2) res += n / t;
            else res -= n / t;
        }
    }

    cout << res << endl;

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/53410/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

自己的代码

#include <iostream>
using namespace std;
const int N = 20;
int p[20];
typedef long long LL;
int main(){
    int n, m, res = 0;
    int i, j;
    cin>>n>>m;
    for(i = 0; i < m; i ++) cin>>p[i];
    for(i = 1; i < 1 << m; i ++){//1<<m 可以表示2的n次方
        int t = 1, s = 0;
        for(j = 0; j < m; j ++){
            if(i >> j & 1){
                if((LL)t * p[j] > n){
                    t = -1;
                    break;
                }
                t = t * p[j];
                s ++;
            }
        }
        if(t != -1){
            if(s % 2 == 0) res -= n / t;
            else res += n / t;
        }
    }
    cout<<res<<endl;
}

总结

容斥原理一般用来包含这多个集合的求个数
需要
1、集合构造
2、集合个数求解
3、对于重合部分的集合个数求解
这三步是看题目自己分析

博弈论

在这里插入图片描述
每个玩家行动的动作是一样的

有向图游戏
在这里插入图片描述

Nim模型

通常的Nim游戏的定义是这样的:有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
这游戏看上去有点复杂,先从简单情况开始研究吧。如果轮到你的时候,只剩下一堆石子,那么此时的必胜策略肯定是把这堆石子全部拿完一颗也不给对手剩,然后对手就输了。如果剩下两堆不相等的石子,必胜策略是通过取多的一堆的石子将两堆石子变得相等,以后如果对手在某一堆里拿若干颗,你就可以在另一堆中拿同样多的颗数,直至胜利。如果你面对的是两堆相等的石子,那么此时你是没有任何必胜策略的,反而对手可以遵循上面的策略保证必胜。如果是三堆石子……好像已经很难分析了,看来我们必须要借助一些其它好用的(最好是程式化的)分析方法了,或者说,我们最好能够设计出一种在有必胜策略时就能找到必胜策略的算法。

先手必胜和先手必败状态

定义P-position和N-position,其中P代表Previous,N代表Next。直观的说,上一次move的人有必胜策略的局面是P-position,也就是“后手可保证必胜”或者“先手必败”,现在轮到move的人有必胜策略的局面是N-position,也就是“先手可保证必胜”。更严谨的定义是:1.无法进行任何移动的局面(也就是terminal position)是P-position;2.可以移动到P-position的局面是N-position;3.所有移动都导致N-position的局面是P-position。

先手必胜状态(N-position)—— 可以走到某一个必败状态(存在我走的某一步,使得对方是必败状态)
先手必败状态(P-position)—— 走不到任何一个必败状态(我怎么走对方都是赢,对方走不到任何一个必败状态)

如何判断当前是先手必败还是必胜

将所有异或起来
在这里插入图片描述

(Bouton’s Theorem):对于一个Nim游戏的局面(a1,a2,…,an),它是P-position当且仅当a1a2an=0,其中表示异或(xor)运算。

证明

根据定义,证明一种判断position的性质的方法的正确性,只需证明三个命题: 1、这个判断将所有terminal position判为P-position;2、根据这个判断被判为N-position的局面一定可以移动到某个P-position;3、根据这个判断被判为P-position的局面无法移动到某个P-position。

第一个命题显然,terminal position只有一个,就是全0,异或仍然是0。

第二个命题,对于某个局面(a1,a2,…,an),若a1a2an!=0,一定存在某个合法的移动,将ai改变成ai’后满足a1a2ai’an=0。不妨设a1a2an=k,则一定存在某个ai,它的二进制表示在k的最高位上是1(否则k的最高位那个1是怎么得到的)。这时aik<ai一定成立。则我们可以将ai改变成ai’=aik,此时a1a2ai’an=a1a2ank=0。

第三个命题,对于某个局面(a1,a2,…,an),若a1a2an=0,一定不存在某个合法的移动,将ai改变成ai’后满足a1a2ai’an=0。因为异或运算满足消去率,由a1a2an=a1a2ai’an可以得到ai=ai’。所以将ai改变成ai’不是一个合法的移动。证毕。

根据这个定理,我们可以在O(n)的时间内判断一个Nim的局面的性质,且如果它是N-position,也可以在O(n)的时间内找到所有的必胜策略。Nim问题就这样基本上完美的解决了。

简单来说,
先手手里的一定不是0,则抛给后手的一定是0,
如果先手拿到的是0,则抛给后手的一定不是0
简单来说,就是如果我先手拿到的不是全0局面,我一定可以通过某种操作把他们变成0,抛给后手的一定是全0局面,所以队手必败
反过来,队手拿到的不是全0局面,他一定可以通过某种操作把他们都变成0,所以我必败

应用

使用Nim模型去求解问题的关键
在于简单判断当前是否为先手必胜状态还是先手必败状态
如果是先手必胜状态则,找到必胜策略。

台阶Nim游戏

题目

现在,有一个 n 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有 ai 个石子(i≥1)。

两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。

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

问如果两人都采用最优策略,先手是否必胜。

输入格式
第一行包含整数 n。

第二行包含 n 个整数,其中第 i 个整数表示第 i 级台阶上的石子数 ai。

输出格式
如果先手方必胜,则输出 Yes。

否则,输出 No。

数据范围
1≤n≤105,
1≤ai≤109
输入样例:
3
2 1 3
输出样例:
Yes

思路

根据样例,我们寻找最优策略,在怎么样的最优策略下,会使得对手一定是处于必败状态
我们寻找到最优策略,我们针对样例
在这里插入图片描述
如果用户从第三级台阶往下移,则我们将第一级台阶的下移移到地面,始终保持第一级台阶和第三级台阶的个数相等。
在这里插入图片描述
如果用户从第二级台阶往下移,即变为213,则用户将第一级台阶的下移到地面,始终保持1,3相等

如果用户继续从第三级下移,则我们从第一级下移,始终保持1,3相等。
在这里插入图片描述

找到如下情况的最优策略后,我们进行分析发现我们只需要经过操作后,保证奇数位台阶上的个数相等,然后把这个必败的局面扔给对手,对手则一定必败,所以我们只需要判断奇数位上的就可以了

于是我们判断奇数位台阶的个数
在这里插入图片描述
如果其异或等于0,说明必败。
如果不等于0,说明必胜。

模板

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int main()
{
    int n;
    scanf("%d", &n);

    int res = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        scanf("%d", &x);
        if (i & 1) res ^= x;
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/53528/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

代码

#include <iostream>
using namespace std;
int main(){
    int n,res = 0, i , x;
    cin>>n;
    for(i = 1; i <= n; i ++){
        cin>>x;
        if(i % 2) res ^= x; 
    }
    if(res) cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
}

集合—Nim

题目

给定 n 堆石子以及一个由 k 个不同正整数构成的数字集合 S。

现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 S,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式
第一行包含整数 k,表示数字集合 S 中数字的个数。

第二行包含 k 个整数,其中第 i 个整数表示数字集合 S 中的第 i 个数 si。

第三行包含整数 n。

第四行包含 n 个整数,其中第 i 个整数表示第 i 堆石子的数量 hi。

输出格式
如果先手方必胜,则输出 Yes。

否则,输出 No。

数据范围
1≤n,k≤100,
1≤si,hi≤10000
输入样例:
2
2 5
3
2 4 7
输出样例:
Yes

思想

这里的区别在于规定了每次操作的数量,所以如果起始石堆个数不同,则构建的图是可能不同的,于是就不存在说像之前,虽然是多堆,但每次的操作总能在一个大图中找到的情况,所以会存在多个图的情况
于是引入SG函数

游戏和的SG函数等于各个游戏SG函数的Nim和。这样就可以将每一个子游戏分而治之,从而简化了问题。
而Bouton定理就是Sprague-Grundy定理在Nim游戏中的直接应用,因为单堆的Nim游戏 SG函数满足 SG(x) = x。

Nim和 : 各个数相异或的结果
介绍SG 函数之前先介绍mex操作

mex —— 找到集合中不存在的最小的自然数

SG函数
x可以到y1-yk 有k个状态,所以SG(x) = 如下图所示
mex(SG(后继)。。。。)
在这里插入图片描述任何一种非0状态时一定有一种方式可以到0的,任何一种0状态是不存在方式可以到0
例子
在这里插入图片描述
在这里插入图片描述
有多张图可以走,玩家可以从任意一张图开始走,所以怎么判断他们是必败还是必胜,就把他们异或起来

如果最后异或的值为0,则必败,如果不等于0,则必胜。

模板

#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>

using namespace std;

const int N = 110, M = 10010;

int n, m;
int s[N], f[M];


int sg(int x)
{
    if (f[x] != -1) return f[x];

    unordered_set<int> S;
    for (int i = 0; i < m; i ++ )
    {
        int sum = s[i];
        if (x >= sum) S.insert(sg(x - sum));
    }

    for (int i = 0; ; i ++ )
        if (!S.count(i))
            return f[x] = i;
}


int main()
{
    cin >> m;
    for (int i = 0; i < m; i ++ ) cin >> s[i];
    cin >> n;

    memset(f, -1, sizeof f);

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/53562/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

代码

#include <iostream>
#include <unordered_set>
#include <cstring>
using namespace std;
const int N = 200, M = 1e5 + 10;
int s[N], f[M];
int n, m;
int sg(int x){
    int i;
    if(f[x] != -1) return f[x];
    unordered_set<int> S;
    for(i = 0; i < m; i ++){
        int sum = s[i];
        if(x >= sum) {
            S.insert(sg(x - sum));
        }
    }
    for(i = 0; ; i ++){
        if(!S.count(i)) return f[x] = i;
    }
}
int main(){
    int i, x, res = 0;
    cin>>m;//输入操作数
    for(i = 0; i < m; i++) cin>>s[i];
    cin>>n;
    memset(f, -1, sizeof f);
    for(i = 0; i < n; i++){
        cin>>x;
        res = res ^ sg(x);
    }
    if(res) cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
}

拆分Nim游戏

题目

给定 n 堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式
第一行包含整数 n。

第二行包含 n 个整数,其中第 i 个整数表示第 i 堆石子的数量 ai。

输出格式
如果先手方必胜,则输出 Yes。

否则,输出 No。

数据范围
1≤n,ai≤100
输入样例:
2
2 3
输出样例:
Yes

思路

题目意思一堆可以分成两个规模较小的堆,且两个新堆的石子总数可以大于取走的那堆石子数,同时新堆也可以等于0
结束:不能拆分的时候

如果有多个独立的局面的情况,可以将SG函数异或起来求值

sg函数的构造

这里a可以分成多个局面的情况,所以a对应的后继为(b1,b2)…(c1,c2)等等
根据定理
sg(b1,b2) = sg(b1) ^ sg(b2)
所以构造函数构造完毕
在这里插入图片描述

模板

#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>

using namespace std;

const int N = 110;


int n;
int f[N];


int sg(int x)
{
    if (f[x] != -1) return f[x];

    unordered_set<int> S;
    for (int i = 0; i < x; i ++ )
        for (int j = 0; j <= i; j ++ )
            S.insert(sg(i) ^ sg(j));

    for (int i = 0;; i ++ )
        if (!S.count(i))
            return f[x] = i;
}


int main()
{
    cin >> n;

    memset(f, -1, sizeof f);

    int res = 0;
    while (n -- )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/53564/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

自己的代码

#include <iostream>
#include <unordered_set>
#include <cstring>
using namespace std;
const int N = 110;
int f[N];
int sg(int x){
    int i, j;
    if(f[x] != -1) return f[x];
    unordered_set<int> S;
    for(i = 0; i < x; i ++){
        for(j = 0; j <= i; j ++){//到j==i就可以,因为后面都是颠倒的,是一样的
            S.insert(sg(i) ^ sg(j));
        }
    }
    for(i = 0;; i ++)
        if(!S.count(i)) return f[x] = i;
    
}
int main(){
    int n, res = 0, x;
    cin>>n;
    memset(f, -1, sizeof f);
    while(n --){
        cin>>x;
        res ^= sg(x);
    }
    if(res) cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
}

博弈论问题的总结

1、对于只有一种独立局面的问题
寻找最优策略,然后判断如果哪些异或为0的时候是必败情况
就把那些异或起来,就可以判断是必败还是必胜

2、对于有多种独立局面的问题
构造sg函数
画图,看一个状况的后继情况,然后判断sg函数
将所有局面的sg函数异或起来,判断是否等于或不等于0

疑问:1、如何判读那是1种独立局面还是多种独立局面
2、如何寻找那种最优策略?人为寻找还是?

博弈论的有很多理解还不是很清晰,之后需要再次梳理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值