当前位置: 首页 > news >正文

算法中的数论基础

算法中的数论基础

本篇文章适用于算法考试或比赛之前的临场复习记忆,没有复杂公式推理,基本上是知识点以及函数模版,涵盖取模操作、位运算的小技巧、组合数、概率期望、进制转换、最大公约数、最小公倍数、唯一分解定理、素数、快速幂等知识点

文章目录

  • 算法中的数论基础
    • 一、取模操作
    • 二、位运算的小技巧
        • 1.基础位运算
        • 2.给一个数n,确定它的二进制表示中的第x位是0还是一
        • 3.将一个数的二进制表示中的第x位修改为0或1
        • 4.位图的思想
        • 5.提取一个数(n)二进制中最右边的1
        • 6.干掉一个数(n)二进制表示中最右侧的1
        • 7.位运算的优先级
        • 9,异或运算
    • 三、组合数
      • 组合数的思想
      • 如何应用组合数
      • 注意事项
    • 四、概率论与期望集定义式
        • C++中的实现方法
          • 1. 直接计算期望(小规模问题)
          • 2. 动态规划(中等规模问题)
        • 注意事项与优化技巧
    • 五、进制转换
      • C++中进制转换详解(二进制、十进制、十六进制互转)
        • 一、核心方法总结
        • 二、具体实现方法
          • 1. 十进制 → 二进制
          • 2. 十进制 → 十六进制
          • 3. 二进制 → 十进制
          • 4. 十六进制 → 十进制
          • 5. 二进制 ↔ 十六进制(直接转换)
        • 三、应用场景与注意事项
    • 六、最大公约数,最小公倍数,唯一分解定理
        • 最大公约数(GCD)
        • 最小公倍数(LCM)
        • 唯一分解定理(质因数分解)
    • 七、素数
      • 素数判断方法
        • 1. 试除法(Trial Division)
      • 素数筛法
        • 1. 埃拉托斯特尼筛法(Sieve of Eratosthenes)
        • 2. 欧拉筛法(线性筛法)
    • 八、快速幂,费马小定理求逆元
      • 快速幂
      • 费马小定理求逆元
      • 注意事项
    • 九、排列组合,第二类斯特林数
      • 一、排列组合
        • 1. 概念
        • 2. 实现方法
      • 二、第二类斯特林数(Stirling Numbers of the Second Kind)
        • 1. 概念
        • 2. 实现方法
        • 3. 使用场景

一、取模操作

取模也叫取余,设 a,b 为两个给定的整数,a≠0。设 dd 是一个给定的整数。那么,一定存在唯一的一对整数 qq 和 rr,满足 b=qa+r,r<b。也就是我们很就学过的除法取余。

在编程语言中,我们常用 % 符号代表取模。

在比赛题目中,当结果非常大时,通常要求取模运算,取模有如下性质:

  • (a % M + b % M) % M = (a + b) % M
  • (a % M) * (b % M) % M = (a * b) % M

所以在编程比赛中,为了防止整数溢出,基本每进行一次运算就要一次取模,因此常常会看到:

(a * b % M + c) % M * d % M

注意:取模仅对加法和乘法满足上述性质,但是对除法不满足

二、位运算的小技巧

1.基础位运算

包括:<< >> ~ & | ^ 这些运算符的使用方法

2.给一个数n,确定它的二进制表示中的第x位是0还是一
if((n >> x) & 1)	return 1;
else return 0;
3.将一个数的二进制表示中的第x位修改为0或1
//修改为0
n = n & (~(1 << x))
//修改为1
n = n | (1 << x)
4.位图的思想

将一个变量的每一个二进制位看成一个标志位,这就像STM32中的寄存器一样,每一位存放不同的数代表不同的状态

5.提取一个数(n)二进制中最右边的1
n & (-n);

示例:

 0110101000//n
 1001010111//n的反码
 1001011000//加1之后的补码
&0110101000
 0000001000
