一点点数论和算法

199 阅读3分钟

一点点数论和算法

本文已参与「新人创作礼」活动.一起开启掘金创作之路。

运行环境

  • Dev c++ IDE V5.7.1
  • 1秒内大概能执行1e8条执行,也就是在O(N^2)的时间复杂度下,N应该小于1万(1e4)

前提基础

什么是约数,因数?

约数,又称因数整数a 除以 整数b(b≠0) 除得的正好是整数而没有余数,我们就说a能被b整除,或b能整除a。a称为b的倍数,b称为a的约数。

最大公约数与最小公倍数

最大公约数

最大公约数是指两个或者多个整数共有的约数当中,最大的一个约数。求最大公约数常用的方法是辗转相除法,又称欧几里得算法:

证明的方法很简单,原理如下:

最简单一句话:缩小a和b的数值范围,就是求解的核心所在。

你如果很在意a和b的大小关系不妨思考一下为什么GCD(a,b)后要调转位置呢?调转位置就是解决a、b大小问题的核心所在,巧妙之处。

代码如下:

#include <iostream>
#include <stdio.h>
​
using namespace std;
​
int GCD(int a,int b)
{
    if(b==0)//当余数等于0的时候可得到g 
        return a;
    else
    {
        GCD(b,a%b);//反转位置后,递归调用。 
    } 
}
​
int GCD_NoRec(int a,int b)
{
    int temp=0;
    while(b!=0)
    {
        temp=a;
        a=b;
        b=temp%b;
    }
    return a;
} 
​
int main()
{
    int a=0,b=0;
    cin>>a>>b;
    cout<<GCD_NoRec(a,b);
    
}

最小公倍数

求取方法:两个数的乘积除以他们的最大公约数。

证明一下:

以下是证明笔记,可以参考一下,阅读顺序从左到右从上到下

1-2

测试题目:

KY35最简真分数

养成做题好习惯,先看要求:

image-20220505222709868

数据量大小:

每组包含n(n<=600)和n个不同的整数,整数大于1且小于等于1000。

推测时间复杂度:

至少 O(N^3)

思路:

先处理读入的问题:这题目上看着是以0结尾多组数据输入,实际上是。。。每组只有一个n。

读入以后 O(N^2)每个数字和对应的数字遍历一遍作比较看是不是互质的。互质就是GCD结果为1,水题一个10mins写完,没什么难度。顺手练练Vector。

#include <iostream>
#include <stdio.h>
#include <vector>
​
using namespace std;
​
int GCD_NoRec(int a,int b)
{
    int temp=0;
    while(b!=0)
    {
        temp=a;//辗转相除 
        a=b;
        b=temp%b;
    }
    return a;
} 
​
int main()
{
    /*数据定义*/
    int n=0;
    int temp=0; 
    int ans=0;
    vector<int> datas;
     /*数据读入*/
        cin>>n;
        if(n==0)
            return 0;
        else
        {
            /*数据处理*/
            for(int i=0;i<n;i++)
            {
                cin>>temp;
                datas.push_back(temp);
            }
            for(int i=0;i<n;i++)
            {
                for(int j=i;j<n;j++) 
                {
                    (GCD_NoRec(datas[i],datas[j]) == 1 )? ans++ : ans ;
                }
            }
            /*数据输出*/
            cout<<ans<<"\n";
            datas.clear();
            ans=0;
        }   
}
 

质数(素数)

什么是质数?

质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。

问题,如何最快找到一个区间内的全部质数?

我们举一个题目的例子:

测试题目:Leetcode 204.计数质数

image-20220505233301632

数据量大小:

0 <= n <= 5 * 10^6

数据范围大概50W,所以大概 只有n能通过。

从基本的方法开始:

暴力的想法很简单就是去枚举尝试,对于数字N来说,只要存在N % i == 0其中(i 在(2,N)之间)的情况就存在一个整数因数,所以第一个想法就是枚举。但是肯定无法通过这道题目的,因为枚举就大概需要 O(N^2)的时间复杂度,肯定不行。

介绍一种算法:

欧拉筛(线性筛)算法

他的思路很简单:

如果2为素数那么4 、6、8、10都不可能是素数,同理素数的整数倍的数字都不可能是素数。

我们先把所有不是素数的数字选择出来,剩下就是素数了。

但是你会意识到一个问题,就是对于6来说它会被3和2同时筛选一次,这是多余的操作.

如何减少这个多余的操作呢,思路就是找到哪个是他的唯一标识,让他只被筛选一次即可,这里就选择唯一的因数——最小质因数。

