【算法基础篇】(四十六)同余方程终极攻略:从基础转化到实战破解


目录

​编辑

前言

一、同余方程的核心概念:从定义到转化

[1.1 同余方程的定义](#1.1 同余方程的定义)

关键说明:

[1.2 同余方程与线性不定方程的转化](#1.2 同余方程与线性不定方程的转化)

[1.3 解的存在性判定:裴蜀定理的应用](#1.3 解的存在性判定:裴蜀定理的应用)

示例验证:

二、核心求解工具:扩展欧几里得算法

[2.1 扩展欧几里得算法的回顾](#2.1 扩展欧几里得算法的回顾)

算法核心推导:

[C++ 实现扩展欧几里得算法](#C++ 实现扩展欧几里得算法)

[2.2 同余方程的通解推导](#2.2 同余方程的通解推导)

[步骤 1:求不定方程ax + my = b的特解](#步骤 1:求不定方程ax + my = b的特解)

[步骤 2:推导通解](#步骤 2:推导通解)

[步骤 3:同余方程的最小正整数解](#步骤 3:同余方程的最小正整数解)

[示例推导:求4x ≡ 2(mod 6)的最小正整数解](#示例推导:求4x ≡ 2(mod 6)的最小正整数解)

[三、实战例题 1:牛客网【模板】同余方程](#三、实战例题 1:牛客网【模板】同余方程)

[3.1 题目分析](#3.1 题目分析)

[3.2 C++ 实现](#3.2 C++ 实现)

[3.3 代码验证](#3.3 代码验证)

[3.4 关键优化](#3.4 关键优化)

[四、实战例题 2:洛谷 P1516 青蛙的约会](#四、实战例题 2:洛谷 P1516 青蛙的约会)

[4.1 题目分析](#4.1 题目分析)

[4.2 C++ 实现](#4.2 C++ 实现)

[4.3 代码验证](#4.3 代码验证)

[4.4 细节处理](#4.4 细节处理)

五、常见误区与避坑指南

[5.1 方程转化错误](#5.1 方程转化错误)

[5.2 忽略解的存在性判定](#5.2 忽略解的存在性判定)

[5.3 最小正整数解计算错误](#5.3 最小正整数解计算错误)

[5.4 数据溢出问题](#5.4 数据溢出问题)

[5.5 混淆通解的周期](#5.5 混淆通解的周期)

总结


前言

在算法竞赛的数论战场中,同余方程是绕不开的核心考点。它看似抽象难懂,实则是 "线性不定方程" 的另一种表现形式,只要掌握了转化技巧和求解方法,就能轻松破解各类问题。从简单的ax ≡ b(mod m)求解,到青蛙约会这类趣味应用题,同余方程的身影无处不在。本文将从同余方程的定义切入,详解其与不定方程的转化逻辑,依托扩展欧几里得算法搭建求解框架,结合例题,手把手教你从理论到实战的全流程,让你在同余问题中不再迷茫。下面就让我们正式开始吧!


一、同余方程的核心概念:从定义到转化

1.1 同余方程的定义

同余方程的标准形式为:ax ≡ b(mod m),其中 a、b、m 是给定的整数,x 是待求的整数解。它的含义是:ax 除以 m 的余数与 b 除以 m 的余数相等 ,即**ax - b能被 m 整除** (记作m | (ax - b))。

举个直观的例子:4x ≡ 2(mod 6),这个方程的解是 x=2(4×2=8,8 mod 6=2)、x=5(4×5=20,20 mod 6=2)等,存在无数个整数解。

关键说明:

  • 解的存在性 :并非所有同余方程都有解。例如2x ≡ 1(mod 4),无论 x 取何整数,2x 都是偶数,偶数 mod 4 的结果只能是 0 或 2,无法等于 1,因此无解;
  • 解的形式:若方程有解,则解一定是周期性的,存在无数个整数解(后续将推导通解公式);
  • 核心目标:竞赛中通常要求求出最小正整数解,或判断方程是否无解。

1.2 同余方程与线性不定方程的转化

要解决同余方程,关键一步是将其转化为我们熟悉的 "二元一次不定方程"。根据同余方程的定义ax ≡ b(mod m),可推导如下:

  1. m | (ax - b),可知存在整数 y,使得ax - b = my
  2. 移项后得到:ax - my = b
  3. y' = -y(y' 也是整数),方程进一步转化为:ax + my' = b

此时,同余方程ax ≡ b(mod m)的求解问题,就等价于二元一次不定方程ax + my = b的整数解求解问题。而判断同余方程是否有解,也转化为判断不定方程是否有解 ------ 这正是裴蜀定理的用武之地!

1.3 解的存在性判定:裴蜀定理的应用

根据裴蜀定理,二元一次不定方程ax + by = c有整数解的充要条件gcd(a, b) | c(即 c 能被 a 和 b 的最大公约数整除)。

对应到同余方程转化后的不定方程ax + my = b,其有解的充要条件gcd(a, m) | b。这也是判断同余方程ax ≡ b(mod m)是否有解的核心准则。

示例验证:

  • 方程4x ≡ 2(mod 6):a=4,m=6,b=2,gcd(4,6)=2,2 能整除 2,因此有解;
  • 方程2x ≡ 1(mod 4):a=2,m=4,b=1,gcd(2,4)=2,2 不能整除 1,因此无解;
  • 方程3x ≡ 1(mod 10):a=3,m=10,b=1,gcd(3,10)=1,1 能整除 1,因此有解。

二、核心求解工具:扩展欧几里得算法

当同余方程有解时,我们需要求出具体的解。此时,扩展欧几里得算法(exgcd)成为核心工具 ------ 它不仅能计算 a 和 m 的最大公约数,还能同时求出不定方程ax + my = gcd(a, m)的一组特解,进而推导出同余方程的通解和最小正整数解。

2.1 扩展欧几里得算法的回顾

扩展欧几里得算法的核心逻辑基于欧几里得算法(辗转相除法)的递归过程,其核心思想是:在计算gcd(a, m)的同时,反向推导不定方程ax + my = gcd(a, m)的特解

算法核心推导:

  1. 递归终止条件 :当 m=0 时,gcd(a, 0)=a,此时方程ax + 0*y = a的一组特解为x=1,y=0
  2. 递归过程 :假设我们已经求出gcd(m, a mod m)的一组特解(x1, y1),即m*x1 + (a mod m)*y1 = gcd(a, m)
  3. 由于*a mod m = a - floor(a/m)m(floor (a/m) 表示 a 除以 m 的商),代入上式得:m∗x1+(a−floor(a/m)∗m)∗y1=gcd(a,m)
  4. 整理后:a∗y1+m∗(x1−floor(a/m)∗y1)=gcd(a,m)
  5. 对比目标方程a*x + m*y = gcd(a, m),可得当前层的特解:x=y1 y=x1−floor(a/m)∗y1

C++ 实现扩展欧几里得算法

cpp 复制代码
#include <iostream>
using namespace std;

typedef long long LL;

// 扩展欧几里得算法:返回gcd(a,b),并通过引用返回方程ax+by=gcd(a,b)的一组特解(x,y)
LL exgcd(LL a, LL b, LL& x, LL& y) {
    if (b == 0) {
        x = 1, y = 0;
        return a;
    }
    LL x1, y1, d;
    d = exgcd(b, a % b, x1, y1);
    // 推导当前层特解
    x = y1;
    y = x1 - (a / b) * y1;
    return d;
}

int main() {
    LL a = 4, m = 6;
    LL x, y;
    LL d = exgcd(a, m, x, y);
    cout << "gcd(" << a << ", " << m << ") = " << d << endl;
    cout << "方程 " << a << "x + " << m << "y = " << d << " 的特解:x=" << x << ", y=" << y << endl;
    // 输出:gcd(4,6)=2;特解x= -1, y=1(4*(-1) +6*1=2)
    return 0;
}

2.2 同余方程的通解推导

当我们通过扩展欧几里得算法求出不定方程ax + my = gcd(a, m)的一组特解(x0, y0)后,需要进一步推导同余方程ax ≡ b(mod m)的通解。

步骤 1:求不定方程ax + my = b的特解

d = gcd(a, m),由于方程有解(d | b),令k = b / d。将特解(x0, y0)缩放 k 倍,得到ax + my = b的一组特解:x1=x0∗k y1=y0∗k

步骤 2:推导通解

不定方程ax + my = b的通解公式为:x=x1+k∗(m/d)(k∈Z) y=y1−k∗(a/d)(k∈Z)

其中,m/d是 x 的增量(周期),k 取任意整数时,x 和 y 都满足方程。

步骤 3:同余方程的最小正整数解

由于 x 的通解是周期性的,周期为m/d,因此同余方程ax ≡ b(mod m)的最小正整数解为: x min=(x1 mod (m/d)+(m/d)) mod (m/d)

添加(m/d)再取模是为了避免 x1 为负数时出现负解。

示例推导:求4x ≡ 2(mod 6)的最小正整数解

  1. 转化为不定方程:4x + 6y = 2
  2. 计算d = gcd(4,6)=2,k=2/2=1;
  3. 4x +6y=2的特解:通过 exgcd 求出4x +6y=2的特解x0=-1, y0=1,缩放 k=1 后,特解x1=-1
  4. 通解:x = -1 + k*(6/2) = -1 + 3k(k 为整数);
  5. 最小正整数解:( -1 mod 3 + 3 ) mod 3 = (2 + 3) mod 3=2,即 x=2(与之前的示例一致)。

三、实战例题 1:牛客网【模板】同余方程

题目链接:https://ac.nowcoder.com/acm/problem/229005

3.1 题目分析

题目描述 :求关于 x 的同余方程ax ≡ 1(mod b)的最小正整数解,若无解输出 "-1"。

输入描述 :第一行一个整数 T,表示 T 组数据;每组数据一行两个正整数 a、b(2≤a,b≤2×10⁹)。输出描述 :每组数据输出最小正整数解或 "-1"。示例输入 :23 102 4示例输出:7-1

核心思路

  • 方程转化为ax + by = 1(不定方程);
  • 有解的充要条件是gcd(a, b)=1(因为 b 是模数,1 必须被 gcd (a,b) 整除);
  • 用 exgcd 求出特解,再转化为最小正整数解。

3.2 C++ 实现

cpp 复制代码
#include <iostream>
using namespace std;

typedef long long LL;

LL exgcd(LL a, LL b, LL& x, LL& y) {
    if (b == 0) {
        x = 1, y = 0;
        return a;
    }
    LL x1, y1, d;
    d = exgcd(b, a % b, x1, y1);
    x = y1;
    y = x1 - (a / b) * y1;
    return d;
}

LL solve(LL a, LL b) {
    LL x, y;
    LL d = exgcd(a, b, x, y);
    if (d != 1) {
        return -1; // 无解
    }
    // 转化为最小正整数解
    x = (x % b + b) % b;
    return x;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int T;
    cin >> T;
    while (T--) {
        LL a, b;
        cin >> a >> b;
        LL ans = solve(a, b);
        cout << ans << endl;
    }
    return 0;
}

3.3 代码验证

  • 第一组输入:3 10 → 方程3x ≡1(mod10)
    • gcd(3,10)=1,有解;
    • exgcd 求出特解x=7, y=-2(3×7 +10×(-2)=1);
    • 最小正整数解为 7,输出 7;
  • 第二组输入:2 4 → 方程2x ≡1(mod4)
    • gcd(2,4)=2≠1,无解,输出 - 1。

3.4 关键优化

  • 数据范围:a 和 b 可达 2×10⁹,必须使用 long long 避免溢出;
  • 最小正整数解计算(x % b + b) % b确保结果为正,即使 x 是负数;
  • 效率:exgcd 的时间复杂度为 O (log min (a,b)),对于 2×10⁹的数据完全可以毫秒级完成。

四、实战例题 2:洛谷 P1516 青蛙的约会

题目链接:https://www.luogu.com.cn/problem/P1516

4.1 题目分析

题目描述:两只青蛙在纬度线上跳跃,青蛙 A 从坐标 x 出发,每次跳 m 米;青蛙 B 从坐标 y 出发,每次跳 n 米。纬度线总长 L 米(首尾相接),求它们跳多少次后碰面,若永远不能碰面输出 "Impossible"。

输入描述:一行五个整数 x、y、m、n、L。

输出描述:碰面次数或 "Impossible"。

示例输入:1 2 3 4 5 → 输出:4。

核心思路

  1. 设跳 t 次后碰面,此时青蛙 A 的坐标为x + mt,青蛙 B 的坐标为y + nt
  2. 碰面条件:(x + mt) - (y + nt) = kL(k 为整数,A 比 B 多跳 k 圈);
  3. 整理方程:(m - n)t - Lk = y - x
  4. a = m - nb = Lc = y - x,方程转化为同余方程a*t ≡ c(mod b)
  5. 求解该同余方程,最小正整数解 t 即为答案。

4.2 C++ 实现

cpp 复制代码
#include <iostream>
using namespace std;

typedef long long LL;

LL exgcd(LL a, LL b, LL& x, LL& y) {
    if (b == 0) {
        x = 1, y = 0;
        return a;
    }
    LL x1, y1, d;
    d = exgcd(b, a % b, x1, y1);
    x = y1;
    y = x1 - (a / b) * y1;
    return d;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    LL x, y, m, n, L;
    cin >> x >> y >> m >> n >> L;
    
    LL a = m - n;
    LL b = L;
    LL c = y - x;
    
    // 处理a为负数的情况,避免影响后续计算
    if (a < 0) {
        a = -a;
        c = -c;
    }
    
    LL x0, y0, d;
    d = exgcd(a, b, x0, y0);
    
    if (c % d != 0) {
        cout << "Impossible" << endl;
    } else {
        // 求特解
        x0 = x0 * (c / d);
        // 通解:t = x0 + k*(b/d),求最小正整数解
        LL k1 = b / d;
        x0 = (x0 % k1 + k1) % k1;
        cout << x0 << endl;
    }
    
    return 0;
}

4.3 代码验证

示例输入:1 2 3 4 5 → x=1,y=2,m=3,n=4,L=5;

  • a = 3-4 = -1 → 处理后 a=1;
  • c = 2-1 = 1 → 处理后 c=-1;
  • 方程转化为1*t ≡ -1(mod 5) → 等价于t ≡4(mod5)
  • exgcd 求出1*t +5*y = -1的特解x0=-1
  • 最小正整数解:(-1 mod5 +5) mod5=4,输出 4(跳 4 次后碰面)。

4.4 细节处理

  • a 为负数:当 m < n 时,a = m-n 为负数,此时将 a 和 c 同时取反,方程等价不变;
  • 最小正整数解 :通过(x0 % k1 + k1) % k1确保结果为正;
  • 无解判断:当 c 不能被 d 整除时,输出 "Impossible"。

五、常见误区与避坑指南

5.1 方程转化错误

  • 误区 :将同余方程ax ≡ b(mod m)错误转化为ax - my = -b或其他形式,导致后续求解错误;
  • 避坑 :严格按照定义推导,确保转化后的不定方程为ax + my = b(或等价形式),避免符号错误。

5.2 忽略解的存在性判定

  • 误区 :未判断gcd(a,m) | b就直接求解,导致无效计算;
  • 避坑 :先计算d = gcd(a,m),若b % d != 0,直接输出无解,无需后续步骤。

5.3 最小正整数解计算错误

  • 误区:直接对特解 x1 取模,未处理负数情况(如 x1=-1,m/d=3,直接取模得 - 1,而非 2);
  • 避坑 :使用公式(x1 % (m/d) + (m/d)) % (m/d),确保结果为正整数。

5.4 数据溢出问题

  • 误区:使用 int 类型存储 a、m、b 等变量,当数据达到 1e9 时发生溢出;
  • 避坑 :所有变量统一使用 long long 类型,尤其是在计算a*xm*y等乘积时。

5.5 混淆通解的周期

  • 误区 :将通解中 x 的周期误认为 m,而非m/d
  • 避坑 :牢记通解周期为m/d(d=gcd (a,m)),例如4x ≡2(mod6)中,d=2,周期为 6/2=3,与示例中 x=2、5、8... 一致。

总结

同余方程是数论中的核心考点,其求解的核心逻辑是 "转化为不定方程 + 扩展欧几里得算法求解"。如果在学习过程中遇到具体题目无法解决,或想了解中国剩余定理、快速乘等延伸知识点,可以随时留言交流。后续将持续更新数论进阶内容,敬请关注!

相关推荐
你怎么知道我是队长2 小时前
C语言---强制类型转换
c语言·开发语言·算法
程序员泡椒4 小时前
二分查找Go版本实现
数据结构·c++·算法·leetcode·go·二分
小雨下雨的雨4 小时前
Flutter鸿蒙共赢——墨染算法:柏林噪声与鸿蒙生态中的数字水墨意境
算法·flutter·华为·交互·harmonyos·鸿蒙
NAGNIP10 小时前
万字长文!回归模型最全讲解!
算法·面试
知乎的哥廷根数学学派10 小时前
面向可信机械故障诊断的自适应置信度惩罚深度校准算法(Pytorch)
人工智能·pytorch·python·深度学习·算法·机器学习·矩阵
txinyu的博客10 小时前
解析业务层的key冲突问题
开发语言·c++·分布式
666HZ66611 小时前
数据结构2.0 线性表
c语言·数据结构·算法
SmartRadio11 小时前
ESP32添加修改蓝牙名称和获取蓝牙连接状态的AT命令-完整UART BLE服务功能后的完整`main.c`代码
c语言·开发语言·c++·esp32·ble
实心儿儿12 小时前
Linux —— 基础开发工具5
linux·运维·算法