考研机试题收获——数论基础

目录

一、基础方面

一、stl:sort()

二、stl:数学函数 

三、数据类型

二、约数

一、求一个数约数的集合—试除法

二、求1~N每个数的正约数集合—倍数法

 三、高精度求最大公因数

三、质数

一、判断一个正整数是否为质数

二、对一个数进行质因数分解

三、欧拉筛

 四、数论分块

五、欧拉函数

六、费马小定理—乘法逆元

七、第二类斯特林数

八、卡特兰数

九、莫比乌斯反演

X、数学思考

一、二分查找


对于数学问题,需要特别严谨,不能随便更改数据,认为正确。

一、基础方面

一、stl:sort()

在定义sort()的比较函数时,比如:

bool cmp(int a,int b){
}

        cmp返回true第一个在前面,cmp返回false第二个在前面。sort()使用的是快速排序,不能保证原顺序保持不变,如果需要原顺序保持不变,那就需要一个下标比较。

bool cmp(int & a,int& b){
    return a.pos<b.pos;//a.pos小的时候返回true,a放在前面
}

二、stl:数学函数 

需要头文件#include<cmath>或#include<math.h>(万能头搞定)

sqrt():求平方根。浮点型 

int num;
num=sqrt(num);//赋值给整型,向下取整

stl 常用数学函数_std反正切函数-CSDN博客

三、数据类型

①在有些题中,要求结果模去1e9+7,你可能会认为此时不需要开long long因为结果一定在1e9+7内,但是如果用两个1e9的int做乘法,其中间结果会爆int类型,导致答案错误。所以模1e9的问题,最好用long long 一定不会超。

②float t=sqrt(n), t*t==n是成立的,但是int(t)*t==n也是可能成立的,因为n的取值可能刚好使得t无限趋近于int(t),所以我们要做的时候int(t)*int(t)==n才是最严谨的。

③取模运算:

        a%b,如果a是负数,那么结果会是负数余数。需要余正数必须这样写(a+b)%b

                   如果a是正数,那么结果是正数余数。

④类型转换:

long * int ,int会自动转换成long,然后与long运算,结果为long。

(1)long(num)*long(result[0]*result[1]);

(2)long(num)*long(result[0])*long(result[1]);

(3)num*result[0]*long(result[1]);

(4)num*long(result[0])*result[1];

上面的(1)问题在于,result[0]和result[1]两个都是int类型,结果会先爆掉int,然后再转换成long

(2)对的

(3)乘法按顺序进行,num和result[0]是两个int类型,会爆掉,然后再转成long与之后的运算

(4)乘法按顺序进行,result[0]是long类型,num转换成long,运算结果还是long,接着result[1]转换成long,与(2)的结果是一样的。

⑤小心int,用long long!:

#include<bits/stdc++.h>
using namespace std;
#define INF 998244353
int main(void){
    ios_base::sync_with_stdio(0);cin.tie(0);
    int n;
    cin>>n;
    long long ans=0;
    for(int i=0;i<n;++i){
        int x,y;
        cin>>x>>y;
        x=y-x;
        int c=1;
        long long temp=0;
        while(x!=1){
            temp=INF/x;
            x=(temp+1)*x%INF;
            c=(c*(temp+1))%INF;
        }
        c=c*y%INF;/****************注意两个int相乘*****************/
        ans=((ans+1)*c)%INF;
    }
    cout<<ans;
    return 0;
}

x和y<=1e9,你自认为c和ans都不可能爆int,但实际上确实不会,但问题出现在,运算的中间过程爆int。比如c=c*y%INF,c和y都是int型,y=1e9,c=10就会爆掉int,这样写才对:

c=long(c)*y%INF,中间过程计算时其中一个数转成long long!

别给自己添麻烦,别给自己增加错误的可能,定义long long y,long long c!!因为有些竞赛看不到对错,你交了一个int  你自己还不知道中间爆了,多可惜!

二、约数

对于任何一个数N,若其有约数d,则必然有N/d也是N的约数。

