第四章 数学知识(四)——容斥原理、博弈论

前言

算法基础课:
第四章 数学知识(四)。

共5题,知识点如下。

容斥原理:
AcWing 890. 能被整除的数。
博弈论:
AcWing 891. Nim游戏、
AcWing 892. 台阶-Nim游戏、
AcWing 893. 集合-Nim游戏、
AcWing 894. 拆分-Nim游戏。

容斥原理

其实跟求面积区别不大
|S|表示集合元素个数
在这里插入图片描述

里面一共有2n -1项,所以时间复杂度为2n

在这里插入图片描述

右侧2n 表示每个数都有选或不选的情况,n个数,即有2n个方案

在这里插入图片描述
第k个的

在这里插入图片描述

AcWing 890. 能被整除的数

思路:
算每一个集合的时间复杂度为O(k)
在这里插入图片描述
p能被整除和不能被整除时,1-n 中 p 的倍数分别对应的个数
在这里插入图片描述
把 i 看成 n 位的二进制数,把 1 - 2n-1 的所有数都看成二进制数
【二进制数,1为选中,0为未选】
就可以用 n 位二进制数来表示各种选法
在这里插入图片描述

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 20;

int n, m;
int p[N];   

int main() {
    // 读入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++) { //  1 << m, 即2的m次方
        int t = 1, cnt = 0; // t:当前所有质数的乘积。cnt:当前i包含几个1【当前选法有几个集合】
        for (int j = 0; j < m; j++) {
            if (i >> j & 1) {   // 当前位是1
                cnt++;
                if ((LL)t * p[j] > n) { // p很大,容易超出范围
                    t = -1;
                    break;
                }
                t *= p[j];
            }
        }
        // 每轮乘积都会算进去即
        if (t != -1) { // 没超过n,就需要奇数集合,减去偶数集合
            if (cnt % 2) res += n / t;
            else res -= n / t;
        }
    }
    cout << res << endl;
    
    return 0;
}

算法相关的数学问题基本都是离散数学,有时间去补补

想提高敲代码的速度的话,敲一道题目,多敲几遍,敲个五六遍就快了,
熟能生巧

简单博弈论

模版等

NIM游戏 —— 模板题 AcWing 891. Nim游戏

在这里插入图片描述

公平组合游戏ICG

在这里插入图片描述

有向图游戏

在这里插入图片描述

Mex运算

在这里插入图片描述

SG函数

在这里插入图片描述

有向图游戏的和 —— 模板题 AcWing 893. 集合-Nim游戏

在这里插入图片描述

定理

在这里插入图片描述

题目

模板题 AcWing 891. Nim游戏

思路:

异或,相同为0,不同为1

此时的式子是下一步状态为0或者不为0
在这里插入图片描述
1、已经全部为0了,异或上也为0,则必败了
在这里插入图片描述

2、当前状态为异或上所有数。值为x,拿走一颗石子,
可以将其转化到等于0的状态
在这里插入图片描述
在这里插入图片描述
ai ^ x一定小于ai,如下
在这里插入图片描述
取走 ai - ai ^ x 后,则ai位置变为ai^x
a1异或到an值为x,则x^x = 0,即下一步一定为0,即必胜
在这里插入图片描述
3、当前状态为异或上全部值为0,则不管怎么拿,下一步一定不为0
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
如果当前是②状态,则拿到最后,后手一定为0,即先手必胜
如果当前是③状态,则拿到最后,先手一定为0,即先手必败

原因:②状态拿完后一定变为③状态,③拿完也一定会变成②状态

先手必胜状态:可以走到某一个必败状态【对手必败】
先手必败状态:走不到任何一个必败状态【对手必胜】

#include <iostream>
#include <algorithm>

using namespace std;