具体实现思路如下:

首先就是要有基本素数,没有第一个素数后边没办法进行筛选,由于素数数量并不确定所以我们最好使用vector来当存储的容器,这里还有一个小技巧我们使用bitset这个C++中容器来作为标记位,他的数据每一个只占用一位,注意是一位,不是一个字节,这样使用的空间就大大的省下来了,里面还内置了许多方法方便统计,在默认情况下bitset的内容都是0,这样也很方便使用。

numbers作为合数的标记,如果是合数就1、如果是质数就是0,我们从2开始因为2是第一个素数,这样我们就不用在循环前先把2单独加入到vector里面了,2作为第一个素数,将会把4筛选掉,循环继续进行这时候来到素数3,作为素数他直接被加入到了vector里面,这就迎来了第一个问题,你怎么知道他一定是素数呢?换个说法就是

怎么判断当前的数是质数还是合数?

我在这里简单说一下,不一定正确但是是我个人的理解,首先存在条件:

算数基本定理 :任何一个大于1的自然数 N,如果N不为素数,那么N可以唯一分解成有限个质数的乘积 。

也就是说我们循环到某个数之前一定已经把他前边筛选完了,因为他一定是小于他的某几个质数的乘积。所以肯定会被小于他的质数筛选掉。看起来可能有些问题,为啥因为小就能筛选掉了呢?不是可能存在多个相同的最小质因数吗?接着往下看:

举个例子:在遍历到N的时候如果存在K为最小质因数,那么一定有:N/K > K ,此时N/K不一定为质数,但是:N/K一定在区间(1,N),也就是说我们在遍历的时候将N之前的所有数字和N对应的所有质数相乘,来筛选素数就能保证到N的时候,不会有漏洞的合数被认定为素数。

这样就完成了逻辑上完美,数论有些绕,希望各位仔细看看,有问题欢迎讨论!

这样解决的方法已经呼之欲出了。

我们只需要解决两个问题:

  • 怎么判断当前的数是质数还是合数?
  • 怎么判断最小质因数?

第一个问题已经解决了,第二个问题也很简单,我们构造一个Vector来存放一个递增的现有的质数序列,一个一个去尝试因为序列是递增的,第一个尝试到的一定是最小的这时候直接break就行了,这也是这段代码的原因:

if(i%primeNumbers[j] == 0)
    break;

最后把完整的代码放上来:

bitset <MAXNUMBER> numbers; 
​
int countPrimes(int n) {
    vector <int> primeNumbers;
    for(long long i=2;i<n;i+=1)
    {
        if(numbers[i] == 0)
            primeNumbers.push_back(i);  
        for(int j=0;j<primeNumbers.size() && primeNumbers[j]*i < n; j++)
        {
            numbers[primeNumbers[j]*i]=1;
            if(i%primeNumbers[j] == 0)
                break;
        }
    }
    return primeNumbers.size(); 
}

这段代码有几个小细节:

细节1:long long i当数据量较大的时候可能会超出产生的目前vector的范围,而且可能会超出int,这时候就需要一个longlong来产生隐式数据类型提升。仔细看i所使用和计算的位置。

细节2:枚举的范围可能会超出n,这都是没必要的,所以增加判断primeNumbers[j]*i < n节省时间。

还有提一嘴其实可以再变换一下啊,偶数都不是质数!也就说我们只需要枚举奇数就可以了!时间是直接减少一半还是很可观的。

完整的代码请看github或者gitee。

质因数分解

质因数分解算法。

质因数分解思路很简单,简单来说就是从所有质数里面筛选出来对应的质因数。

枚举的技巧:

对于数字N,他只可能存在一个大于Sqrt(N)的质因数。反证法即可证明。

所以我们枚举的时候只需要到Sqrt(N)即可,如果到时候结果不为N/目前质因数不为1,则添加一个除法结果即可。

/*
质因数分解 
*/
vector<int> getPrimeFactor(int n)
{
    
    long long result=n;
    vector<int> ans;
    
    ol_PrimeNumbers(sqrt(n)+1);//先使用线性筛筛选出质数。
    
    for(long long i=0;i< primeNumbers.size();i++)//对质数进行枚举
    {
        while(result% primeNumbers[i]==0 && result!=1)//针对相同质因数进行循环一次性处理完
        {
            result/=primeNumbers[i];
            ans.push_back(primeNumbers[i]);
        }
    }
    if(result!=1)//存在大于sqrt(n)的数字
        ans.push_back(result);
    return  ans ;
}

\