当d=N/d时(或者满足d<N/d的最大值),则d=eq?%7B%5Csqrt%5B%5D%7BN%7D%7D,即当d<=eq?%7B%5Csqrt%5B%5D%7BN%7D%7D是N的约数时,必然有N/d>=eq?%7B%5Csqrt%5B%5D%7BN%7D%7D是N的约数。

则约数总是成对出现的,仅当sqrt(N)*sqrt(N)=N时,此时仅算一个

一、求一个数约数的集合—试除法

时间复杂度:O(eq?%7B%5Csqrt%5B%5D%7BN%7D%7D)

只需要判断1~eq?%7B%5Csqrt%5B%5D%7BN%7D%7D是不是N的约数即可。对于eq?%7B%5Csqrt%5B%5D%7BN%7D%7D我们特殊处理。

原因在于:除了根号N这个约数之外,其他约数都是成对存在的(而这个成对必然是在根号N的左右),只需要判断到根号N就行。

#include<bits/stdc++.h>
using namespace std;
int main(void){
    int num;
    cin>>num;
    vector<int> factor;
    float j=sqrt(num);
//求根号返回值是float,因此我们需要用浮点型存储,不然丧失精度,循环用小于号会出现问题
//根号N的取地板,(根号N-1)*(根号N-1)=N-2*根号N+1 一定不是N的约数。
    for(int i=1;i<j;++i){
        if(num%i==0){//我们限制i在根号N的取地板之下,必然保证了成对出现
            factor.push_back(i);
            factor.push_back(num/i);
        }
    }
    if(int(j)*int(j)==num) factor.push_back(j);
//对于根号N 特殊处理,浮点数要转换成整型,不然任何数还是等于!
    for(int i=0;i<factor.size();++i){
        cout<<factor[i]<<' ';
    }
    return 0;
}

二、求1~N每个数的正约数集合—倍数法

        需要求解1~N中所有数的正约数集合,我们可以采用倍数法,如果我们对每个数采用试除法,时间复杂度为O(Neq?%7B%5Csqrt%5B%5D%7BN%7D%7D),但是如果我们采用倍数法,则只需要O(NlogN)。

对于每一个数d<=N,它能作为谁的约数?d,2d,3d,4d,地板(N/d) * d,(一共N/d次保存)

从1开始遍历到N,则时间复杂度:

N/1+N/2+N/3+N/4·······+N/N=O(NlogN).

vector<int> factor(500010);
for(int d=1;d<=N;++d)
    for(int i=1;i<=N/d;++i){
        factor[d*i].push_back(d);
    }

 三、高精度求最大公因数

考虑最大公因数可以,用质数表示法来考虑。

①当a==b时,gcd(a,b)=a;

②当a和b均为偶数时,gcd(a,b)=2*gcd(a/2,b/2)

③当a是偶数,b是奇数时,根据质数表示法可以发现,此时b一定没有因数2,a和b的最大公因数在a的质数表示去掉2的部分里,因此gcd(a,b)=gcd(a/2,b)

④当a和b均为奇数时,将a和b的质数表示法的最大公因数部分拿出来,则

a-b的因数包含a和b的最大公因数,如果a-b与b还有其他公因数,则b含有该公因数,a-b含有该公因数,则a也含有该公因数,与该公因数不在最大公因数里矛盾,因此a-b与b的最大公因数就是a和b的最大公因数,gcd(a,b)=gcd(a-b,b) //a>b

那么我们在求高精度最大公因数时,可以采用上述方法,逐渐减小两数,直到两数相等。其中我们需要记录的是第②步的2的个数,这样在求出来最后的公因数时,还应当乘以若干个2才是答案。(此时当a==b时,退出递归,然后计算若干个2和a的乘积,得到原始a和b的最大公因数)

三、质数

一、判断一个正整数是否为质数

对于任何一个数n只能有以下六种情况:

n=6m

n=6m+1

n=6m+2

n=6m+3

n=6m+4

n=6m+5

我们可以对一个数模6,除了2和3之外,当其余数为0,2,3,4时,它必然不是一个质数。当它余数为1或5的时候它可能是一个质数。这样我们先筛选出了一部分可能。

