题解:AcWing 204 表达整数的奇怪方式

【题目来源】

AcWing:204 表达整数的奇怪方式 - AcWing题库

【题目描述】

给定 \(2n\) 个整数 \(a_1,a_2,\dots,a_n\)\(m_1,m_2,\dots,m_n\),求一个最小的非负整数 \(x\),满足 \(\forall \in [1,n],x\equiv m_i(mod\ a_i)\)

【输入】

\(1\) 行包含整数 \(n\)

\(2\dots n+1\) 行:每 \(i+1\) 行包含两个整数 \(a_i\)\(m_i\),数之间用空格隔开。

【输出】

输出最小非负整数,如果 \(x\) 不存在,则输出 \(-1\)

【输入样例】

2
8 7
11 9

【输出样例】

31

【核心思想】

  1. 问题分析:给定 \(n\) 个同余方程 \(x \equiv m_i \pmod{a_i}\),求最小非负整数 \(x\) 满足所有方程,若不存在则输出 \(-1\)。这是一个经典的数论问题,核心在于利用中国剩余定理(CRT)将多个同余方程两两合并,逐步求解。

  2. 算法选择

    • 中国剩余定理(CRT):用于求解一组同余方程的公共解。当模数两两互质时,有唯一解模 \(\prod a_i\);当模数不互质时,需要判断方程是否相容(有解条件)
    • 扩展欧几里得算法(exgcd):用于求解线性丢番图方程 \(a_1 k_1 + a_2 k_2 = \gcd(a_1, a_2)\) 的整数解,是合并两个同余方程的关键工具
    • 两两合并策略:从第一个方程开始,逐个与后续方程合并,最终得到统一的同余方程
  3. 关键步骤

    • 读取输入:整数 \(n\)(方程数量),以及 \(n\)\((a_i, m_i)\)
    • 初始化:设当前合并结果为 \(x \equiv m_1 \pmod{a_1}\)
    • 逐个合并(对于 \(i = 2\)\(n\)):
      • 当前方程:\(x \equiv m_1 \pmod{a_1}\)
      • 下一个方程:\(x \equiv m_2 \pmod{a_2}\)
      • \(x = a_1 \cdot k_1 + m_1 = a_2 \cdot k_2 + m_2\),整理得:
        • \(a_1 \cdot k_1 - a_2 \cdot k_2 = m_2 - m_1\)
      • 使用 exgcd 求解 \(a_1 \cdot k_1 + a_2 \cdot k_2 = \gcd(a_1, a_2)\),得到一组特解 \((k_1, k_2)\)
      • 判断有解性:若 \((m_2 - m_1) \bmod \gcd(a_1, a_2) \neq 0\),则方程组无解,输出 \(-1\)
      • 调整特解:将 \(k_1\) 乘以 \((m_2 - m_1) / \gcd(a_1, a_2)\),得到原方程的特解
      • 求最小非负解:令 \(t = a_2 / \gcd(a_1, a_2)\),将 \(k_1\) 调整到最小非负整数:
        • \(k_1 = (k_1 \bmod t + t) \bmod t\)
      • 更新合并结果
        • 新的余数:\(m_1 = a_1 \cdot k_1 + m_1\)
        • 新的模数:\(a_1 = \text{lcm}(a_1, a_2) = a_1 / \gcd(a_1, a_2) \cdot a_2\)
        • \(m_1\) 对新的 \(a_1\) 取模,保持最小非负
    • 输出答案\((m_1 \bmod a_1 + a_1) \bmod a_1\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot \log A_{\max})\),其中 \(A_{\max}\)\(a_i\) 的最大值。每次合并需要一次 exgcd,时间为 \(O(\log(\min(a_1, a_2)))\)
    • 空间复杂度:\(O(1)\),仅使用常数额外变量
  5. 中国剩余定理的核心思想

    • 同余方程合并:将 \(n\) 个同余方程两两合并,每次将两个方程 \(x \equiv m_1 \pmod{a_1}\)\(x \equiv m_2 \pmod{a_2}\) 合并为一个新的方程 \(x \equiv m \pmod{\text{lcm}(a_1, a_2)}\)
    • 有解条件:两个同余方程有公共解的充要条件是 \(m_1 \equiv m_2 \pmod{\gcd(a_1, a_2)}\),即 \((m_2 - m_1)\) 能被 \(\gcd(a_1, a_2)\) 整除。这是推广到非互质模数的关键条件
    • 通解结构:若 \(x_0\) 是一组特解,则所有解为 \(x = x_0 + k \cdot \text{lcm}(a_1, a_2, \ldots, a_n)\),其中 \(k \in \mathbb{Z}\)
    • exgcd 的桥梁作用:exgcd 不仅求出 \(\gcd(a_1, a_2)\),还构造出满足 \(a_1 k_1 + a_2 k_2 = \gcd(a_1, a_2)\) 的整数 \(k_1, k_2\),从而将同余问题转化为线性丢番图方程
    • 与互质版 CRT 的区别:经典 CRT 要求模数两两互质,解唯一模 \(\prod a_i\);本题模数不一定互质,需要先判断相容性,解唯一模 \(\text{lcm}(a_1, \ldots, a_n)\)
    • 适用于同余方程组求解、模线性方程、密码学中的模运算等问题

