学习笔记:二元一次不定方程

设整数 \(k\ge2\)\(c,a_1,\cdots,a_k\) 为整数,且 \(a_1,\cdots,a_k\) 不为零,\(x_1,\cdots,x_k\) 为整数变量,则方程

\[a_1x_1+\dots+a_kx_k=c\tag{1} \]

称为 \(k\) 元一次不定方程\(a_1,\cdots,a_k\) 称为 系数

一次不定方程有解的条件

定理(Bézout) 不定方程 \((1)\) 有解的充分必要条件是 \(\gcd(a_1,\cdots,a_k)\mid c\)

证明省略。

进而,设 \(g=\gcd(a_1,\cdots,a_k)\),则 \((1)\) 的解与

\[\frac{a_1}gx_1+\dots+\frac{a_k}gx_k=\dfrac cg \]

的解相同。

一次不定方程的求解:扩展 Euclid 算法(exgcd)

在算法竞赛中,最常用的是求 二元一次不定方程 \(ax+by=c\) 的解。

由 Bézout 定理,设 \(d=\gcd(a,b)\),则求出 \(ax+by=d\) 的解,再将 \(x,y\) 乘以 \(c/d\),就得到原方程的解。

下面讨论如何求出 \(ax+by=d\) 的解。

由下一小节的定理,求出一组特解 \(x_0,y_0\) 就可以求出所有解。

为了求出一组特解,采用 辗转相除法,将方程系数规模减小。

一般地,对于方程 \(ax+by=d\),由于 \(d=\gcd(a,b)=\gcd(b,a\bmod b)\)\(ax+by=d\) 有解等价于 \(bx'+(a\bmod b)y'=d\) 有解,其中由 \(a\bmod b=a-\lfloor a/b\rfloor b\) 可解出