bool Is_prime(int num){
    if(num==1) return false;
    if(num==2||num==3) return true;
    if(num%6!=1&&num%6!=5) return false;
    if(num%2==0||num%3==0) return false;
    int j=sqrt(num);
    for(int i=5;i<=j;i+=6){
        if(num%i==0||num%(i+2)==0){
            return false;
        }
    }
    return true;
}
//试除法
//这里判断一个数是否为质数,只需要看看有没有质因子,因此只需要除以可能的质数看看是否余数为0,
//并且由于因数成对出现,则只需要判断到 i*i<=num即可
//在2~根号num中没有因数则必然根号num~num-1也没有。

二、对一个数进行质因数分解

        一个数N可以写成其质因数分解的形式,我们从小到大遍历质数,遇到是N的因数的质数,则将其记录,并用N除尽该质数,记录次数,然后除尽后的数继续去找它的质因数。由于质因数也是因数,则也只需要遍历到需要找质因数的数的平方根即可。

        我们从2开始遍历,一旦N%i==0,则必有i是质数,因为,我们将N进行质因数分解,我们可以发现能整除N的最小值(不考虑1)是N的最小质因数,当我们将这个最小质因数从N中除尽之后,能整除新的N的也是其最小质因数!所以当N%i==0时,必然有i是质数。

        当然,我们会发现,当你除到最后一个质因数的时候,并且最后一个质因数的次数为1,一定不会再有N%i==0,因为i等于N的时候才能被除尽,而N*N<=N不成立,因此会退出循环,所以N>1是必要的。

vector<int> ans;
vector<int> nums;
int N;
for(int i=2;i*i<=N;++i){//N不断减小,i不断增大
    if(N%i==0){//≤根号N 的一个质因子i
        ans.push_back(i);
        nums.push_back(0);
        while(N%i==0) {nums[nums.size()-1]++;N/=i;}
//把该质因数除尽,然后求Ni的质因数,它的最小质因数一定比当前i大~ 所以仍然遍历即可。
    }
}
if(N>1) {  
//14=2*7  
//如果N在除尽了所有≤根号N的质因子之后不等于1,则剩下的这个N一定是一个质数,因为小于等于根号N的质因子都被筛选完了,如果这个数不是质数,则一定存在小于等于根号N的质因子,不然这个数只有两个>根号N的因数,这两个因数的乘积必然大于N,矛盾,因此这个数是质数,所以这一定是大于根号N的一个质数
//大于根号N的质数只能有一个,并且用之前的循环可能筛选不掉,所以需要最后的一次判断。
    ans.push_back(N);
    nums.push_back(1);
}

三、欧拉筛

在埃氏筛法:从2开始遍历,将2的所有倍数标记,

接下来用下一个遍历,如果一个数没有被标记,那么这个数一定是质数,原因在于这个数的最小质因数一定比它小,而比它小的质因数已经标记了它的所有倍数,而我没被标记说明,没有任何一个质数是我的因数,我也是质数。

欧拉筛(线性筛):时间复杂度O(n)

在埃氏筛法的基础上,我们只用一个数的最小质因数来标记该数。

算法思路如下:

(1)从2开始到n遍历数i,

如果数i没有被标记没有被标记这个数一定是质数,并且质数也一定不会被标记,我们把这个没有被标记的数i放入到质数集的末尾中。

(2)从最小质数开始遍历当前质数集,记当前质数为p:

标记所有i*p,一旦遇到i%p==0,则这个数不是质数且将其标记,并退出该循环。用最小质数标记了数字i。或者当i*p>N时退出循环,因为这个时候超出了标记范围,之后都会超出。

为什么一定会被最小质数标记?

对于任意一个数i=p*j而言,p是其一个质因子,由于质因子一定会被放入质数集中,且p小于i,因此在循环(1)数字为i时,(2)中一定会遍历到p,由于j比i小,因此之前p*j这个数已经被标记。不会出现没有被标记的情况吗? 不会,因为p是i的最小质因数,则j>p,p已经被放入质数集中,且j中不可能含有比p小的质因数。因此在(1)遍历数字为j时,质数集遍历到p时,循环(2)都不会退出,因为j%p==0是不一定不成立的(如果j%p==0成立,则也会先标记j*p再退出),j%p0==0(p0<p)是不成立的(因为p是i的最小质因数如果成立,则存在i的更小质因数p0)。因此任何遍历到的数字 i 一定会被标记,而且会被其最小质数标记。当出现i%p==0成立时,退出循环即可,不会出现问题,比如即使是这样退出循环,i也会被标记。

