欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章

🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
前言:
数论是算法竞赛与程序设计中极为重要的数学基础,贯穿了从简单模拟到复杂数学优化的各类题型。无论是质数筛法、最大公约数与最小公倍数,还是排列组合、快速幂、质因数分解等内容,都是解决竞赛题目、提升代码效率的必备工具。
本文将从位运算入手,系统整理数论基础知识点与常用 C++ 代码模板,方便大家在学习和刷题时快速查阅、直接套用,为后续更深入的算法学习打下坚实基础。
文章目录
- 前言:
- 正文:
-
- 数论基础
-
- [0. 位运算:](#0. 位运算:)
- [1. 有关质数](#1. 有关质数)
- [2. 组合数](#2. 组合数)
- [3. 排列](#3. 排列)
- [4. 错排](#4. 错排)
- [5. 快速幂](#5. 快速幂)
- [6. 唯一分解定理](#6. 唯一分解定理)
- [7. GCD(最大公约数)](#7. GCD(最大公约数))
- [8. LCM(最小公倍数)](#8. LCM(最小公倍数))
正文:
数论基础
0. 位运算:
取二进制末位
代码模板:
cpp
int main()
{
ios::sync_with_stdio(false);//关闭同步流
cin.tie(nullptr);
int n = 9;
cout << n;
while(n > 0)
{
int a = n & 1;
cout << a << " ";
n >>= 1;
}//输出:1 0 0 1
return 0;
}
判断奇偶
利用末尾 == n & 1;的性质,最后一位为1是奇数,是0为偶数
提取 / 设置指定位
原理: 用按位与&提取位,按位或|设置位。
例子:
cpp
// 提取num的第k位(0表示最低位)
int getBit(int num, int k) {
return (num >> k) & 1; // 先右移k位,再与1提取最后一位
}
// 设置num的第k位为1
int setBit(int num, int k) {
return num | (1 << k); // 1左移k位,再与num或
}//利用移位以后第k位是1,则或运算以后一定为1.
int main() {
int num = 5; // 0000 0101
cout << getBit(num, 2) << endl; // 提取第2位:1
cout << setBit(num, 1) << endl; // 设置第1位为1:0000 0111 → 7
return 0;
}
快速幂的本质也是位运算
1. 有关质数
- 埃拉托斯特尼筛法,是一类十分简单,并且好理解的筛法。
模板:
cpp
// 假设 N 是预先定义的常量,比如 const int N = 1e7;
const int N = 1e7;
vector<int> prime; // 存储素数
bool is_prime[N]; // 判断是否是素数
void Era(int n) {
// 初始化:2~n 先默认都是素数
for (int i = 2; i <= n; ++i) is_prime[i] = true;
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) { // 如果i是素数
prime.push_back(i); // 加入素数列表
// 关键行:1ll 避免 i*i 溢出
if (1ll * i * i > n) continue; // 若i²超过n,无需筛除倍数(后续j=i*i会超出n)
// 筛除i的倍数:从i²开始(更小的倍数已被更小的素数筛过)
for (int j = i * i; j <= n; j += i) {
is_prime[j] = false;
}
}
}
}
使用:
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e7;
bool is_prime[N];
vector<int> prime;
void Era(int n)//埃式筛
{
//第一步:打上标记,所有数字
for(int i = 2;i <= n; i++) is_prime[i] = true;
//遍历,有标记的加入,并去除其倍数的标记
for(int i = 2;i <= n; i++)
{
if(is_prime[i])
{//只有是素数的时候才去标记
prime.push_back(i);
//防止 int 乘法溢出
if(1ll * i * i > n) continue;
//去掉标记
for(int j = i*i; j <= n;j += i)
{
is_prime[j] = false;
}
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n = N;
Era(n);
//100002 - 1 第100002个素数
cout << prime[100001];
return 0;
}
for循环判断质数
cpp
bool Is_prime(int n)
{//先处理特殊值,除2以外,所有的偶数都不是素数
if(n < 2) return false;
if(n == 2) return true;
if(n % 2 == 0) return false;
for(int i = 3;i * i <= n;i += 2)
{
if(n % i == 0) return false;
}
return true;
}
2. 组合数
组合数,也叫二项式系数,记作C(n,k)
公式:
C ( n , k ) = n ! / k ! ( n − k ) ! C(n,k)= n!/k!(n−k)! C(n,k)=n!/k!(n−k)!
利用杨辉三角可以得到:
C ( n , k ) = C ( n − 1 , k − 1 ) + C ( n − 1 , k ) C(n,k)=C(n−1,k−1)+C(n−1,k) C(n,k)=C(n−1,k−1)+C(n−1,k)
在编写代码之前要处理好 C[i][0] = 1 ;
模板:
cpp
void init_C(int n) { // 求一个 n x n的杨辉三角
C[0][0] = 1;
for (int i = 1; i <= n; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j) {
C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
}
}
}
使用:
cpp
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int C[16][16];
void Init_C(int m, int n)
{
C[0][0] = 1;//特殊值特殊处理
for(int i = 1;i <= m;i++)
{
C[i][0] = 1;
for(int j = 1; j <= i; j++)
{
C[i][j] = C[i-1][j-1] + C[i-1][j];
//由于二维数组main函数外定义自动初始化的特点C[x][0]必然为0,不需要考虑
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 0;
cin >> t;
for(int i = 0; i < t; i++)
{
int m = 0,n = 0;
cin >> m >> n;
Init_C(m,n);
cout << C[m][n] << "\n";
}
return 0;
}
3. 排列
从 n 个东西里,选出 k 个,并且按顺序排好,一共有多少种排法 → 这个数量,就叫排列数。
全排列公式 :
P n n = n ! P_n^n = n! Pnn=n!
全排列模板(阶乘模板):
cpp
#define MOD 10//取模根据实际情况调整
typedef long long ll;
//阶乘
ll Fact(ll n)
{
ll sum = 1;
for (int i = 1;i <= n;i++)
{
sum = ((sum % MOD) * (i % MOD) % MOD);
}
return sum;
}
排列公式:
P n k = n ! ( n − k ) ! ( n ≥ k ≥ 0 ) P_n^k = \frac{n!}{(n-k)!} \quad (n \ge k \ge 0) Pnk=(n−k)!n!(n≥k≥0)
cpp
#define MOD 10
typedef long long ll;
//阶乘
ll Fact(ll n)
{
ll sum = 1;
for (int i = 1;i <= n;i++)
{
sum = ((sum % MOD) * (i % MOD) % MOD);
}
return sum;
}
//排列
ll P(ll n,ll m)
{
return Fact(n) / Fact(n - m);
}
4. 错排
错排其实和排列有一点关系,可以用容斥原理 得出公式,这里不展开,主要讲说利用递推公式计算错排的c++代码模板:
{ D 1 = 0 D 2 = 1 D n = ( n − 1 ) × ( D n − 1 + D n − 2 ) ( n ≥ 3 ) \begin{cases} D_1 = 0 \\ D_2 = 1 \\ D_n = (n-1) \times (D_{n-1} + D_{n-2}) \quad (n \ge 3) \end{cases} ⎩ ⎨ ⎧D1=0D2=1Dn=(n−1)×(Dn−1+Dn−2)(n≥3)
还有3,4的错排可以直接记忆:
{ D 3 = 2 D 4 = 9 \begin{cases} D_3 = 2 \\ D_4 = 9 \\ \end{cases} {D3=2D4=9
下面我们利用上述公式给出代码:
cpp
#define MOD 10//根据题目要求调整
const int MAXN = 1e6;
ll D[MAXN] = { 0 }; // 全局数组默认初始化为0
ll Init_D(ll n)
{
D[1] = 0;
D[2] = 1;
for (ll i = 3; i <= n;i++)
{
D[i] = ((i - 1) % MOD * (D[i - 1] + D[i - 2]) % MOD) % MOD;
}
return D[n];
}
5. 快速幂
快速幂的原理:
① a x + y = a x × a y ①a ^ {x+y} =a ^x×a ^y ①ax+y=ax×ay
② a n = a p 0 × 2 0 + p 1 × 2 1 + p 2 × 2 2 + . . . . . . + p z × 2 z ②a ^n = a ^ {p_0 ×2^0 + p_1 ×2^1 + p_2 ×2^2 + ...... + p_z ×2^z} ②an=ap0×20+p1×21+p2×22+......+pz×2z
这里将 n n n转化为 a a a 的多项式,z 表示二进制的最高位( 0 0 0或 1 1 1),那我们就能继续拆解成:
a n = a p 0 × 2 0 × a p 1 × 2 1 × a p 2 × 2 2 × . . . × a p z × 2 z a ^n = a ^ {p_0 ×2^0} × a ^ {p_1 ×2^1} × a ^ {p_2 ×2^2 }× ... × a ^ {p_z ×2^z} an=ap0×20×ap1×21×ap2×22×...×apz×2z
这就自然而然成了一个累乘模板:
cpp
long long quick_pow(long long a, long long n) {
long long ans = 1;
while (n > 0) {
if (n & 1) { // 如果该二进制位存在
ans = ans * a % MOD;
}
a = a * a % MOD;
n >>= 1; // n除以2,判断下一个二进制位
}
return ans;
}
使用:
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll quick_pow(ll b,ll p,ll k)
{
ll ans = 1;
//取二进制位
while(p > 0)
{
if(p & 1) ans = ans * b % k;
b = b * b % k;
p >>= 1;
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
static ll k = 0;
ll b = 0,p = 0;
cin >> b >> p >> k;
cout << quick_pow(b,p,k);
return 0;
}
6. 唯一分解定理
原理:任何一个大于 1 的正整数,都可以唯一地分解成若干个质数的乘积(不考虑质数的排列顺序)。
数学直观 :
n = p 1 k 1 × p 2 k 2 × ⋯ × p m k m n = p_1^{k1} × p _2^{k2} ×⋯×p_m^{km} n=p1k1×p2k2×⋯×pmkm
其中 p m 为第 m 个质数 其中p_m为第m个质数 其中pm为第m个质数
代码模板:
cpp
void prime(int n)
{
for(int i = 2;i*i <= n;++i)
{
if(n%i == 0)
{
int cnt = 0;
while(n%i == 0)
{
n /= i;
cnt++;
}
cout << i << "^" << cnt << " * ";
}
} //出循环意味着n不能继续分解 :1. n为 1 ; 2. n为质数
if(n > 1) cout << n << "^" << "1" << "\n";
else cout << "1";
}
应用:
- 求最大公约数(GCD): 两个数的 GCD 等于它们公共质因数的最低幂次乘积(比如 12=2²×3,18=2×3²,GCD=2¹×3¹=6);
- 求最小公倍数(LCM): 两个数的 LCM 等于它们所有质因数的最高幂次乘积(比如 12 和 18 的 LCM=2²×3²=36);
- 其他数论问题: 求一个数的约数个数(约数个数 =(k₁+1)(k₂+1)...(kₘ+1))、约数和等...
7. GCD(最大公约数)
一般求GCD采用欧几里得算法,也叫辗转相除法。它的核心思想是:通过一系列的除法操作,不断缩小问题的规模,最终能够求出最大公约数。
下面给出证明:

用递归的模板:
cpp
int gcd(int a, int b) {
return b ? gcd(b,a % b) : a;
}
8. LCM(最小公倍数)
原理:最小公倍数与最大公约数存在固定关系,两个数的乘积等于它们的最大公约数与最小公倍数的乘积。
核心公式:
L C M ( a , b ) = ∣ a × b ∣ G C D ( a , b ) LCM(a,b) = \frac{|a \times b|}{GCD(a,b)} LCM(a,b)=GCD(a,b)∣a×b∣
注意:计算时要先除后乘,避免数据溢出,这是最安全的写法。
用递归的模板:
cpp
typedef long long ll;
ll gcd(ll a, ll b) {
return b ? gcd(b, a % b) : a;
}
// 最小公倍数
ll lcm(ll a, ll b) {
// 先除后乘,防止溢出
return a / gcd(a, b) * b;
}
完整使用示例:
cpp
#include <iostream>
using namespace std;
typedef long long ll;
ll gcd(ll a, ll b) {
return b ? gcd(b, a % b) : a;
}
ll lcm(ll a, ll b) {
return a / gcd(a, b) * b;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll a, b;
cin >> a >> b;
cout << "GCD: " << gcd(a, b) << "\n";
cout << "LCM: " << lcm(a, b) << "\n";
return 0;
}
- 本节完...