Skip to content

FractionConstructionProblem - 题解

标签与难度

标签: 数论, 扩展欧几里得算法, 构造, 线性筛, 质因数分解 难度: 1800

题目大意喵~

哈喽,各位算法探险家们,喵~ 今天我要带大家解决一个关于分数的构造问题,听起来就很有趣对不对?

题目是这样哒:对于每一组查询,我们都会拿到两个正整数 aabb。你的任务呢,就是像变魔术一样,找出四个正整数 c,d,e,fc, d, e, f,让它们满足下面这些奇妙的条件:

  1. 等式要成立:cdef=ab\frac{c}{d} - \frac{e}{f} = \frac{a}{b}
  2. 分母要更小:d<bd < b 并且 f<bf < b
  3. 分子可以很大:ccee 的值在 114×10124 \times 10^{12} 之间。
  4. aabb 的范围是 112×1062 \times 10^6

如果能找到这样的一组 c,d,e,fc, d, e, f,就把它们开心地打印出来。如果无论怎么抓破猫耳朵都找不到,那就只好输出 -1 -1 -1 -1 啦。

解题思路分析

拿到这个问题,我的胡须都兴奋地翘起来了呢!这是一个典型的构造题,需要我们运用一些数论知识来找到解。我们先把那个等式通分一下,看看能不能发现什么线索,喵~

cdef=cfdedf=ab\frac{c}{d} - \frac{e}{f} = \frac{cf - de}{df} = \frac{a}{b}

这个形式告诉我们,cf - dedf 的比值应该等于 ab 的比值。为了简化问题,一个很自然的想法就是,我们能不能直接让 dfdfbb 产生某种联系,比如让 dfdfbb 的倍数?

最简单的想法,就是让 dfdf 等于 bb 吗?不对,题目要求 d<bd<bf<bf<b 呢。不过,我们可以从这里出发,把问题分成两种情况来讨论,就像猫咪面对两条路,要选择走哪一条一样!

Case 1: aabb 不互质 (简单情况喵~)

aabb 有大于 1 的最大公约数时,问题会变得非常简单哦!设 g=gcd(a,b)g = \gcd(a, b),并且 g>1g > 1。 我们可以把原式右边先约分一下:

ab=a/gb/g\frac{a}{b} = \frac{a/g}{b/g}

现在我们的目标是构造 cdef=a/gb/g\frac{c}{d} - \frac{e}{f} = \frac{a/g}{b/g}

一个超级巧妙的构造方法是,让 ddff 相等!比如说,我们令 d=f=b/gd = f = b/g。 因为 g>1g > 1,所以 b/g<bb/g < b,这完美满足了 d<bd<bf<bf<b 的条件,太棒啦!

代入我们的选择,等式就变成了:

cb/geb/g=ceb/g=a/gb/g\frac{c}{b/g} - \frac{e}{b/g} = \frac{c-e}{b/g} = \frac{a/g}{b/g}

两边都乘以 b/gb/g,就得到 ce=a/gc - e = a/g。 我们需要找到两个正整数 ccee 满足这个式子。这可太容易了,就像从罐头里拿小鱼干一样简单!我们可以选一个非常简单的解,比如令 e=1e=1,那么 c=a/g+1c = a/g + 1。因为 a,ga, g 都是正整数,所以 ccee 也肯定是正整数。

所以,当 gcd(a,b)>1\gcd(a, b) > 1 时,我们找到了一组解: c=a/g+1c = a/g + 1, d=b/gd = b/g, e=1e = 1, f=b/gf = b/g

Case 2: aabb 互质 (有点挑战的情况呐)

如果 aabb 互质,即 gcd(a,b)=1\gcd(a, b) = 1,上面的方法就不行了,因为 g=1g=1 会导致 d=bd=b,不满足 d<bd<b 的说。 这时,我们需要回到最初的思路:cfdedf=ab\frac{cf-de}{df} = \frac{a}{b}

一个直接的构造是令 df=bdf = b 并且 cfde=acf - de = a。 要满足 df=bdf=bd,f<bd, f < b,意味着 ddff 必须是 bb 的两个大于 1 的真因子。这就要求 bb 必须是一个合数。如果 b=1b=1 或者 bb 是一个质数,我们是找不到这样的 d,fd,f 的,所以这种情况下无解。

现在我们来看 cfde=acf - de = a。这是一个关于变量 ccee 的线性丢番图方程。它有解的充要条件是 gcd(f,d)\gcd(f, -d) 能够整除 aa,也就是 gcd(f,d)\gcd(f, d) 要能整除 aa。 但是,我们现在是在 gcd(a,b)=1\gcd(a, b) = 1 的情况下讨论。因为 d,fd,f 都是 bb 的因子,所以 aad,fd,f 都没有公因子。因此,要让 gcd(d,f)\gcd(d, f) 整除 aa,唯一的可能性就是 gcd(d,f)=1\gcd(d, f) = 1

所以,问题转化成了:

  1. 找到 bb 的两个大于 1 的、互质的因子 ddff,使得 df=bdf = b
  2. 如果找到了,就用它们来解方程 cfde=acf - de = a