由于每个数只标记一次,则时间复杂度是O(n)的。

代码:

vector<int> prime;
vector<int> notp(N+1);
notp[1]=1;//初始化为0,因此默认一个数是质数,不是质数用1标记
for(int i=2;i<N;++i){
    if(!notp[i]) prime.push_back(i);
//如果一个数没被标记,其一定是质数,不是质数的一定被其最小质因数标记了。
    for(int j=0;j<prime.size();++j){
        if(i * prime[j]>N) break;//范围超限,之后都超限,直接不管了
        notp[i*prime[j]]=1;//这个数一定不是质数
        if(i % prime[j]==0) break;//prime[j]是i的最小质因数! 退出咯
    }
}

 四、数论分块

6e37ed18e42f49aeb6f16ca628cad347.png

b2033b1411e94d01b98bc2574a3ddfb1.png12f74c7f65984980a5c4860c83821b0c.png

在求这个问题时,会存在很多相同的商,我们可以对相同的商只求一次,将整数1~n 通过不同的商划分为不同的区间,而同样的结果划分在同一个区间,这就是一个典型的数论分块问题。

我们来思考一下,如果我们不向下取整,那么我们求出来的n/m,这样的值就是带小数点的,在同样一样整数部分,他们在向下取整时被分为一类记为区间x,将该整数部分记为商q。

我们考虑n/q向下取整得到的值什么?一定是在该整数部分,且靠近小数部分是0的那个数,因为,在该区间x内,n除以这里的数,得到的商越小,说明这个数越大,当刚好整除时,这个数达到该区间x内最大的极限,所以在一个连续的i区间内,该用n除以该区间内的商,可以得到一个数,这数向下取整,一定是该i连续区间的商为该数的最大值。因此可以通过左区间数得到区间最右端的数,同时可以得到比该区间小的区间最右端的数。(不能得到比该区间大的,因为比该区间大商并不一定只大1,可能大很多,也就是说,在达到右边区间的商时,永远无法跨出本区间的右端点)

不过 可以用商找比自己小的区间,用除数直接找比自己大的区间。

实际上,由于无论如何i是连续的,可以用本区间的任何一个值,找到同一个商的最大值i,i+1就是下一个区间的第一个值了。

用这个商-1,可以去求比这个区间商小的,区间中最大的数。

五、欧拉函数

欧拉函数的性质:

①对正整数n,欧拉函数eq?%5Cvarphi%28n%29是少于或等于n的数中与n互质的数的数目。

因此eq?%5Cvarphi%281%29%3D1

②欧拉函数是积性函数,如果gcd(a,b)=1,则eq?%5Cvarphi%20%28a*b%29%3D%5Cvarphi%20%28a%29*%5Cvarphi%20%28b%29

③当n=eq?p%5Ekeq?%5Cvarphi%20%28n%29%3Dp%5Ek-p%5E%7Bk-1%7D%3Dp%5E%7Bk-1%7D%28p-1%29

原因是在1,2,···p,p+1,····2p,2p+1·····3p·····p^k,中,与n不互质的只有p的倍数,一共有p^{k-1}个。

④由以上可得欧拉函数公式。

016d429653794cfea2ce16187dc9c502.png

在计算机中,1/pi,等于0,因此需要先将答案初始化为n

其中n一定可以整除pi。

#include<bits/stdc++.h> 
using namespace std;
int main( )
{
    int n;
    cin>>n;
    int ans=n;//欧拉函数一定比ans小
    for(int i=2;i*i<=n;++i){
        if(n%i==0){//n的一个≤根号n的质因子i
            ans=ans/i*(i-1);
            while(n%i==0){
                n/=i;
            }
        }
    }
    if(n>1) ans=ans/n*(n-1);  //n>1,则n是一个大于根号n的质因子,比如n=2,质因子2大于根号2
    cout<<ans;
    return 0;
}