int main() {
    int n;
    int res = 0;

    scanf("%d", &n);
    while (n--) {
        int x;
        scanf("%d", &x);
        res ^= x; 
    }

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

AcWing 892. 台阶-Nim游戏

样例

3
2 1 3
Yes

思路:
先手必胜,保持拿完后,台阶1和3永远相等,让对手优先看到0-0,对手必败
在这里插入图片描述
在这里插入图片描述
一般情况
在这里插入图片描述
所有奇数台阶异或结果x不为0,则必胜

可以把奇数台阶上的石子看成是一个经典nim游戏

原因:不为0的时候,经典nim游戏中证明过,一定有一种方式可以拿完石子后,剩下所有奇数异或结果x为0
抛给对手后,x就等于0了

如果对手拿偶数台阶放到奇数,则顺次将其移到下一个偶数台阶,操作完后奇数台阶石子是不变的,则对手局面x依然是0

如果对手拿奇数,那么拿完后x一定不是0,我依然可将其弄为x为0的局面给对手
使对手面对的奇数台阶上永远是0,则对手必败

如果在我这时x=0,则对手也可以让我永远面对x=0的情况,即必败

有的事一开始就注定了,知道结果了,无法改变,就是这样

#include <iostream>
#include <algorithm>

using namespace std;

int main() {
    int n;
    int res = 0;

    scanf("%d", &n);

    for (int i = 1; i <= n; i++) { // 注意,台阶从1开始
        int x;
        scanf("%d", &x);
        if (i % 2) res ^= x; // 所有奇数结果异或和
    }

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

}

AcWing 893. 集合-Nim游戏

mex(S):当前状态不能到的,最小的自然数是多少

如图

能到0能到1就是2,

只能到0就是1,

能到1不能到0就是0

能到0能到1能到2就是3

在这里插入图片描述
如果当前
SG(x) = 0,则为必败
SG(x) != 0,则为必胜
SG(x) != 0时。x一定可以到0
任何一个非0的状态都可以到0
先手为非0的话则一定可以保证后手为0
后手不管怎么走一定变成非0
即先手可以保证自己永远是非0,对手永远是0

在这里插入图片描述
有n个图可以选择,证明方式与Nim游戏的一样
在这里插入图片描述
可以通过把每个有向图的起点值求出来并且异或,判断是否为0
如果是0说明必败,是1必胜
在这里插入图片描述
可以通过这样的方式,求出来每一堆石子表示的状态图的SG的值,然后把他们异或起来,如果是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]; // s表示sg的个数,f表示sg的值

// 用记忆化搜索来做
int sg(int x) {
    // 用数组来存一下表示某个状态是否被算过,被算过就直接返回
    // 保证只被搜索一次,时间为100*10000 = 1e6
    if (f[x] != -1) return f[x];
    // 用哈希表来存所有可以到的局面
    unordered_set<int> S;
    for (int i = 0; i < m; i++) {
        int sum = s[i]; 
        // 当前数的个数大于sum才可以取石子
        // 存入单个图内每个点的sg值
        if (x >= sum) S.insert(sg(x - sum));
    }
    // 判断下集合中不存在的最小自然数是多少
    for (int i = 0; ; i++) {
        if (!S.count(i)) {
            return f[x] = i;
        }
    }
    
}

int main() {
    // 读入k个数和k个可进行的操作
    cin >> m;
    for (int i =0 ; i < m; i++) cin >> s[i];
    
    // 读入每一堆的石子个数
    cin >> n;
    
    memset(f, -1, sizeof f);
    
    // 将每堆石子的sg值进行异或
    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;
}

AcWing 894. 拆分-Nim游戏

一般求SG函数使用的是记忆化搜索,即dp

定理本身不好理解,但用起来特别简单

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

using namespace std;

const int N = 110;

int f[N];

int sg(int x) {
    if (f[x] != -1) return f[x]; // 如果f[x]算过了,直接返回
    
    unordered_set<int> S; // 存储当前每个局面拆分后能到的局面
    for (int i = 0; i < x; i ++) // 一堆石子最多不能超过原来的
        for (int j = 0; j <= i; j++) // 另一堆石子最多跟第一堆一样[避免重复];其实 j < x也可以
            S.insert(sg(i) ^ sg(j));// 算出每堆石子拆分后的sg值
    
    // mex操作
    // 找到集合当中不存在的一个最小的自然数
    for (int i = 0; ; i++) 
        if (!S.count(i)) 
            return f[x] = i; // 不存在,就返回i
}

int main() {
    int n;
    scanf("%d", &n); // cin >> n;
    
    memset(f, -1, sizeof f); // 所有sg值都是大于0的,用-1表示没算过
    
    int res = 0;
    for (int i = 0; i < n; i++) {
        int x;
        scanf("%d", &x); // cin >> x;
        res ^= sg(x); // res^当前石子的sg值
    }
    
    if (res) puts("Yes"); // 整个局面的sg值不是0,先手必胜
    else puts("No");
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值