这里是目录
数论
写在前面
之前讲过一篇关于数学的,但是数论和数学是有很多不同的,数学的题是用数学思维求解,而数论就是提供模板可以直接用的,但是数论有很多,我将会持续更新,总的会有质数、约数、欧拉函数、快速幂、扩展欧几里得算法、中国剩余定理、高斯消元、求组合数、容斥原理、博弈论;这次就先更新质数和约数。
质数
质数的定义:
质数又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数(规定1既不是质数也不是合数)。
试除法判定质数
给定 n 个正整数 ai,判定每个数是否是质数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个正整数 ai。
输出格式
共 n 行,其中第 i 行输出第 i 个正整数 ai 是否为质数,是则输出 Yes,否则输出 No。
数据范围
1≤n≤100,
1≤ai≤231−1
输入样例:
2
2
6
输出样例:
Yes
No
暴力写法O(n)
试除法:
bool is_prime(int x)
{
if (x < 2) return false;
for (int i = 2; i < x; i ++ ) // 用2到x-1的数去除x,如果能整除,返回false
if (x % i == 0)
return false;
return true;
}
可以看到,上述做法的时间复杂度为O(n),那么有没有什么方法能优化它呢?
优化O(sqrt(n))
bool is_prime(int x)
{
if (x < 2) return false;
for (int i = 2; i <= x/i; i ++ )
if (x % i == 0)
return false;
return true;
}
为什么可以这样优化呢?
因为一个数的因数是成对出现的,这很好理解,如:
12 ÷ 3 = 4, 3是12的因数,4也是12的因数。
因此,如果我们只枚举较小的因数,对较大的因数就可以直接放过,这样就节省了遍历的时间;
即对12来说,只需要枚举 2 3 就可以了,相比于枚举 2 到 12,大大减少了时间复杂度。
那么,抽象出来:
设n能被d整除,那么n/d得到的整数,也是能整除n的,那么,只需要枚举到n/i即可,对更多的值,便不用枚举了。这样就将时间复杂度从O(n)优化到了O(sqrt(n)),即:
for (int i = 2; i <= x/i; i ++ )
最后请看优化后的代码:
#include <iostream>
#include <algorithm>
using namespace std;
bool is_prime(int x)
{
if (x < 2) return false;
for (int i = 2; i <= x/i; i ++ )
if (x % i == 0)
return false;
return true;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
if (is_prime(x)) puts("Yes");
else puts("No");
}
return 0;
}
优化后的时间复杂度只有O(sqrt(n))
分解质因数
给定 n 个正整数 ai,将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个正整数 ai。
输出格式
对于每个正整数 ai,按照从小到大的顺序输出其分解质因数后,每个质因数的底数和指数,每个底数和指数占一行。
每个正整数的质因数全部输出完毕后,输出一个空行。
数据范围
1≤n≤100,
2≤ai≤2×109
输入样例:
2
6
8
输出样例:
2 1
3 1
2 3
暴力写法O(n)
从小到大枚举所有的数
for(int i =2 ; i <= n ; i++)
{
if(n % i == 0)//i一定是质数
{
int s = 0;
while(n % i == 0)
{
n /= i;
s ++;
}
cout << i << " " << s;
}
}
可以看到,上述做法的时间复杂度为O(n),那么有没有什么方法能优化它呢?
优化O(sqrt(n))
n中最多只包含一个大于sqrt(n)的质因子。
n 的质因子最多只包含一个大于 根号n 的质数。如果有两个,这两个因子的乘积就会大于 n,矛盾。
i 从 2 遍历到 根号n。 用 n / i,如果余数为 0,则 i 是一个质因子。
s 表示质因子 i 的指数,n /= i 为 0,则 s++, n = n / i 。
最后检查是否有大于 根号n的质因子,如果有,输出。
for (int i = 2; i <= n / i; i ++ )
if (n % i == 0)
{
int s = 0;
while (n % i == 0) n /= i, n ++ ;
cout << i << ' ' << s << endl;
}
if (n > 1) cout << n << ' ' << 1 << endl;
cout << endl;
最后请看代码:
#include <iostream>
#include <algorithm>
using namespace std;
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ )//i <= x / i:防止越界,速度大于 i < sqrt(x)
if (x % i == 0)//i为底数
{
int s = 0;//s为指数
while (x % i == 0) x /= i, s ++ ;
cout << i << ' ' << s << endl;//输出
}
if (x > 1) cout << x << ' ' << 1 << endl;//如果x还有剩余,单独处理
cout << endl;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
divide(x);
}
return 0;
}
优化后的时间复杂度应该是介于O(logn)和O(sqrt(n))之间的。
筛质数
给定一个正整数 n,请你求出 1∼n 中质数的个数。
输入格式
共一行,包含整数 n。
输出格式
共一行,包含一个整数,表示 1∼n 中质数的个数。
数据范围
1≤n≤106
输入样例:
8
输出样例:
4
最普通的筛法 O(nlogn)
void get_primes2(){
for(int i=2;i<=n;i++){
if(!st[i]) primes[cnt++]=i;//把素数存起来
for(int j=i;j<=n;j+=i){//不管是合数还是质数,都用来筛掉后面它的倍数
st[j]=true;
}
}
}
埃氏筛法 O(nloglogn)
void get_primes1(){
for(int i=2;i<=n;i++){
if(!st[i]){
primes[cnt++]=i;
for(int j=i;j<=n;j+=i) st[j]=true;//可以用质数就把所有的合数都筛掉;
}
}
}
线性筛法 O(n)
void get_primes(){
//外层从2~n迭代,因为这毕竟算的是1~n中质数的个数,而不是某个数是不是质数的判定
for(int i=2;i<=n;i++){
if(!st[i]) primes[cnt++]=i;
for(int j=0;primes[j]<=n/i;j++){//primes[j]<=n/i:变形一下得到——primes[j]*i<=n,把大于n的合数都筛了就
//没啥意义了
st[primes[j]*i]=true;//用最小质因子去筛合数
//1)当i%primes[j]!=0时,说明此时遍历到的primes[j]不是i的质因子,那么只可能是此时的primes[j]<i的
//最小质因子,所以primes[j]*i的最小质因子就是primes[j];
//2)当有i%primes[j]==0时,说明i的最小质因子是primes[j],因此primes[j]*i的最小质因子也就应该是
//prime[j],之后接着用st[primes[j+1]*i]=true去筛合数时,就不是用最小质因子去更新了,因为i有最小
//质因子primes[j]<primes[j+1],此时的primes[j+1]不是primes[j+1]*i的最小质因子,此时就应该
//退出循环,避免之后重复进行筛选。
if(i%primes[j]==0) break;
}
}
}
最后请看总的代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N= 1000010;
int primes[N], cnt;
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int main()
{
int n;
cin >> n;
get_primes(n);
cout << cnt << endl;
return 0;
}
约数
约数的定义
约数,又称因数。整数a除以整数b(b≠0)
除得的商正好是整数而没有余数,我们就说a能被b整除,或b能整除a。a称为b的倍数,b称为a的约数。在大学之前,"约数"一词所指的一般只限于正约数。约数和倍数都是二元关系的概念,不能孤立地说某个整数是约数或倍数。一个整数的约数是有限的。同时,它可以在特定情况下成为公约数。
试除法求约数
给定 n 个正整数 ai,对于每个整数 ai,请你按照从小到大的顺序输出它的所有约数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出共 n 行,其中第 i 行输出第 i 个整数 ai 的所有约数。
数据范围
1≤n≤100,
2≤ai≤2×109
输入样例:
2
6
8
输出样例:
1 2 3 6
1 2 4 8
这里和前面求质数是差不多的,只需要枚举到sqrt(n) 就可以了。
vector<int> res;
for (int i = 1; i <= x / i; i ++ )
if (x % i == 0)
{
res.push_back(i);
if (i != x / i) res.push_back(x / i);
}
sort(res.begin(), res.end());
return res;
最后请看总的代码:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
vector<int> get_divisors(int x)
{
vector<int> res;
for (int i = 1; i <= x / i; i ++ )
if (x % i == 0)
{
res.push_back(i);
if (i != x / i) res.push_back(x / i);
}
sort(res.begin(), res.end());
return res;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
auto res = get_divisors(x);
for (auto x : res) cout << x << ' ';
cout << endl;
}
return 0;
}
约数个数
给定 n 个正整数 ai,请你输出这些数的乘积的约数个数,答案对 109+7 取模。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出一个整数,表示所给正整数的乘积的约数个数,答案需对 109+7 取模。
数据范围
1≤n≤100,
1≤ai≤2×109
输入样例:
3
2
6
8
输出样例:
12
算法分析
举个例子:
24=2*2*2*3=2³*3
再用各个质数的指数加一后再相乘即为此数的约数个数,
比如 (3+1)*(1+1)=4*2=8, 即表示24有8个约数。
24的约数:1、2、3、4、6、8、12、24
思路就是先把原数分解为质因数,最后把每一个数的指数累加即可。从a1一直分解到an,由于a的数据过大,此处用哈希表进行存储。
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int main()
{
int n;
cin >> n;
unordered_map<int, int> primes;//映射函数
while (n -- )
{
int x;
cin >> x;
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0)
{
x /= i;//方便求得约数的数量
primes[i] ++ ;
}
if (x > 1) primes[x] ++ ;//x的最大公约数可能大于sqrt(x);
}
LL res = 1;
for (auto p : primes) res = res * (p.second + 1) % mod;//将统计出来的数按照由图中公式所得出来的结论得出答案
cout << res << endl;
return 0;
}
约数之和
给定 n 个正整数 ai,请你输出这些数的乘积的约数之和,答案对 109+7 取模。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出一个整数,表示所给正整数的乘积的约数之和,答案需对 109+7 取模。
数据范围
1≤n≤100,
1≤ai≤2×109
输入样例:
3
2
6
8
输出样例:
252
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int main()
{
int n;
cin >> n;
unordered_map<int, int> primes;
while (n -- )
{
int x;
cin >> x;
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0)
{
x /= i;
primes[i] ++ ;
}
if (x > 1) primes[x] ++ ;
}
LL res = 1;
for (auto p : primes)
{
LL a = p.first, b = p.second;
LL t = 1;
while (b -- ) t = (t * a + 1) % mod;
res = res * t % mod;
}
cout << res << endl;
return 0;
}
最大公约数
最大公约数定义
最大公因数,也称最大公约数、最大公因子,指两个或多个整数共有约数中最大的一个。a,b的最大公约数记为(a,b),同样的,a,b,c的最大公约数记为(a,b,c),多个整数的最大公约数也有同样的记号。求最大公约数有多种方法,常见的有质因数分解法、短除法、辗转相除法、更相减损法。与最大公约数相对应的概念是最小公倍数,a,b的最小公倍数记为[a,b]。
这里介绍一下欧几里得算法//辗转相除法
int gcd1(int a, int b){//辗转相除
return b ? gcd1(b, a % b) : a;
}
给定 n 对正整数 ai,bi,请你求出每对数的最大公约数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数对 ai,bi。
输出格式
输出共 n 行,每行输出一个整数对的最大公约数。
数据范围
1≤n≤105,
1≤ai,bi≤2×109
输入样例:
2
3 6
4 6
输出样例:
3
2
最后请看代码:
#include <iostream>
#include <algorithm>
using namespace std;
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", gcd(a, b));
}
return 0;
}