六、费马小定理—乘法逆元

4b46bf590dee4245a3189b2ff79dd8c5.png

这是一个典型的费马小定理的题目,p是一个质数,求n的乘法逆元。不仅是乘法逆元,还能求xa≡b(mod p),只需要先求出逆元,然后再乘以b即可。

一种不用费马小定理解决的方法:

a和p互质,p是质数
xa≡1(mod p)
在求xa≡1(mod p)的时候,我在程序中用到了m=p/a向下取整,那么a*(m+1)%p能够得到一个新数,并且这个数一定小于a,因为p/a一定是不整除的,则a*m<p,但a*(m+1)>p,并且是p<a*(m+1)<p+a的,只有当p/a整除时,a*(m+1)=p+a。因此得到的数a*(m+1)是一个小于a,大于等于1的数,把它当做新的a。

并且我们记录(m+1)作为x的一个因子。循环一直到a=1即可。

gpt如是说:

9f28a0f7ede341f4bca7f3869efc1dd6.png

7f94360f745943d68c6eb40843562f06.png

886ac7361973499b9370e8324da6ccc6.png

用此方法依然可以解决乘法逆元问题(时间复杂度无法证明),下面方法的基本思路是:

尝试让n乘以数字i(i从2到x),直到这个数模p为1,当然要使得越靠近1,那么就得让n乘以这个数超过p,这样可以模掉变小,所以求出temp来乘。

#include<bits/stdc++.h> 
using namespace std;
int main( )
{
    long long n;
    cin>>n;
    long long x=1;
    long long p=1e9+7;
    while(n!=1){
        long long temp=p/n;
        n=(n*(temp+1))%p;
        x=((temp+1)*x)%p;
    }
    cout<<x;
    return 0;
}

费马小定理:

如果p是一个质数,且a不是p的倍数,即gcd(a,p)=1,则满足:

8c908fccc2a445cdbaabc5799695b057.png

证明:

①若gcd(m,c)=1,且ac≡bc(mod m),则a≡b(mod m)

因为ac-bc≡0(mod m),(a-b)c≡0(mod m),因为gcd(m,c)=1,则a-b是m的倍数

所以,(a-b)≡0(mod m) 即 a≡b(mod m)
c5420bb4d00441efa52ea151657ee26d.png

所以本题只需要求eq?n%5E%7Bp-2%7D即可

快速幂 & 费马小定理:

#include<bits/stdc++.h> 
using namespace std;
int main( )
{
    long long n;
    cin>>n;
    long long x=1;
    long long p=1e9+7;  
    long long b=p-2;
    while(b){
        if(b&1) x=(x*n)%p;
        n=(n*n)%p;
        b>>=1;
    }
    cout<<x; //n^(p-2)%p
    return 0;
}

七、第二类斯特林数

7d5a9bfb20e84248acacc3dadcc532aa.png

组合数:很好理解,n个里面拿出m个,可以将n分为a1~an,从a2~an里面就拿出m个或者从a2~an里面拿出m-1个,另外再直接拿剩下的这个a1。

1c99d11912bf465fa6c55279ab64e63d.png

S(n,m)=S(n-1,m-1)+mS(n-1,m)

将球标号为a1~an,我们可以考虑a1存放的情况。

第一种情况:a1单独放在一个盒子里,这样只有一种情况,即S(n-1,m-1)

第二种情况:a1与其他球放在同意盒子里,我们这样考虑:

S(n-1,m),假设这个结果是n-1放入m种不同盒子的组合,设为x,每一种组合x[i]。对于剩下的一个球m而言,存在m*x[i]种放法。由于不存在x[j]和x[i]是完全相同的(j≠i),则因此m*x[i]和m*x[j]的情况也是不相同的,因为x[i]是一种组合情况,即使m*x[i]和m*x[j]个别盒子的球一样,他们也不属于同一种组合。

则对于a1而言有mS(n-1,m)种放法。


88fa032daed64104b1fa200cacc87ac5.png

第二类Stirling数(第二类斯特林数)-CSDN博客

八、卡特兰数