如何找到互质的因子 ddff 呢? 如果一个数 bb 可以被分解成两个大于 1 且互质的数相乘,那它必须拥有至少两个不同的质因子。如果 bb 只是某个质数 pp 的幂(b=pk,k2b=p^k, k \ge 2),那么它的任何两个大于1的因子(比如 pip^ipjp^j)都必然有公因子 pp,不可能互质。

所以,当 gcd(a,b)=1\gcd(a,b)=1 时,有解的一个必要条件是:bb 必须拥有至少两个不同的质因子。 我们可以用线性筛预处理出 2×1062 \times 10^6 内所有数的最小质因子(Smallest Prime Factor, SPF)。 对于一个给定的 bb,我们找到它的最小质因子 pp。然后,我们把 bb 中所有的因子 pp 都提出来,形成我们的第一个因子 dd。也就是 d=pkd = p^k,其中 pkp^kppbb 的质因数分解中的最高次幂。剩下的部分就是 f=b/df = b/d。 通过这种构造,d 的质因子只有 pp,而 f 的质因子中没有 pp,所以 ddff 天然互质! 如果这样构造出来的 f=1f=1,就说明 bb 本身就是 pp 的幂,无解。否则,我们就成功找到了需要的 ddff

如何求解 cfde=acf - de = a 呢? 这就要请出我们的好朋友——扩展欧几里得算法(exgcd)啦! 我们可以用 exgcd 求解方程 fx+dy=gcd(f,d)fx + dy = \gcd(f, d)。因为我们已经保证了 gcd(f,d)=1\gcd(f,d)=1,所以我们解的是 fx+dy=1fx + dy = 1。 设 exgcd 给了我们一组解 (x0,y0)(x_0, y_0)

那么对于方程 cfde=acf - de = a,我们可以构造出两组可能的解:

  1. fcde=af \cdot c - d \cdot e = a 出发。 一个特解是 c=ax0c = a \cdot x_0e=ay0e = -a \cdot y_0。 如果 x0>0x_0 > 0y0<0y_0 < 0,那么 c=ax0>0c = a x_0 > 0e=ay0>0e = -a y_0 > 0,我们直接就找到了一组正整数解!此时的答案就是 (c,d,e,f)=(ax0,d,ay0,f)(c, d, e, f) = (a x_0, d, -a y_0, f)

  2. 如果上面那组解不是正数解(即 x0<0,y0>0x_0 < 0, y_0 > 0),怎么办呢? 我们可以把原式变形成 efcd=ab\frac{e}{f} - \frac{c}{d} = \frac{-a}{b},然后解 decf=ade - cf = a。 同样利用 fx0+dy0=1fx_0 + dy_0 = 1,我们可以构造出这个方程的解:e=ay0,c=ax0e = a \cdot y_0, c = -a \cdot x_0。 因为 y0>0y_0 > 0x0<0x_0 < 0,所以 e=ay0>0e = a y_0 > 0c=ax0>0c = -a x_0 > 0。这又是一组正整数解!此时的答案是 (c,d,e,f)=(ax0,f,ay0,d)(c, d, e, f) = (-a x_0, f, a y_0, d)。注意 d,fd, f 的位置也交换了哦!

因为对于 fx0+dy0=1fx_0+dy_0=1 (f,d>1f,d>1), x0x_0y0y_0 必定异号,所以上面两种情况总有一种能给我们提供一组正整数解。问题解决啦,喵~

总结一下我们的策略:

  1. 计算 g=gcd(a,b)g = \gcd(a,b)
  2. g>1g>1,使用简单构造法。
  3. g=1g=1,检查 bb 是否有至少两个不同质因子。
    • 若没有(b=1b=1, bb是质数, b=pkb=p^k),则无解。
    • 若有,分解出互质的 d,fd, f,然后用 exgcd 求解。根据 exgcd 返回解的符号,选择两种构造方式中的一种。

代码实现

下面是我根据上面的思路,精心编写的代码哦!注释写得很详细,希望能帮助你理解,喵~

cpp
#include <iostream>
#include <vector>
#include <numeric>

// 使用 long long 防止 c, e 溢出
using int64 = long long;

// 最大范围是 2e6,稍微开大一点防止边界问题
const int MAX_B = 2000000 + 5;
// spf[i] 存储 i 的最小质因子 (Smallest Prime Factor)
int spf[MAX_B];
std::vector<int> primes;

// 线性筛预处理所有数的最小质因子
void linear_sieve() {
    for (int i = 2; i < MAX_B; ++i) {
        if (spf[i] == 0) {
            spf[i] = i;
            primes.push_back(i);
        }
        for (int p : primes) {
            if (p > spf[i] || (int64)i * p >= MAX_B) {
                break;
            }
            spf[i * p] = p;
        }
    }
}

// 扩展欧几里得算法
// 求解 a*x + b*y = gcd(a, b)
// 返回 gcd(a, b),并通过引用修改 x 和 y 的值
int64 exgcd(int64 a, int64 b, int64& x, int64& y) {
    if (b == 0) {
        x = 1;
        y = 0;
        return a;
    }
    int64 d = exgcd(b, a % b, y, x);
    y -= (a / b) * x;
    return d;
}

