一点点数论和算法
本文已参与「新人创作礼」活动.一起开启掘金创作之路。
运行环境
- 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);
}
最小公倍数
求取方法:两个数的乘积除以他们的最大公约数。
证明一下:
以下是证明笔记,可以参考一下,阅读顺序从左到右从上到下。
测试题目:
养成做题好习惯,先看要求:
数据量大小:
每组包含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.计数质数
数据量大小:
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 ;
}
\