eq?C_%7Bm&plus;n%7D%5E%7Bm%7D%3DC_%7Bm&plus;n%7D%5E%7Bn%7D%3D%5Cfrac%7B%28m&plus;n%29%21%7D%7Bm%21n%21%7D%3D%5Cfrac%7B%28m&plus;n%29%28m&plus;n-1%29%5Ccdots%20%28n&plus;1%29%7D%7Bm%21%7D

推导情景:

小明从学校回家,学校在m×n矩阵方格的左下角顶点处,家在矩阵的右上角顶点处,每一次他只能沿着矩阵边沿往上或者往左走。问一共有多少种走法。

        我们可以知道,要从学校走到家里,无论何种走法,必须需要往右走n步,往上走m步,一共需要走m+n步,在这m+n步的序列中,拿出m步往上走或拿出n步往左走的组合方式即是答案。

卡特兰数:从原点(0,0)出发,每次向x轴或者y轴正方向移动1个单位,直到到达(n,n)点,且在移动过程中不越过第一象限平分线的移动方案总数。卡特兰数(Catalan)-CSDN博客

在具体求解时,由于数特别大,因此我们可以用分解质因数的方式求解。

九、莫比乌斯反演

莫比乌斯函数、莫比乌斯反演-CSDN博客

例题1:

9e987650ca7c4f4bb655fc51dce16619.png

dc171ab5bf1c4b36a499e672de6ed5bf.png

考虑到要对2~N的所有数求其质因数分解,我们可以用O(N)的时间复杂度去求2~N的所有质数(欧拉筛),然后再去求质因数分解时,只考虑质数即可。

如果不用欧拉筛,可能是可行的,但是速度会慢一些,因为每个数i,都要遍历2~sqrt(i) 去找质数然后除尽。

#include<bits/stdc++.h> 
using namespace std;
int main( )
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int N;
    cin>>N;
    int flag[1000001]={};//两重用,第一重欧拉筛,第二重记录质数个数
    vector<int> prime;
/*----------------欧拉筛--------------------*/
    for(int i=2;i<=N;++i){
        if(flag[i]==0){
            prime.push_back(i); //没被标记i是质数
        }
        for(int j=0;j<prime.size();++j){
            if(prime[j]*i>N) break;
            flag[prime[j]*i]=-1;
            if(i%prime[j]==0) break;
        }
    }
/*-----------对所有数进行质因数分解------------*/
    for(int i=2;i<=N;++i){
        int n=i;
        for(int j=0;j<prime.size();++j){
            while(n%prime[j]==0){
                flag[prime[j]]++;
                n/=prime[j];
            }
            if(prime[j]>sqrt(n)) break;
        }
        if(n>1) flag[n]++;
    }
    for(int i=2;i<=N;++i){
        if(flag[i]>0){
            cout<<i<<' '<<flag[i]<<'\n';
        }
    }
    return 0;
}

X、数学思考

一、二分查找

找到target的位置,或者target应该被插入的位置:

    int searchInsert(vector<int>& nums, int target) {
        int left=0,right=nums.size()-1;
        while(left<=right){
            int mid=(left+right)>>1;
            if(nums[mid]==target) return mid;
            if(nums[mid]>target) right=mid-1;
            else left=mid+1;
        }
        return left;
    }

(1)实际上 int mid=(left-left + right-left)>>1+left=(right-left)>>1+left;

即我这里想说明的问题是:任何两端区间[left,right],你都能在[0,right-left]中类比查看,它们两个区间只是一个偏移量的问题。

因此我们接下来只需要查看[0,bound]的情况

(2)bound可能是奇数,也可能是偶数;

当bound是奇数时,一共有偶数个数,如0  1  2  3 / 0 1 2 3 4 5,因此mid不是最中间。

当bound是偶数时,一共有奇数个数,mid一定在最中间。

(3)不论哪种情况,bound总在奇 偶 之间变化,最终随着区间不断缩小会变成[0,0] 或者[0,1]这两种情况的一种。不过

(4)left和right的变化实际上就是要找到一个区间[left,right]使之包含target,当区间交错之后,你会发现target的大小,还是在right和left之间的那个位置。因为要交错,必然是其中某一个选择要移动,要移动的目的就是保证target在范围内。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yorelee.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值