// 解决单个查询的函数
void solve() {
    int64 a, b;
    std::cin >> a >> b;

    int64 g = std::gcd(a, b);

    if (g > 1) {
        // Case 1: a 和 b 不互质,使用简单构造法
        // c/d - e/f = (a/g)/(b/g)
        // 令 d = f = b/g, 则 c - e = a/g
        // 取 e=1, c=a/g+1 即可
        int64 c = a / g + 1;
        int64 d = b / g;
        int64 e = 1;
        int64 f = b / g;
        std::cout << c << " " << d << " " << e << " " << f << "\n";
    } else {
        // Case 2: a 和 b 互质
        if (b == 1) {
            // b=1, d,f 必须是正整数且 < 1,不可能
            std::cout << "-1 -1 -1 -1\n";
            return;
        }

        // 找到 b 的最小质因子 p
        int p = spf[b];
        // 构造 d = p^k
        int64 d_val = 1;
        int64 temp_b = b;
        while (temp_b % p == 0) {
            d_val *= p;
            temp_b /= p;
        }
        
        // f_val 是 b 的剩余部分
        int64 f_val = temp_b;

        if (f_val == 1) {
            // b 是单个质数的幂,无法分解成两个互质的因子
            std::cout << "-1 -1 -1 -1\n";
            return;
        }

        // 现在我们有 df=b, gcd(d,f)=1, d,f > 1
        // 求解 cf - de = a
        // 先用 exgcd 解出 fx + dy = 1
        int64 x, y;
        exgcd(f_val, d_val, x, y); // x, y 是一组特解

        // 根据 x, y 的符号构造 c 和 e
        // 我们知道 x 和 y 必定异号
        // 方案A: 解 f*c - d*e = a, 得 c=a*x, e=-a*y.
        //       要求 c,e > 0, 即 x>0, y<0.
        // 方案B: 解 d*e - f*c = a, 得 e=a*y, c=-a*x.
        //       要求 c,e > 0, 即 y>0, x<0.
        
        // exgcd(f,d,x,y) 返回的 x,y 符号不确定,但总有一种方案可行
        // C++ exgcd 的实现通常使得 |x| <= d, |y| <= f
        // 如果 x 为负,则 y 必定为正
        if (x < 0) {
            // x < 0, y > 0, 使用方案 B
            // c = -a*x, d = f_val, e = a*y, f = d_val
            std::cout << -a * x << " " << f_val << " " << a * y << " " << d_val << "\n";
        } else {
            // x > 0, y < 0, 使用方案 A
            // c = a*x, d = d_val, e = -a*y, f = f_val
            std::cout << a * x << " " << d_val << " " << -a * y << " " << f_val << "\n";
        }
    }
}

int main() {
    // 关掉同步流,让输入输出更快一点喵~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    // 预处理
    linear_sieve();

    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }

    return 0;
}

复杂度分析

  • 时间复杂度: O(MAX_B+Tlogb)O(\text{MAX\_B} + T \cdot \log b)

    • linear_sieve() 的预处理时间复杂度是 O(MAX_B)O(\text{MAX\_B}),其中 MAX_B\text{MAX\_B}bb 的最大值。
    • 对于每个查询 TT 次:
      • std::gcd(a, b) 的复杂度是 O(log(min(a,b)))O(\log(\min(a,b)))
      • 使用预处理的 spf 数组分解质因数,复杂度是 O(logb)O(\log b)
      • exgcd 的复杂度是 O(log(min(d,f)))=O(logb)O(\log(\min(d,f))) = O(\log b)
    • 所以总时间复杂度由预处理和所有查询的总和决定。
  • 空间复杂度: O(MAX_B)O(\text{MAX\_B})

    • 主要的空间开销来自于 spf 数组,用于存储每个数的最小质因子。

知识点总结

这道题真是一次愉快的数论探险呢!我们用到了不少有趣的工具,总结一下吧:

  1. 分类讨论思想: 将问题根据 gcd(a, b) 是否为 1 分成两种情况,是解题的关键突破口。简单情况用简单方法,复杂情况再想办法,这让思路清晰很多!
  2. 构造法: 对于 gcd(a, b) > 1 的情况,我们直接构造出了一组巧妙的解。构造法在算法竞赛中非常重要,有时候一个简单的设定就能让问题迎刃而解。
  3. 线性筛求最小质因子: 为了高效地对 bb 进行质因数分解,我们预处理了 spf 数组。线性筛是一种非常高效的筛法,可以在 O(N)O(N) 时间内完成任务。
  4. 扩展欧几里得算法: 这是解决线性丢番图方程 ax+by=cax+by=c 的核心武器。本题中,我们用它来求解 cfde=acf-de=a,找到了构造分子 c,ec, e 的方法。
  5. 数论性质: 理解 df=bdf=bgcd(d,f)=1\gcd(d,f)=1 意味着 bb 必须有至少两个不同的质因子,是判断无解情况的基础。

希望这篇题解能帮到你,让你也爱上用数学工具解决问题的感觉!下次再一起探险吧,喵~