\[x=y',\quad y=x'-\left\lfloor\frac ab\right\rfloor y'. \]

故根据递归的思想,像这样不断减小系数的规模,直到 \(b=0\),此时 \(d=a,x=1,y=0\),再递归回去,由 \((b,a\bmod b)\) 对应的 \(x',y'\) 推出上一步 \((a,b)\) 对应的 \(x,y\)。代码如下:

void exgcd(int a, int b, int &x, int &y) {
    if(!b) return x = 1, y = 0, void();
    int d = exgcd(b, a % b, x, y);
    int _y = x - (a / b) * y;
    x = y, y = _y;
    return;
}

或者:

void exgcd(int a, int b, int &x, int &y) {
    if (!b) return x = 1, y = 0, void();
    exgcd(b, a % b, y, x), y -= a / b * x;
}

时间复杂度为辗转相除法的复杂度 \(O(\log V)\),其中 \(V\) 为值域大小。

\(d=\gcd(a,b)\)可以证明 exgcd 算法求得的 \(ax+by=c\) 的解是 绝对值最小 的解,其范围满足 \(|x|\le|b/2d|\)\(|y|\le|a/2d|\)

二元一次不定方程解的结构

二元一次不定方程的通解

定理 设 \(x_0,y_0\) 是方程 \(ax+by=c\) 的一组解,则它的所有解为

\[\begin{cases} x=x_0+\dfrac{b}{\gcd(a,b)}t,\\ y=y_0-\dfrac{a}{\gcd(a,b)}t, \end{cases}\qquad t=0,\pm1,\pm2,\cdots. \]

不难验证上式给出的 \(x,y\) 是方程的解。证明所有 \(x,y\) 都能表示成上述形式的过程省略。

由此我们可以讨论二元一次不定方程解的结构。在实际问题中,往往非负解或正解才是有意义的。那么如何判断方程是否有非负解或正解呢?

二元一次不定方程的非负解与正解

\(a,b\) 为正整数。由于两边乘除 \((a,b)\),得到的解是完全相同的,我们不妨令 \((a,b)=1\),那么全部解形如

\[x=x_0+bt,\quad y=y_0-at,\quad t\in\Z. \]

显然,\(t\) 越大,\(x\) 越大,\(y\) 越小。如果要限制 \(x\ge0\),则解得 \(t\ge\left\lceil\dfrac{-x_0}b\right\rceil\);同理,限制 \(b\ge0\),则解得 \(t\le\left\lfloor\dfrac{y_0}b\right\rfloor\)

同理可求正解对应 \(t\) 的范围。限制 \(x>0\),则 \(t\ge\left\lceil\dfrac{-x_0+1}b\right\rceil\);限制 \(y>0\),则 \(t\le\left\lfloor\dfrac{y_0-1}a\right\rfloor\)

二元一次不定方程非负解与正解的个数

首先,如果 \(a,b\) 异号,那么显然有无穷多组解。下面设 \(a,b\) 同号,我们有下面的定理。

定理 设 \(a,b,c\) 均为正整数,\((a,b)=1\),则:

  • \(c>ab-a-b\) 时,不定方程 \(ax+by=c\) 有非负解,解的个数等于 \(\left\lfloor\dfrac{x_0}b\right\rfloor+\left\lfloor\dfrac{y_0}a\right\rfloor+1\),也等于 \(\left\lfloor\dfrac c{ab}\right\rfloor\)\(\left\lfloor\dfrac c{ab}\right\rfloor+1\)
  • \(c=ab-a-b\) 时,不定方程 \(ax+by=c\) 无非负解。

证明由上面求 \(t\) 的范围可推得,具体过程省略。

显然,要使 \(x=0\)\(y=0\),必有 \(b\mid c\)\(a\mid c\)

下面讨论正解的个数。

定理 设 \(a,b,c\) 均为正整数,\((a,b)=1\),则:

  • \(c>ab\) 时,不定方程 \(ax+by=c\) 有正解,解的个数等于 \(\left\lceil\dfrac{x_0}b\right\rceil+\left\lceil\dfrac{y_0}a\right\rceil-1\),也等于 \(\left\lceil\dfrac c{ab}\right\rceil-1\)\(\left\lceil\dfrac c{ab}\right\rceil\)
  • \(c=ab\) 时,不定方程 \(ax+by=c\) 无正解。

因此,只要求出一组特解 \(x_0,y_0\),就能求出非负解与正解的个数。

例题

洛谷-P5656 【模板】二元一次不定方程 (exgcd)

给定正整数 \(a,b,c\),对于不定方程 \(ax+by=c\)

  • 若方程无整数解,输出 \(-1\)
  • 若方程有整数解,但无正整数解,输出所有整数解中 \(x\) 的最小正整数值和 \(y\) 的最小正整数值;
  • 若方程有正整数解,输出方程正整数解的数量、所有正整数解中 \(x\) 的最小值、\(y\) 的最小值、\(x\) 的最大值和 \(y\) 的最大值。

\(1\le T\le2\cdot10^5\)\(1\le a,b,c\le10^9\)

注意 exgcd 算法求出的是 \(ax+by=d=\gcd(a,b)\) 的特解 \(x,y\),再乘 \(c/d\) 才是原方程的解。

先求出一组特解 \(x,y\) 之后,获得 \(x\) 的最小正整数解 \(x_\min\),其对应的是 \(y_\max\),容易求出。同理求得 \(y_\min\) 和对应的 \(x_\max\)

如果 \(y_\max\le0\),说明无正整数解,输出 \(x_\min\)\(y_\min\)

否则,由通解公式可知,正整数解的数量为 \(N=(x_\max-x_\min)/(b/d)+1\)(加一的原因,类似于植树问题),输出 \(N,x_\min,y_\min,x_\max,y_\max\) 即可。

点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;

void exgcd(int a, int b, int &x, int &y) {
    if (!b) return x = 1, y = 0, void();
    exgcd(b, a % b, y, x), y -= a / b * x;
}

void solve() {
    int a, b, c;
    cin >> a >> b >> c;
    int d = __gcd(a, b);
    if (c % d != 0) return cout << "-1\n", void();
    int x, y;
    exgcd(a, b, x, y);
    x *= c / d, y *= c / d;

    int xmn = x % (b / d); // x_min
    if (xmn <= 0) xmn += b / d;
    int ymx = (c - a * xmn) / b; // x_min 对应 y_max
    
    int ymn = y % (a / d); // y_min
    if (ymn <= 0) ymn += a / d;
    int xmx = (c - b * ymn) / a; // y_min 对应 x_max
    
    if (ymx <= 0) {
        cout << xmn << ' ' << ymn << '\n';
    } else {
        cout << (xmx - xmn) / (b / d) + 1 << ' ' << xmn << ' ' << ymn << ' ' << xmx << ' ' << ymx << '\n';
    }
    return;
}

signed main() {
    cin.tie(0)->sync_with_stdio(false);

    int tt = 1;
    cin >> tt;
    while (tt--) solve();

    return 0;
}

Gym-104008E Draw a triangle

给定平面上三角形的两个顶点坐标 \((x_1,y_1),(x_2,y_2)\),求另外一个顶点坐标 \((x_3,y_3)\),使得三角形面积最小。

要求三个顶点的坐标都为整数。输出任意一个解即可。

\(1\le T\le50000\)\(-10^9\le x_1,y_1,x_2,y_2\le10^9\)\(-10^{18}\le x_3,y_3\le10^{18}\)

\(x_1=x_2\)\(y_1=y_2\) 的情况是平凡的,下面设 \(x_1\ne x_2\)\(y_1\ne y_2\)

不妨设 \(x_1<x_2\)。先将三角形的两个顶点平移到 \((0,0),(x,y)\),其中 \(x=x_2-x_1,y=y_2-y_1\)

不妨设 \(y>0\)\(y<0\) 的情况将 \(|y|\) 代入下面步骤后令 \(v=-v\) 即可。

设对这个三角形求出的坐标为 \((u,v)\),则 \((x_3,y_3)=(u+x_1,v+y_1)\)

由向量的知识可知,三角形的面积为 \(S=\dfrac12|(x,y)\times(u,v)|=\dfrac12|xu-yv|\),最小化 \(|xu-yv|\)

由于 \(xu-yv=w\) 有整数解当且仅当 \(\gcd(x,y)\mid w\),故令 \(xu-yv=\gcd(x,y)\),用 exgcd 解出 \((u,v)\) 即可。

点击查看代码
#include <bits/stdc++.h>
#define fi first
#define se second
#define int long long
using namespace std;
typedef pair<int, int> pii;

void exgcd(int a, int b, int &x, int &y) {
    if (!b) return x = 1, y = 0, void();
    exgcd(b, a % b, y, x), y -= a / b * x;
}

pii calc(int x, int y) {
    int u, v;
    exgcd(x, -y, u, v); // xv - yu = gcd(x, y)
    return {u, v};
}

void solve() {
    int x1, y1, x2, y2;
    cin >> x1 >> y1 >> x2 >> y2;
    int x = x2 - x1, y = y2 - y1;
    if (x == 0) {
        cout << x1 + 1 << ' ' << y1 << '\n';
    } else if (y == 0) {
        cout << x1 << ' ' << y1 + 1 << '\n';
    } else {
        if (x1 > x2) swap(x1, x2), swap(y1, y2), x = -x, y = -y;
        if (y > 0) {
            pii res = calc(x, y);
            cout << x1 + res.fi << ' ' << y1 + res.se << '\n';
        } else {
            pii res = calc(x, -y);
            cout << x1 + res.fi << ' ' << y1 - res.se << '\n';
        }
    }
    return;
}

signed main() {
    cin.tie(0)->sync_with_stdio(false);

    int tt = 1;
    cin >> tt;
    while (tt--) solve();

    return 0;
}

参考资料

posted @ 2025-07-22 17:30  f2021ljh  阅读(81)  评论(0)    收藏  举报