【AcWing算法基础课】第四章 数学知识(未完待续)(3)

简介: 根据下面公式来预处理出等式右边的组合数的值,那么等式左边就可以用等式右边已经算过的值来进行计算(有点像dp)。

五、求组合数

核心模板

根据下面公式来预处理出等式右边的组合数的值,那么等式左边就可以用等式右边已经算过的值来进行计算(有点像dp)。

4309abeb7ced7fab2b8792124d6c11b7_057d047797a84d7dbe85a0752b6df535.png

//c[a][b]表示从a个苹果中选b个的方案数
for(int i=0;i<N;i++){
    for(int j=0;j<=i;j++){
        if(!j) c[i][j]=1;
        else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
    }
}


如下

c5a52b80be4d7c6e58d6414043807a4d_396299e50dd043b3bebf3a7e98743ed3.png

首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]

如果取模的数是质数,可以用费马小定理求逆元

typedef long long LL;
//快速幂模版
int qmi(int a,int k,int p){
    LL res=1%p;
    while(k){
        if(k&1) res=res*a%p;
        k>>=1;
        a=(LL)a*a%p;
    }
    return res;
}
//预处理阶乘的余数和阶乘逆元的余数
fact[0]=infact[0]=1;
for(int i=1;i<N;i++){
    fact[i]=(LL)fact[i-1]*i%mod;
    infact[i]=(LL)infact[i-1]*qmi(i,mod - 2,mod)%mod;
}


题目一

题目链接:885. 求组合数 I


5.1题目描述

54025ba5417cf060459e7fcbdc084c99_67c1ce81a9d340f2afec8c9db9e621de.png


5.2思路分析

利用模板1求解即可。


5.3代码实现

#include <iostream>
using namespace std;
const int N=2010,mod=1e9+7;
int c[N][N];
int n;
//求组合数
void solve(){
    for(int i=0;i<N;i++){
        for(int j=0;j<=i;j++){
            if(!j) c[i][j]=1;
            else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
        }
    }
}
int main(){
    cin>>n;
    solve();
    while(n--){
        int a,b;
        cin>>a>>b;
        cout<<c[a][b]<<endl;
    }
    return 0;
}


题目二

题目链接:

886. 求组合数 II


5.4题目描述

6718e7c7963105c91b1ae53703c0c1dc_07d463193c1c4694ab78279d280b50c6.png


5.5思路分析

利用模板2求解即可。


下图作者如图,侵删。

d0200490a5fcfe84ceaa958347a7c859_219f17b0d0c04f1bbcbcdadfa04bedd4.png

5.6代码实现

#include <iostream>
using namespace std;
typedef long long LL;
const int N=100010,mod=1e9+7;
int fact[N],infact[N];   //fact[i]存储i!%mod的值;infact[i]存储i!的逆元%mod的值
int n;
//快速幂模板
int qmi(int a,int k,int p){
    LL res=1%p;
    while(k){
        if(k&1) res=res*a%p;
        k>>=1;
        a=(LL)a*a%p;
    }
    return res;
}
int main(){
    cin>>n;
    fact[0]=infact[0]=1;    //0!%mod和其逆元%mod的值为1
    //预处理出fact[]和infact[]
    for(int i=1;i<N;i++){
        fact[i]=(LL)fact[i-1]*i%mod;            //求每个阶乘%mod的结果
        infact[i]=(LL)infact[i-1]*qmi(i,mod-2,mod)%mod;    //求每个阶乘的逆元%mod的结果
    }
    while(n--){
        int a,b;
        cin>>a>>b;
        cout<<(LL)fact[a]*infact[b]%mod*infact[a-b]%mod<<endl;    //按照组合数公式进行计算
    }
    return 0;
}


六、博弈论

NIM游戏

给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。

我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。

所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。

NIM博弈不存在平局,只有先手必胜和先手必败两种情况。

f4646bedefcbdf4938289115f9b65cce_e605b5251dc34c94bdf18bd35e04c775.png

定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0




题目一

题目链接: 891. Nim游戏


6.1题目描述

给定 n 堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。


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


输入格式


第一行包含整数 n。


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


输出格式


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


否则,输出 No。


数据范围


1≤n≤105,1≤每堆石子数≤109


输入样例:


2
2 3


输出样例:


Yes


6.2思路分析

计算所有数的异或值,如果值不为0,则先手必胜,否则先手必败。


6.3代码实现

#include <iostream>
using namespace std;
int n;
int main(){
    cin>>n;
    int res=0;
    while(n--){
        int x;
        cin>>x;
        res^=x;      //计算所有数的异或值
    }
    if(res) cout<<"Yes"<<endl;   //如果值不为0,则先手必胜
    else cout<<"No"<<endl;       //否则,先手必败
    return 0;
}


公平组合游戏ICG

若一个游戏满足:


由两名玩家交替行动;

在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;

不能行动的玩家判负;

则称该游戏为一个公平组合游戏。

NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。


有向图游戏

给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。

任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。


