有关算法的简单数学问题

欢迎来到 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. 有关质数

  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;
}
  1. 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";
}

应用

  1. ​求最大公约数(GCD): 两个数的 GCD 等于它们公共质因数的最低幂次乘积(比如 12=2²×3,18=2×3²,GCD=2¹×3¹=6);
  2. 求最小公倍数(LCM): 两个数的 LCM 等于它们所有质因数的最高幂次乘积(比如 12 和 18 的 LCM=2²×3²=36);
  3. 其他数论问题: 求一个数的约数个数(约数个数 =(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;
}

  • 本节完...
相关推荐
Halo_tjn2 小时前
Java 接口的定义重构学生管理系统
java·开发语言·算法
jimy12 小时前
故事化叙事的软件/计算机历史节目---非常浪漫与传奇
职场和发展·生活
阿Y加油吧2 小时前
栈的经典应用:从「有效括号」到「寻找两个正序数组的中位数」深度解析
开发语言·python·算法
小杰帅气2 小时前
算法的时间和空间复杂度
数据结构
阿Y加油吧2 小时前
二分查找进阶:旋转排序数组的两道经典题深度解析
数据结构·算法
想带你从多云到转晴2 小时前
05、数据结构与算法---栈与队列
java·数据结构·算法
無限進步D2 小时前
蓝桥杯赛后总结
算法·蓝桥杯·竞赛
QuZero2 小时前
ReentrantLock principle
java·算法
m0_716765232 小时前
数据结构--顺序表的插入、删除、查找详解
c语言·开发语言·数据结构·c++·学习·算法·visual studio