【解题思路】

【算法标签】

中国剩余定理

【代码详解】

#include <bits/stdc++.h>
using namespace std;

typedef long long LL; // 定义 LL 为 long long 类型

// 扩展欧几里得算法,用于求解 ax + by = gcd(a, b) 的一组整数解
LL exgcd(LL a, LL b, LL &x, LL &y)
{
    if (b == 0) { // 如果 b 为 0,递归终止
        x = 1, y = 0; // 解为 x = 1, y = 0
        return a; // 返回 gcd(a, b)
    }
    LL r = exgcd(b, a % b, x, y); // 递归求解
    LL x1 = x, y1 = y; // 保存递归结果
    x = y1, y = x1 - a / b * y1; // 更新 x 和 y
    return r; // 返回 gcd(a, b)
}

int main()
{
    int n; // 定义整数 n,表示方程的数量
    bool has_answer = true; // 标记是否有解
    LL a1, m1; // a1 和 m1 表示第一个方程的系数和常数项
    cin >> n; // 输入方程的数量
    cin >> a1 >> m1; // 输入第一个方程的系数和常数项

    // 循环处理剩余的 n - 1 个方程
    for (int i = 0; i < n - 1; i++) {
        LL a2, m2; // a2 和 m2 表示下一个方程的系数和常数项
        cin >> a2 >> m2; // 输入下一个方程的系数和常数项
        LL k1, k2; // k1 和 k2 用于存储扩展欧几里得算法的解
        // 使用扩展欧几里得算法求解 a1 * k1 + a2 * k2 = gcd(a1, a2)
        LL d = exgcd(a1, a2, k1, k2);
        // 如果 (m1 - m2) 不能被 gcd(a1, a2) 整除,则无解
        if ((m1 - m2) % d) {
            has_answer = false; // 标记无解
            break; // 跳出循环
        }
        // 调整 k1 和 k2 的值
        k1 = k1 * (m2 - m1) / d;
        k2 = k2 * (m2 - m1) / d;
        LL t = a2 / d;
        // 保证 k1 为最小非负整数解
        k1 = (k1 + t) % t;
        // 更新 m1 的值
        m1 = a1 * k1 + m1;
        // 更新 a1 的值
        a1 = a1 / d * a2;
    }

    if (has_answer)
        // 输出最小非负整数解
        cout << (m1 + a1) % a1 << endl;
    else
        // 无解输出 -1
        cout << -1 << endl;

    return 0; // 程序结束
}
#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用long long防止大数溢出

// 扩展欧几里得算法
int exgcd(int a, int b, int &x, int &y)
{
    if (!b)  // 基本情况:b = 0
    {
        x = 1;     // 当b=0时,gcd(a,0)=a
        y = 0;     // 所以x=1, y=0满足 ax+0*y=a
        return a;  // 返回最大公约数
    }

    int x1, y1, d;  // 局部变量存储递归结果
    d = exgcd(b, a % b, x1, y1);  // 递归调用

    x = y1;                 // 递推公式:x = y1
    y = x1 - a / b * y1;    // 递推公式:y = x1 - (a/b)*y1

    return d;  // 返回最大公约数
}

signed main()  // 因为用了#define int long long,所以用signed main
{
    int n;  // 方程个数
    cin >> n;

    bool has_answer = true;  // 标记是否有解

    int a1, m1;  // 第一个方程的系数
    cin >> a1 >> m1;  // 输入第一个方程 x ≡ m1 (mod a1)

    // 逐个合并方程
    for (int i = 0; i < n - 1; i++)  // 还需要合并n-1个方程
    {
        int a2, m2;  // 下一个方程的系数
        cin >> a2 >> m2;  // 输入下一个方程 x ≡ m2 (mod a2)

        int k1, k2;  // 扩展欧几里得解
        int d = exgcd(a1, a2, k1, k2);  // 求解 a1*k1 + a2*k2 = d

        // 判断是否有解
        // 方程要相容,需要 (m2-m1) 能被 d 整除
        if ((m2 - m1) % d)  // 如果余数不为0
        {
            has_answer = false;  // 无解
            break;
        }

        // 从 a1*k1 + a2*k2 = d 得到 a1*k1 + a2*k2 = m2-m1
        k1 *= (m2 - m1) / d;  // 调整系数

        // 将k1调整到最小非负解
        int t = a2 / d;  // 解的周期
        k1 = (k1 % t + t) % t;  // 确保k1是最小非负整数

        // 更新合并后的方程参数
        m1 = a1 * k1 + m1;  // 新的余数
        a1 = a1 / d * a2;   // 新的模数(最小公倍数)

        // 可选:确保m1是非负的
        m1 = (m1 % a1 + a1) % a1;
    }

    if (has_answer)
    {
        // 输出最小非负整数解
        int result = (m1 % a1 + a1) % a1;
        cout << result << endl;
    }
    else
    {
        cout << -1 << endl;  // 无解
    }

    return 0;
}

【运行结果】

2
8 7
11 9
31
posted @ 2026-06-23 11:23  团爸讲算法  阅读(12)  评论(0)    收藏  举报