Mex运算

设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:

mex(S) = min{x}, x属于自然数,且x不属于S。


SG函数

在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:

SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})

特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。


有向图游戏的和

设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。

有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:

SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)


定理:

有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。

有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。


题目二

题目链接:893. 集合-Nim游戏


6.4题目描述

给定 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


6.5思路分析

详见代码注释。


6.6代码实现

#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N=110,M=10010;
int k,n;       //k为可以取石子的方案数(即一次取多少个)
int s[N],f[M];  //f[]存储每堆石子的sg值
//求石子数量为x的这一堆石子的sg值
int sg(int x){
    if(f[x]!=-1) return f[x];   //如果该值已经被算过则直接返回
    unordered_set<int> S;     //存放x各个后继结点的sg值
    //遍历k种取法
    for(int i=0;i<k;i++){
        int sum=s[i];          //sum为本次取法取多少个石子
        if(x>=sum) S.insert(sg(x-sum));    //如果可以取,则将取后的后继结点的sg值加入S
    }
    for(int i=0;;i++){
        //对S求mex,求出不在集合中的最小自然数
        if(!S.count(i)) return f[x]=i;
    }
}
int main(){
    cin>>k;
    for(int i=0;i<k;i++) cin>>s[i];
    cin>>n;
    int ans=0;
    memset(f,-1,sizeof f);
    for(int i=0;i<n;i++){
        int x;
        cin>>x;
        ans^=sg(x);    //n堆石子的sg的值异或起来不为0,则先手必胜
    }
    if(ans) cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
    return 0;
}


七、约数个数和约数之和

下图作者如图,侵删。

a2590e62a1581b20a78b2b9e93faa0ab_735ebb5286ea43e79a823460cbf0d58f.png

int范围内的数最多约数个数约为1600个

1ef5dd55bbee7dfd8b864804c996001d_3bdf9e940f374e719c6c497931cdc6d2.png

核心模板

如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)


题目链接:

870. 约数个数


7.1题目描述

29b122d3c98364c057883ecec49ad77b_bbb7632de1bc474e8486e386a4d4bd12.png


7.2思路分析

将每个数质因数分解,利用unordered_map进行存储所有pi及其指数,即每分解一个数,将分解后对应的pi的指数加上分解质因数的次数。


7.3代码实现

#include <iostream>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int mod=1e9+7;
int n;
int main(){
    cin>>n;
    unordered_map<int,int> hash;   //存储每个pi和其指数
    while(n--){
        int a;
        cin>>a;
        for(int i=2;i<=a/i;i++){   //注意循环从2~a/i
            while(a%i==0){
                hash[i]++;   //i的指数++
                a/=i;  
            }
        }
        if(a>1) hash[a]++;    //注意if位置
    }
    LL ans=1;
    for(auto i:hash) ans=ans*(i.second+1)%mod;
    cout<<ans;
    return 0;
}
目录
相关文章
|
3月前
|
存储 安全 算法
|
3月前
|
人工智能 算法 测试技术
【数学】【排序】【C++算法】3027人员站位的方案数
【数学】【排序】【C++算法】3027人员站位的方案数
|
3月前
|
算法 测试技术 C++
【动态规划】【数学】【C++算法】1449. 数位成本和为目标值的最大数字
【动态规划】【数学】【C++算法】1449. 数位成本和为目标值的最大数字
|
3月前
|
算法 测试技术 C++
【动态规划】【 数学】C++算法:514自由之路
【动态规划】【 数学】C++算法:514自由之路
|
3月前
|
存储 算法 Serverless
【C/C++ 数据结构】深入探索数据结构中算法复杂度:从C++和数学的视角
【C/C++ 数据结构】深入探索数据结构中算法复杂度:从C++和数学的视角
56 0
|
3月前
|
算法
双指针算法(acwing)疑难讲解
双指针算法(acwing)疑难讲解
29 0
|
2月前
|
算法 Java Go
斐波那契数列是一个非常经典的数学问题,在计算机科学中也经常被用作算法设计和分析的例子。
斐波那契数列是一个非常经典的数学问题,在计算机科学中也经常被用作算法设计和分析的例子。
|
1月前
|
算法 安全 网络安全
支付系统,网络安全06----支付安全---,机密性,加密算法,目前最流行的加密算法,AES加密算法,目前最流行的非对称加密算法RSA,对称加密和非对称加密的优缺点,非对称加密是基于非常复杂的数学算法
支付系统,网络安全06----支付安全---,机密性,加密算法,目前最流行的加密算法,AES加密算法,目前最流行的非对称加密算法RSA,对称加密和非对称加密的优缺点,非对称加密是基于非常复杂的数学算法
|
2月前
|
存储 人工智能 算法
程序与技术分享:Acwing算法笔记
程序与技术分享:Acwing算法笔记
|
2月前
|
算法 数据挖掘 定位技术
算法必备数学基础:图论方法由浅入深实践与应用
算法必备数学基础:图论方法由浅入深实践与应用