数论们

快考试了 是时候整理一下算法复习一下了 先从最弱的数论开始吧 ~ ~


1. 素数
首先是筛素数
根号n的不说了
埃不拉不拉筛O(Nlogn)
欧拉筛O(N) 好像好有个loglog来 忘记了总之快一点
其实埃筛的复杂度已经很优秀了
上面是预处理某个10^7-8范围内的全部素数的方法

#include<cstdio>
#define maxn 22000
using namespace std;
int l,r,cnt;
bool f[maxn];
void Eprime(){
    f[1]=f[0]=1; 
    for(int i=2;i<=21000;i++)if(f[i]==0)
        for(int j=i+i;j<=21000;j+=i)
            f[j]=1;
}
int main()
{
    scanf("%d%d",&l,&r);
    Eprime();
    for(int i=l;i<=r;i++)
        if(f[i]==0)cnt++;
    printf("%d
",cnt);
    return 0;
}
View Code
#include<cstdio>
#define maxn 22000
using namespace std;
int l,r,cnt,num,prime[maxn];
bool f[maxn];
void Oprime(){
    for(int i=2;i<=21000;i++){
        if(f[i]==0)prime[++num]=i;
        for(int j=1;j<=num;j++){
            if(i*prime[j]>21000)break;
            f[i*prime[j]]=1;
            if(i%prime[j]==0)break;
        }
    }
}
int main()
{
    scanf("%d%d",&l,&r);
    Oprime();
    for(int i=1;i<=num;i++){
        if(prime[i]>=l&&prime[i]<=r)cnt++;
        if(prime[i]>r)break;
    }
    printf("%d
",cnt);
    return 0;
}
View Code

如果给你个大数 比如说超了int 判断素数
那么以上的方法就跑不出来了
我们用到另外两种判断素数的算法
(1) 费马小定理 复杂度(次数*logn)
若p为素数 则a^(p-1)%p=1
但是如果翻过来就不一定正确
优异类数叫做卡迈克尔数就是合数
但上面那个概率是很大的所以嘛
多生成几个a试试 一般出错的概率还是很小的
当然如果考试那天人品XX的话就怨不得谁了

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<ctime>
#define ll long long
using namespace std;
ll p,falg;
ll mul(ll a,ll b){
    a%=p;b%=p;
    ll r=0;
    while(b){
        if(b&1){
            b--;r+=a;r%=p;
        }
        a<<=1;a%=p;b>>=1;
    }
    return r%p;
}
ll qm(ll a,ll b){
    ll r=1;
    while(b){
        if(b&1)r=mul(r,a);
        b>>=1;a=mul(a,a);
    }
    return r;
}
int main()
{
    cin>>p;srand(time(0));
    for(int i=1;i<=15;i++){
        int a=rand()%(p-1)+1;
        int x=qm(a,p-1);
        if(x!=1){
            falg=1;break;
        }
    }
    if(falg)cout<<"No"<<endl;
    else cout<<"Yes"<<endl;
}
View Code

(2)Miller Rabin 复杂度(logn)好像还有个常熟 不过无伤大雅
再说说误判概率 经过k轮这玩意 将合数误判为素数的概率是(1/4)^k
素数误判为合数的概率是0 一般进行个10轮15轮就好了
要是这样的概率都给碰上了那就没啥可说的了....
下面说说算法过程
首先 2特判一下子 对于剩下的奇素数
有 fermat a^(p-1)%p=1
避免出错同时加速这个算法
另一个定理 若x&1,且x*x%p==1 则有x=1或x=-1(即x==p-1)
把p-1写成p-1=m*2^j 然后呢 j次测试 若不满足上面那个东西 直接肯定p不是prime
每次都乘回来 最后又得到了p-1 再来一次fermat就ok了
很不错的算法 Orz谁想出来的

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<ctime>
#define ll long long
using namespace std;
ll p;
ll mul(ll a,ll b){
    a%=p;b%=p;
    ll r=0;
    while(b){
        if(b&1){
            b--;r+=a;r%=p;
        }
        a<<=1;a%=p;b>>=1;
    }
    return r%p;
}
ll qm(ll a,ll b){
    ll r=1;
    while(b){
        if(b&1)r=mul(r,a);
        b>>=1;a=mul(a,a);
    }
    return r;
}
bool Miller_Rabin(){
    if(p<=2)return 1;
    if(p%2==0)return 0;
    ll x,y,m,k=0,T=15,a;
    m=p-1;
    while(m%2==0){
        k++;m>>=1;
    }
    while(T--){
        a=rand()%(p-1)+1;
        x=qm(a,m);
        for(ll i=1;i<=k;i++){
            y=mul(x,x);
            if(y==1&&x!=1&&x!=p-1)return 0;
            x=y;
        }
        if(x!=1)return 0;
    }
    return 1;
}
int main()
{
    cin>>p;
    if(Miller_Rabin())printf("Yes
");
    else printf("No
");
    return 0;
} 
View Code

2. 质因数分解
这个吗 一般算法的复杂度是够用的
当然如果筛素数的话会快 而且对于一个质数 可以直接返回
当然如果对n!每个数质因数分解的话那筛素数就快多了
当然我们还有别的方法 忘记叫什么名字了 咨询的数竞的孩子
n!里p的个数(p为素数)=n/p+n/(p*p)+n/(p*p*p)+n/(p*p*p*p)...(p*p*..)<=n
这样就快得多了 有时排列组合可以用到这个

/*
然后身边学过数竞的孩子说求阶乘里有几个素数有别的方法
n!里p的个数=n/p+n/p*p+n/p*p*p.....知道p==n
知道这个就快了嘛 嗖嗖的..... 
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 1000010
#define ll long long
using namespace std;
ll n,m,p,a[maxn],x,y,ans=1,prime[maxn],num,P[maxn];
bool f[maxn];
void Get(){
    for(int i=2;i<=1000000;i++){
        if(f[i]==0)prime[++num]=i,P[i]=num;
        for(int j=1;j<=num;j++){
            if(i*prime[j]>1000000)break;
            f[i*prime[j]]=1;
            if(i%prime[j]==0)break;
        }
    }
}
ll qm(ll a,ll b,ll c){
    ll r=1;
    while(b){
        if(b&1)r*=a,r%=c;
        b>>=1;a*=a;a%=c;
    }
    return r;
}
int main()
{
    freopen("flower.in","r",stdin);
    freopen("flower.out","w",stdout);
    cin>>n>>m>>p;Get();
    for(int i=1;i<=num;i++){
        ll P=prime[i];
        while(P<=n){
            a[i]+=n/P;
            P*=prime[i];
        }
    }
    for(int i=1;i<=num;i++){
        ll P=prime[i];
        while(P<=m){
            a[i]-=m/P;
            P*=prime[i];
        }
    }
    for(int i=1;i<=num;i++){
        ll P=prime[i];
        while(P<=n-m){
            a[i]-=(n-m)/P;
            P*=prime[i];
        }
    }
    for(int i=1;i<=num;i++)
        ans=ans*qm(prime[i],a[i],p)%p;
    cout<<ans<<endl;
    return 0;
}
View Code

3. 卡特兰数 f(n)=f(n-1)*(4*n-2)/(n+1)
感觉这就是个bug 费劲写出来的dp
结果发现想到卡特兰数的几行就搞完了
QAQ
有许多典型的问题(先背过23333)
比如说 给定1-n问出栈序列的种类数
一个有n个结点的二叉树总共有多少种形态
在一个圆上2*K个不同的结点连K条线段,
每个结点用一次将圆分成最少部分的前提下有多少种连线的方法
n对括号有多少种匹配方式
矩阵链乘
求一个凸多边形区域划分成三角形区域的方法数
.......
比较多的例子 可以自己搜搜
其实原理都差不多的样子
那矩阵链乘说吧 P=a1×a2×a3×……×an +括号 有几种方法
可以这样考虑,首先通过括号化,将P分成两个部分,然后分别对两个部分进行括号化。
比如分成(a1)×(a2×a3.....×an),然后再对(a1)和(a2×a3.....×an)分别括号化;
又如分成(a1×a2)×(a3.....×an),然后再对(a1×a2)和(a3.....×an)括号化。
f(n) = f(1)*f(n-1) + f(2)*f(n-2) + f(3)*f(n-3) + f(n-1)*f(1)。
手写一下的话 就是卡特兰数


4. 欧拉函数
定义:设n为正整数 不大于n的且与n互素的整数个数记为f(n)
求欧拉函数的方法
首先暴力好打 慢
f(n)=n*(1-1/p1)(1-1/p2)(1-1/p3)... pi为n的质因子
那就要分解质因数了 好办
应用嘛 还是比较好使的
求逆元
定义应用
a^(f(p))%p=1

#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
ll n,ans;
ll init()
{
    ll x=0;char s=getchar();
    while(s<'0'||s>'9')s=getchar();
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    return x;
}
int main()
{
    while(1)
      {
          n=init();
          if(n==0)break;
          ans=n;
        for(ll i=2;i*i<=n;i++)
          if(n%i==0)
              {
                while(n%i==0)n/=i;
                ans=ans/i*(i-1);
            }
        if(n>1)ans=ans/n*(n-1);
        printf("%ld
",ans);
      }
    return 0;
}
View Code

5. 排列组合
一般比较好打 关键是能不能看出来 能不能写出公式
C(N,M)=N!/(M!*(N-M)!)
如果%p的话 要是p为素数 就可以搞一下逆元(欧拉函数 gcd都可以)
要是p不是素数 就比较蛋疼了 反正求逆元就是各种错...
质因数分解 然后把分母都给抹了去
要是nm很大很大怎么办呢(%p 且p为素数)
用卢卡斯定理 这是个好东西啊
定义:设p为素数 将非负整数ab表示成p进制
a=ak*p^k+a(k-1)*p^(k-1)+...+a1*p+a0 b同理
允许首位为0 且b<a
则 C(a,b)同余C(ak,bk)*C(a(k-1),b(k-1))*...*C(a0,b0)(mod p)
证明嘛 不会啊23333 可以抓个数竞的孩子问问23333
写代码也是很好写的

#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 100010
#define ll long long
using namespace std;
ll T,n,m,p,f[maxn];
void Get(){
    f[0]=1;
    for(int i=1;i<=p;i++)
        f[i]=f[i-1]*i%p;
}
ll qm(ll a,ll b){
    a%=p;ll r=1;
    while(b){
        if(b&1)r=r*a%p;
        b>>=1;a=a*a%p;
    }
    return r;
}
ll C(ll a,ll b){
    if(b>a)return 0;
    return f[a]*qm(f[b]*f[a-b],p-2)%p;
}
ll Lcs(ll a,ll b){
    if(b==0)return 1;
    return C(a%p,b%p)*Lcs(a/p,b/p)%p;
}
int main()
{
    cin>>T;
    while(T--){
        memset(f,0,sizeof(f));
        cin>>n>>m>>p;Get();
        cout<<Lcs(n+m,n)<<endl;
    }
    return 0;
}
View Code

6. 欧几里得 扩展欧几里得(裴蜀定理)
这个东西用的比较多吧还算是
首先gcd本质就是把两个数质因数分解
然后每个素数的指数取小lcm是取大
欧几里得就是辗转相除法 有库函数但是略慢
扩展欧几里得用来解不定方程 或者求逆元
也可以解mod方程 比如a%p=1 -> ax-bp=1
算法过程 解ax+by=c 实际上解得是ax+by=gcd(a,b)
然后两边都*gcd/c 这样得到一组基解
调整的话 保证等式成立的前提下 x+b/gcd y-a/gcd 或者反过来
可以得到最小正整数解

/*搞个题目做做 poj 1061*/
#include<iostream>
#include<cstdio>
#include<cstring>
#define mod(a,b) (a%b+b)%b
#define ll long long
using namespace std;
ll x,y,g,n,m,l,k;
void gcd(ll a,ll b){
    if(b==0){
        x=1;y=0;g=a;return;
    }
    gcd(b,a%b);
    ll tmp=x;x=y;y=tmp-a/b*y;
}
int main()
{
    cin>>x>>y>>m>>n>>l;
    ll a=mod(m-n,l),b=l,c=mod(y-x,l);
    gcd(a,b);
    if(c%g){cout<<"Impossible"<<endl;return 0;}
    x=x*c/g;k=b/g;x=mod(x,k);
    cout<<x<<endl;
    return 0;
} 
View Code

7. mod运算
支持+-* /的话要搞搞逆元在*上去

8. 快速幂
很简单的 不详细写了
最近转非递归了 快点

9. 矩阵乘法
这玩意还是很好使的
用于解决线性递推式的计算 复杂度logn
手推一个矩阵 还有一个记答案
注意要是递推式里有与n有关的东西 就会比较麻烦
矩阵好好搞搞
还有就是dp优化了
当然还有许多许多用处 网上都能搜到

/*codevs 2314 矩阵乘法 分块写*/
#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
ll n,p,a[3][3],f[3][3];
ll Get(ll x){
    ll r=0;
    while(x)r++,x/=10;
    return r;
}
ll Pow(ll x){
    ll r=1;
    for(int i=1;i<=x;i++)
        r=r*10;
    return r;
}
ll slow(ll a,ll b){
    a%=p;b%=p;
    ll r=0;
    while(b){
        if(b&1)r+=a,r%=p,b--;
        a<<=1;b>>=1;a%=p;
    }
    return r;
}
void mul(ll a[3][3],ll b[3][3]){
    ll c[3][3]={0};
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            for(int k=0;k<3;k++)
                c[i][j]=(c[i][j]+slow(a[i][k],b[k][j]))%p;
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            a[i][j]=c[i][j];
}
ll Cal(ll x,ll l,ll r){
    ll c=r-l;
    x=slow(x,l);x=(x*10%p+l)%p;//慢乘慢乘慢乘 写丑了 这个应该不用特判或许 
    //x=(x*l%p*10%p+l)%p;
    memset(a,0,sizeof(a));
    memset(f,0,sizeof(f));
    f[0][0]=x;f[0][1]=l;f[0][2]=1;
    a[0][0]=l*10%p;a[1][0]=a[1][1]=a[2][0]=a[2][1]=a[2][2]=1;
    while(c){
        if(c&1)mul(f,a);
        c>>=1;mul(a,a);
    }
    return f[0][0];
}
int main()
{
    cin>>n>>p;ll x=0;
    ll len=Get(n);
    for(int i=1;i<len;i++)
        x=Cal(x,Pow(i-1),Pow(i)-1);
    x=Cal(x,Pow(len-1),n);
    cout<<x<<endl;
    return 0;
}
View Code

10. 中国剩余定理
解一次同余方程组
mi模数 bi为余数
s=Σmi Mi=s/mi
X=Σ(Mi*bi*Mi的逆元)%s
X是基解 步长为s

#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
ll n,l,r,b[11],m[11],M[11],Mi[11],s=1,ans,sum,x,y;
void E_gcd(ll a,ll b)
{
    if(!b){x=1;y=0;return;}
    E_gcd(b,a%b);int tmp=x;x=y;y=tmp-a/b*y;
}
int main()
{
    cin>>n>>l>>r;
    for(int i=1;i<=n;i++)
      cin>>m[i]>>b[i],s*=m[i];
    for(int i=1;i<=n;i++)
      M[i]=s/m[i];
    for(int i=1;i<=n;i++)
      {
          x=y=0;
          E_gcd(M[i],m[i]);
          Mi[i]=(x+m[i])%m[i];
      }
    for(int i=1;i<=n;i++)
      ans=(ans+M[i]*Mi[i]%s*b[i])%s;
    if(ans<l||ans>r)ans=sum=0;
    else sum=(r-ans)/s+1;
    cout<<sum<<endl<<ans;
    return 0;
}
View Code

11. 容斥原理
公式好长的不写了
一般把问题的并(不好求的)转化成问题的和-问题的交

原文地址:https://www.cnblogs.com/yanlifneg/p/6054407.html