6.干掉一个数(n)二进制表示中最右侧的1
n & (n - 1);
7.位运算的优先级

对于位运算的优先级,能用括号就用括号

9,异或运算
//1. a ^ 0 = a;
//2. a ^ a = 0;
//3. a ^ b ^ c = a ^ (b ^ c);
  • a + b == 2 * (a & b) + (a ^ b)`:同学们可以尝试证明一下。
  • Lowbit(x) == x & (-x):获取整数 xx 的最低位。
  • a ^ b <= a + b :可通过位的异或性质具体证明。

三、组合数

组合数的思想

组合数C(n,k)表示从n个元素中选取k个元素的方案数,其核心在于无重复、无顺序的选择。这种思想常用于需要枚举所有可能组合或计算组合数量的场景。

如何应用组合数

  1. 直接计算组合数

    • 公式法:利用阶乘直接计算,但需注意溢出,适用于小规模数据。

      int combination(int n, int k) 
      {
          if (k > n) return 0;
          if (k == 0 || k == n) return 1;
          return combination(n - 1, k - 1) + combination(n - 1, k);
      }
      
    • 动态规划预处理:适用于多次查询,结合模运算避免溢出。

      const int MOD = 1e9 + 7;
      vector<int> fact, inv;
      
      // 计算 (a^b) % mod,使用快速幂算法
      int pow_mod(int a, int b, int mod) 
      {
          int ret = 1;
          a %= mod;  										// 确保 a 在 mod 范围内
          while (b > 0) 
          {
              if (b % 2 == 1)   							// 如果当前二进制位为1,乘上对应的幂
                  ret = (ret * 1LL * a) % mod;  			// 注意用 1LL 避免溢出
      
              a = (a * 1LL * a) % mod;  					// 平方并取模
              b /= 2;  									// 移动到下一个二进制位
          }
          return ret;
      }
      
      // 预处理阶乘和逆元
      void precompute(int max_n) 
      {
          fact.resize(max_n + 1);
          inv.resize(max_n + 1);
          fact[0] = 1;
          for (int i = 1; i <= max_n; ++i)
              fact[i] = (1LL * fact[i - 1] * i) % MOD;
          
          inv[max_n] = pow_mod(fact[max_n], MOD - 2, MOD); // 快速幂求逆元
          for (int i = max_n - 1; i >= 0; --i)
              inv[i] = (1LL * inv[i + 1] * (i + 1)) % MOD;
      }
      int C(int n, int k) 
      {
          if (k < 0 || k > n) return 0;
          return (1LL * fact[n] * inv[k] % MOD) * inv[n - k] % MOD;
      }
      

      注意:

      1. (1LL * fact[n] * inv[k] % MOD) * inv[n - k] % MOD;
  2. 生成所有组合

    • 回溯法:通过递归生成所有可能的组合。

      #include <vector>
      using namespace std;
      
      void dfs(int s, int n, int k, vector<int>& path, vector<vector<int>>& ret) 
      {
          if (path.size() == k) 
          {
              ret.push_back(path);
              return;
          }
          // 提前剪枝:剩余元素不足以填满组合时提前终止
          for (int i = s; i <= n - (k - path.size()) + 1; ++i) 
          {
              path.push_back(i);
              dfs(i + 1, n, k, path, ret);
              path.pop_back();
          }
      }
      
      vector<vector<int>> combine(int n, int k) 
      {
          vector<vector<int>> ret;
          vector<int> path;
          dfs(1, n, k, path, ret);
          return ret;
      }
      

注意事项

  • 溢出问题:当n较大时,直接计算阶乘会导致溢出,需使用模运算和预处理。
  • 效率考量:生成所有组合的时间复杂度为O(C(n,k)),应避免在n较大时使用。
  • 剪枝优化:在回溯过程中及时剪枝(如剩余元素不足时终止递归),提升效率。

四、概率论与期望集定义式

C++中的实现方法
1. 直接计算期望(小规模问题)
  • 场景:状态数少且概率明确的问题(如简单骰子问题)。

  • 示例代码

    double calculateDiceExpectation() {
        double expectation = 0.0;
        for (int i = 1; i <= 6; ++i) {
            expectation += i * (1.0 / 6.0);
        }
        return expectation; // 输出3.5
    }
    
2. 动态规划(中等规模问题)
  • 场景:状态转移具有概率依赖的问题(如随机游走、迷宫逃脱)。

  • 示例问题
    一个迷宫中有陷阱(概率( p )死亡)和出口(概率( 1-p )逃脱),求逃脱的期望步数。

  • 递推公式
    [
    E[i] = 1 + p \cdot E[\text{死亡}] + (1-p) \cdot E[\text{逃脱}]
    ]
    简化后:
    [
    E[i] = \frac{1}{1-p} \quad (\text{若死亡则期望为无穷大})
    ]

  • 代码实现

    double mazeEscapeExpectation(double p) {
        if (p >= 1.0) return INFINITY; // 必死情况
        return 1.0 / (1.0 - p);
    }
    
注意事项与优化技巧
  1. 浮点数精度问题
    • 使用double类型时,避免连续乘除导致精度损失。
  • 对精度敏感的问题可改用分数形式(如分子分母分开存储)。
  1. 状态压缩与记忆化

    • 当状态参数较多时,用位运算或哈希表压缩状态。
    • 示例
    unordered_map<State, double> memo;
     double dp(State s) {
         if (memo.count(s)) return memo[s];
         // 计算逻辑
         return memo[s] = result;
     }

五、进制转换

C++中进制转换详解(二进制、十进制、十六进制互转)

一、核心方法总结
转换方向标准库方法手动实现法应用场景
十进制 → 其他cout格式控制符、bitset除基取余法输出格式化、算法题快速实现
其他 → 十进制stoi()/stol()指定基数按权展开计算输入解析、自定义转换逻辑
二 ↔ 十六通过十进制中转或二进制分组转换四位二进制对应一位十六进制数据压缩、内存地址处理
二、具体实现方法
1. 十进制 → 二进制

方法1:使用 bitset(固定位数)

#include <bitset>
int num = 42;
cout << "二进制: " << bitset<8>(num) << endl; // 输出00101010

方法2:递归除基取余法(动态位数)

string decimalToBinary(int n) {
    if (n == 0) return "0";
    string bin;
    while (n > 0) {
        bin = to_string(n % 2) + bin;
        n /= 2;
    }
    return bin;
}
// 调用示例:cout << decimalToBinary(42); // 输出101010
2. 十进制 → 十六进制

方法1:使用 cout 格式控制符

int num = 255;
cout << "十六进制(小写): " << hex << num << endl;    // 输出ff
cout << "十六进制(大写): " << uppercase << hex << num; // 输出FF

方法2:手动转换(支持负数)

string decimalToHex(int num) {
    if (num == 0) return "0";
    const char hexDigits[] = "0123456789ABCDEF";
    string hex;
    unsigned int n = num; // 处理负数转为补码
    while (n > 0) {
        hex = hexDigits[n % 16] + hex;
        n /= 16;
    }
    return hex;
}
// 调用示例:cout << decimalToHex(-42); // 输出FFFFFFD6
3. 二进制 → 十进制

方法1:使用 stoi 函数

string binStr = "101010";
int decimal = stoi(binStr, nullptr, 2); // 第二个参数为终止位置指针
cout << decimal; // 输出42

方法2:按权展开计算

int binaryToDecimal(string binStr) {
    int dec = 0;
    for (char c : binStr) {
        dec = dec * 2 + (c - '0');
    }
    return dec;
}
// 调用示例:cout << binaryToDecimal("101010"); // 输出42
4. 十六进制 → 十进制

方法1:使用 stoi 函数

string hexStr = "FF";
int decimal = stoi(hexStr, nullptr, 16); // 第三个参数指定基数
cout << decimal; // 输出255

方法2:手动转换(支持大小写)

int hexCharToValue(char c) {
    if (isdigit(c)) return c - '0';
    c = toupper(c);
    return 10 + (c - 'A');
}

int hexToDecimal(string hexStr) {
    int dec = 0;
    for (char c : hexStr) {
        dec = dec * 16 + hexCharToValue(c);
    }
    return dec;
}
// 调用示例:cout << hexToDecimal("1a"); // 输出26
5. 二进制 ↔ 十六进制(直接转换)

核心思路:每4位二进制对应1位十六进制

string binaryToHex(string binStr) {
    // 补齐到4的倍数位(左侧补0)
    binStr = string((4 - binStr.size() % 4) % 4, '0') + binStr;
    
    const string hexDigits = "0123456789ABCDEF";
    string hex;
    for (size_t i = 0; i < binStr.size(); i += 4) {
        string chunk = binStr.substr(i, 4);
        int value = bitset<4>(chunk).to_ulong();
        hex += hexDigits[value];
    }
    // 去除前导0(保留最后一个0)
    hex.erase(0, hex.find_first_not_of('0'));
    return hex.empty() ? "0" : hex;
}

// 调用示例:binaryToHex("101010") → "2A"
三、应用场景与注意事项
  1. 算法题常见应用

    • 位运算优化:二进制转换常用于位掩码操作(如状态压缩)
    • 内存地址处理:十六进制用于表示内存地址(如0x7ffeeb0a7c
    • 文件格式解析:如解析PNG文件的IHDR块中的宽度/高度(十六进制)
  2. 关键注意事项

    • 溢出处理:使用stolstoull处理大数(如stoull("FFFF", nullptr, 16)
    • 输入验证:检查非法字符(如二进制字符串中出现非0/1字符)
    bool isValidBinary(string s) {
        return s.find_first_not_of("01") == string::npos;
    }
    
    • 负数处理:手动实现时需考虑补码形式(如bitset<32>(-42).to_string()
  3. 性能优化技巧

    • 预处理映射表:提前建立二进制到十六进制的映射表
    unordered_map<string, char> binToHexMap = {
        {"0000", '0'}, {"0001", '1'}, /* ... */ {"1111", 'F'}
    };
    
    • 位运算加速:用移位代替除法(如num >>= 1代替num /= 2

六、最大公约数,最小公倍数,唯一分解定理

最大公约数(GCD)

使用欧几里得算法(辗转相除法),支持处理负数和零:

#include <cstdlib>  // 用于abs函数

int gcd(int a, int b) 
{
    a = abs(a);
    b = abs(b);
    while (b != 0) 
    {
        int tmp = a % b;
        a = b;
        b = tmp;
    }
    return a;
}
最小公倍数(LCM)

基于GCD计算,处理溢出和零的情况:

long long lcm(int a, int b) 
{
    a = abs(a);
    b = abs(b);
    if (a == 0 || b == 0) return 0;  // 0与任何数的LCM为0
    return (a / gcd(a, b)) * (long long)b;  // 防止溢出
}
唯一分解定理(质因数分解)

高效分解整数为质因数乘积,处理负数和特殊值:

#include <vector>
using namespace std;

vector<pair<int, int>> prime_factors(int n) {
    vector<pair<int, int>> factors;
    if (n == 0) return factors;  // 0无法分解
    if (n < 0) 
    {
        factors.emplace_back(-1, 1);  // 处理负号
        n = -n;
    }
    
    // 处理因子2
    if (n % 2 == 0) 
    {
        int cnt = 0;
        while (n % 2 == 0) 
        {
            n /= 2;
            cnt++;
        }
        factors.emplace_back(2, cnt);
    }
    
    // 处理奇数因子
    for (int i = 3; i * i <= n; i += 2) 
    {
        if (n % i == 0) 
        {
            int cnt = 0;
            while (n % i == 0) 
            {
                n /= i;
                cnt++;
            }
            factors.emplace_back(i, cnt);
        }
    }
    
    // 处理剩余的大质数
    if (n > 1) factors.emplace_back(n, 1);
    return factors;
}

七、素数

质数(Prime number,又称素数),指在大于1的自然数中,除了1和该数自身外,无法被其他自然数整除的数(也可定义为只有1与该数本身两个正因数的数)

素数判断方法

1. 试除法(Trial Division)

原理:检查从2到√n的所有整数是否能整除n。
实现代码

bool isPrime(int n) 
{
    if (n <= 1) return false;      // 0和1非素数
    if (n <= 3) return true;       // 2和3是素数
    if (n % 2 == 0) return false;  // 排除偶数
    
    // 只需检查奇数因子到√n
    for (int i = 3; i * i <= n; i += 2)   
        if (n % i == 0) return false;  
    return true;
}

素数筛法

1. 埃拉托斯特尼筛法(Sieve of Eratosthenes)

原理:标记素数的倍数,逐步筛选出所有素数。
实现代码

vector<bool> sieve(int n) 
{
    vector<bool> isPrime(n+1, true);
    isPrime[0] = isPrime[1] = false;
    
    for (int i = 2; i*i <= n; ++i) 
    {
        if (isPrime[i]) 
        {
            for (int j = i*i; j <= n; j += i) // 标记i的倍数(从i*i开始)
                isPrime[j] = false;
        }
    }
    return isPrime;
}

优点:实现简单,适合大规模区间筛素数。


2. 欧拉筛法(线性筛法)

原理:每个合数仅被其最小质因数标记,保证线性时间复杂度。
实现代码

vector<bool> eulerSieve(int n) {
    vector<bool> isPrime(n+1, true);
    vector<int> primes;  // 存储素数
    isPrime[0] = isPrime[1] = false;
    
    for (int i = 2; i <= n; ++i) {
        if (isPrime[i]) primes.push_back(i);
        // 用当前数和已知素数标记合数
        for (int p : primes) {
            if (i * p > n) break;
            isPrime[i * p] = false;
            if (i % p == 0) break;  // 保证只被最小质因数标记
        }
    }
    return isPrime;
}

时间复杂度:( O(n) ),空间复杂度 ( O(n) )。

优点:效率更高,适合需要极高性能的场景。

边界条件:注意处理 ( n = 0, 1 ) 等特殊情况。

八、快速幂,费马小定理求逆元

快速幂

概念:快速幂是一种高效计算幂运算的算法,将时间复杂度从O(n)降低到O(log n)。其核心思想是通过二分法将指数分解为二进制形式,并利用幂的平方性质减少乘法次数。

实现步骤

  1. 初始化结果为1。
  2. 循环处理指数,当指数大于0时:
    • 若当前指数为奇数,将结果乘以底数并取模。
    • 底数平方并取模,指数右移一位(除以2)。

C++代码示例

long long fast_pow(long long a, long long b, long long mod) {
    long long res = 1;
    a %= mod; // 确保a在mod范围内
    while (b > 0) {
        if (b & 1) res = (res * a) % mod;
        a = (a * a) % mod;
        b >>= 1;
    }
    return res;
}

费马小定理求逆元

概念:当模数p为质数且a与p互质时,a的逆元(即a⁻¹ mod p)可通过费马小定理计算为a^(p-2) mod p。

使用条件

  • 模数p必须是质数。
  • a与p互质(即a不是p的倍数)。

实现方法:直接调用快速幂计算a^(p-2) mod p。

C++代码示例

long long mod_inverse(long long a, long long p) {
    return fast_pow(a, p-2, p);
}

注意事项

  • 模数非质数:使用扩展欧几里得算法求逆元。
  • 溢出问题:确保中间结果不超过数据类型范围,必要时使用快速乘。
  • 输入验证:确保a与p互质,避免求逆元失败。

通过结合快速幂和费马小定理,可以在模数为质数时高效处理涉及除法的模运算问题。

九、排列组合,第二类斯特林数

一、排列组合

1. 概念
  • 排列(Permutation):从 n 个元素中选出 k 个元素 有序排列 的方案数,公式为:
  • 组合(Combination):从 n 个元素中选出 k 个元素 不考虑顺序 的方案数,公式为:
2. 实现方法

在模数 MOD(通常为质数如 1e9+7)下,通过预处理阶乘和阶乘的逆元,实现快速计算:

const int MOD = 1e9+7;
const int MAX_N = 1e5;
long long fact[MAX_N+1], inv_fact[MAX_N+1];

// 预处理阶乘和逆元阶乘
void precompute() {
    fact[0] = 1;
    for (int i = 1; i <= MAX_N; i++) {
        fact[i] = fact[i-1] * i % MOD;
    }
    inv_fact[MAX_N] = fast_pow(fact[MAX_N], MOD-2, MOD); // 费马小定理求逆元
    for (int i = MAX_N-1; i >= 0; i--) {
        inv_fact[i] = inv_fact[i+1] * (i+1) % MOD;
    }
}

// 计算排列 P(n, k)
long long permutation(int n, int k) {
    if (k > n) return 0;
    return fact[n] * inv_fact[n-k] % MOD;
}

// 计算组合 C(n, k)
long long combination(int n, int k) {
    if (k > n) return 0;
    return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
}

二、第二类斯特林数(Stirling Numbers of the Second Kind)

1. 概念
  • 定义:将 n 个不同的元素划分为 k非空集合 的方案数,记为 S(n, k)
  • 递推公式
    在这里插入图片描述
2. 实现方法

通过动态规划递推计算:

const int MAX_N = 1000;
long long stirling[MAX_N+1][MAX_N+1];

void precompute_stirling() {
    stirling[0][0] = 1;
    for (int n = 1; n <= MAX_N; n++) {
        for (int k = 1; k <= n; k++) {
            stirling[n][k] = (stirling[n-1][k-1] + k * stirling[n-1][k]) % MOD;
        }
    }
}


) return 0;
    return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
}
3. 使用场景
  • 计算概率问题时(如抽卡问题)。
  • 动态规划中的状态转移涉及选择元素(如背包问题)。
  • 组合数学问题(如路径计数、多项式展开)。

相关文章:

  • 客运从业资格证适用哪些岗位
  • Linux 多线程编程实战指南
  • 【DeepSeek前沿科技】DeepSeek与Notion知识库搭建指南
  • 多组学空转数据如何进行整合分析?(SpatialGlue库)
  • 驱动-内核空间和用户空间数据交换
  • 生命篇---心肺复苏、AED除颤仪使用、海姆立克急救法、常见情况急救简介
  • 算法学习C++需注意的基本知识
  • CentOS7更换国内YUM源和Docker简单应用
  • 【创建一个YOLO免环境训练包】
  • 入侵检测snort功能概述
  • Java基础 - 泛型(基本概念)
  • 【25软考网工笔记】第二章 数据通信基础(1)信道特性 奈奎斯特 香农定理
  • 使用amos进行简单中介效应分析
  • MySQL 进阶 - 2 ( 9000 字详解)
  • Next.js 简介
  • 自行搭建一个Git仓库托管平台
  • NLP高频面试题(四十一)——什么是 IA3 微调?
  • 国家优青ppt美化_青年科学基金项目B类ppt案例模板
  • 【WPF】自定义控件:ShellEditControl-同列单元格编辑支持文本框、下拉框和弹窗
  • 【解决方案】vscode 不小心打开了列选择模式,选择时只能选中同一列的数据。
  • 自己做网站还是用博客/百度百科官网首页
  • 网站建设公司的电话/网络营销工具分析
  • 建网站没有公司地址怎么办/企业网站源码
  • 网站劫持必须做系统嘛/seo点击排名
  • 鲅鱼圈网站建设/百度搜索引擎推广怎么弄
  • 波波网站建设/如何快速搭建一个网站