Skip to content

最大公约数与同余定理


gcd 最大公约数

基本介绍

(1)欧几里得算法的过程 : 辗转相除法

(2)正确性的证明过程见代码注释部分,证明过程非常好懂,不过直接记忆过程即可

(3)求 gcd(a,b),其中 a > b时间复杂度为 O((log a)的 3 次方),时间复杂度证明略,这个复杂度足够好了

(4)简单转化就可以求最小公倍数

(5)更高效求最大公约数Stein 算法、由最大公约数扩展出的 “ 裴蜀定理 ” ,比赛同学有兴趣可以继续研究

(6)不比赛的同学,哪怕你的目标是最顶级的公司应聘、还是考研,掌握这个只有一行的函数已经足够 !

代码实现

java
// 求最大公约数、最小公倍数
public class Code01_GcdAndLcm {

	// 证明辗转相除法就是证明如下关系:
	// gcd(a, b) = gcd(b, a % b)
	// 假设a % b = r,即需要证明的关系为:gcd(a, b) = gcd(b, r)

	// 证明过程:
	// 因为a % b = r,所以如下两个等式必然成立
	// 1) a = b * q + r,q为0、1、2、3....中的一个整数
	// 2) r = a − b * q,q为0、1、2、3....中的一个整数
	// 假设u是a和b的公因子,则有: a = s * u, b = t * u
	// 把a和b带入2)得到,r = s * u - t * u * q = (s - t * q) * u
	// 这说明 : u如果是a和b的公因子,那么u也是r的因子
	// 假设v是b和r的公因子,则有: b = x * v, r = y * v
	// 把b和r带入1)得到,a = x * v * q + y * v = (x * q + y) * v
	// 这说明 : v如果是b和r的公因子,那么v也是a的公因子
	// 综上,a和b的每一个公因子 也是 b和r的一个公因子,反之亦然
	// 所以,a和b的全体公因子集合 = b和r的全体公因子集合
	// 即gcd(a, b) = gcd(b, r)
	// 证明结束
	public static long gcd(long a, long b) {
		return b == 0 ? a : gcd(b, a % b);
	}
}

lcm 最小公倍数

思路分析

有了最大公约数的基础,两个数任意一个数除以最大公约数 * 另一个数就是最小公倍数

注意代码的写法,a * b 可能会有溢出的风险,强转为 long 类型,先除再乘

代码实现

java
// 求最大公约数、最小公倍数
public class Code01_GcdAndLcm {

	// 证明辗转相除法就是证明如下关系:
	// gcd(a, b) = gcd(b, a % b)
	// 假设a % b = r,即需要证明的关系为:gcd(a, b) = gcd(b, r)

	// 证明过程:
	// 因为a % b = r,所以如下两个等式必然成立
	// 1) a = b * q + r,q为0、1、2、3....中的一个整数
	// 2) r = a − b * q,q为0、1、2、3....中的一个整数
	// 假设u是a和b的公因子,则有: a = s * u, b = t * u
	// 把a和b带入2)得到,r = s * u - t * u * q = (s - t * q) * u
	// 这说明 : u如果是a和b的公因子,那么u也是r的因子
	// 假设v是b和r的公因子,则有: b = x * v, r = y * v
	// 把b和r带入1)得到,a = x * v * q + y * v = (x * q + y) * v
	// 这说明 : v如果是b和r的公因子,那么v也是a的公因子
	// 综上,a和b的每一个公因子 也是 b和r的一个公因子,反之亦然
	// 所以,a和b的全体公因子集合 = b和r的全体公因子集合
	// 即gcd(a, b) = gcd(b, r)
	// 证明结束
	public static long gcd(long a, long b) {
		return b == 0 ? a : gcd(b, a % b);
	}

	public static long lcm(long a, long b) {
		return (long) a / gcd(a, b) * b;
	}
}

第 N 个神奇数字

题目链接

https://leetcode.cn/problems/nth-magical-number/description/

思路分析

本题用到 “ 二分答案法 ” 和 “ 容斥原理 ” 两个重要的算法,不过用的非常浅,之前没有接触过也能理解

“ 二分答案法 ” 非常巧妙可以解决很多问题,整套内容会在后续的【必备】课程里做成专题视频讲述

“ 容斥原理 ” 可以考的非常难,也会在后续的【扩展】课程里做成专题视频讲述

假设 a < b,如果不管 b,那 n * a 就是第 n 个神奇数字,如果 b 加进来了,那就会缩小神奇数字的范围,则边界就是 Math.min(a,b) * n

本题采用二分答案的思想,第 n 个神奇数字,就相当于在某个范围上统计神奇数字的个数,根据某个范围的神奇数字够不够 n 个来决定往哪个区间继续二分

容斥原理的体现:在 0 - n 范围内,总有一部分数字既能被 a 整除,也能被 b 整除,这部分就是重复的,需要减去,重复部分的数字就是可以被 lcm(a,b) 整除的那些数字

则神奇的数字的个数为:n / a + n / b - n / lcm(a, b)

举例:1 - 10 范围内有多少个数字可以被 2 整除,10 / 2 = 5 个,分别是 2 4 6 8 10

代码分析

java
class Solution {
    public int nthMagicalNumber(int n, int a, int b) {
        long lcm = lcm(a, b);
        long ans = 0;
        // l = 0
        // r = (long) n * Math.min(a, b)
        // l ... r 范围上二分,结合容斥原理
        // 若不考虑 b,则第 n 个神奇数字就是 (long) n * Math.min(a, b)
        // b 的加入会缩小第 n 个神奇数字所在的范围
        // 二分思想;某个范围符合够 n 个神奇数字,则范围的边界就是第 n 个神奇数字
        // 容斥原理:某个范围中总有一部分数字既能被 a 整除,也能被 b 整除,这部分就是重复的,需要减去
        for (long left = 0, right = (long) n * Math.min(a, b), mid = 0; left <= right;) {
            mid = (left + right) / 2;
            // 够 n 个,往左二分
            if (mid / a + mid / b - mid / lcm >= n) {
                // 记答案
                ans = mid;
                right = mid - 1;
            } else {
                // 否则往右二分
                left = mid + 1;
            }
        }
        return (int) (ans % 1000000007);
    }

    public long gcd(long a, long b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    public long lcm(long a, long b) {
        return (long) a / gcd(a, b) * b;
    }
}

同余原理

应用场景

某个过程需要连续使用 + - * / 运算,由于结果可能非常大,所以需要返回对一个数取模的结果,并且非负

出现的问题:(1)计算过程中可能会出现负数(2)计算结果可能会有溢出的风险

复杂度分析

对于固定位数的数,+ - * / 的时间复杂度都是 O(1)

对于位数为 k 的数,+ - 时间复杂度为 O(k)* / 时间复杂度为 O(k2

用 BigInteger 不行吗?若某一个中间结果位数很大,根据上述时间复杂度分析,有可能会超时

加法同余

最终的计算结果 mod 等于每个中间过程 mod 的结果累加再 mod

例如:(a + b) % mod =(a % mod + b % mod) % mod,乘法同理

乘法同余

对于乘法需要确保过程中不溢出,所以往往乘法运算的用 long 类型做中间变量

例如:(a * b) % mod = (a % mod * b % mod) % mod

Tip

某个数 % 7 的余数为 -2 和某个数 % 7 的余数为 5 是等效的

因为余数 -2 + 2 和余数 5 + 2 后都可以被 7 整除,所以认为二者等效

即若需要求非负的余数,公式中需要加上 mod 进行转换(-2 + 7 = 5,5 % 7 = 5)

减法同余

(a - b) % mod = (a % mod - b % mod + mod) % mod

加 mod 再 mod 的目的是保证计算结果是非负的

除法同余

除法必须求逆元,后续课讲述

测试代码

java
package class041;

import java.math.BigInteger;

// 加法、减法、乘法的同余原理
// 不包括除法,因为除法必须求逆元,后续课讲述
public class Code03_SameMod {

	// 为了测试
	public static long random() {
		return (long) (Math.random() * Long.MAX_VALUE);
	}

	// 计算 ((a + b) * (c - d) + (a * c - b * d)) % mod 的非负结果
	public static int f1(long a, long b, long c, long d, int mod) {
		BigInteger o1 = new BigInteger(String.valueOf(a)); // a
		BigInteger o2 = new BigInteger(String.valueOf(b)); // b
		BigInteger o3 = new BigInteger(String.valueOf(c)); // c
		BigInteger o4 = new BigInteger(String.valueOf(d)); // d
		BigInteger o5 = o1.add(o2); // a + b
		BigInteger o6 = o3.subtract(o4); // c - d
		BigInteger o7 = o1.multiply(o3); // a * c
		BigInteger o8 = o2.multiply(o4); // b * d
		BigInteger o9 = o5.multiply(o6); // (a + b) * (c - d)
		BigInteger o10 = o7.subtract(o8); // (a * c - b * d)
		BigInteger o11 = o9.add(o10); // ((a + b) * (c - d) + (a * c - b * d))
		// ((a + b) * (c - d) + (a * c - b * d)) % mod
		BigInteger o12 = o11.mod(new BigInteger(String.valueOf(mod)));
		if (o12.signum() == -1) {
			// 如果是负数那么+mod返回
			return o12.add(new BigInteger(String.valueOf(mod))).intValue();
		} else {
			// 如果不是负数直接返回
			return o12.intValue();
		}
	}

	// 计算 ((a + b) * (c - d) + (a * c - b * d)) % mod 的非负结果
	public static int f2(long a, long b, long c, long d, int mod) {
		int o1 = (int) (a % mod); // a
		int o2 = (int) (b % mod); // b
		int o3 = (int) (c % mod); // c
		int o4 = (int) (d % mod); // d
		int o5 = (o1 + o2) % mod; // a + b
		int o6 = (o3 - o4 + mod) % mod; // c - d
		int o7 = (int) (((long) o1 * o3) % mod); // a * c
		int o8 = (int) (((long) o2 * o4) % mod); // b * d
		int o9 = (int) (((long) o5 * o6) % mod); // (a + b) * (c - d)
		int o10 = (o7 - o8 + mod) % mod; // (a * c - b * d)
		int ans = (o9 + o10) % mod; // ((a + b) * (c - d) + (a * c - b * d)) % mod
		return ans;
	}

	public static void main(String[] args) {
		System.out.println("测试开始");
		int testTime = 100000;
		int mod = 1000000007;
		for (int i = 0; i < testTime; i++) {
			long a = random();
			long b = random();
			long c = random();
			long d = random();
			if (f1(a, b, c, d, mod) != f2(a, b, c, d, mod)) {
				System.out.println("出错了!");
			}
		}
		System.out.println("测试结束");

		System.out.println("===");
		long a = random();
		long b = random();
		long c = random();
		long d = random();
		System.out.println("a : " + a);
		System.out.println("b : " + b);
		System.out.println("c : " + c);
		System.out.println("d : " + d);
		System.out.println("===");
		System.out.println("f1 : " + f1(a, b, c, d, mod));
		System.out.println("f2 : " + f2(a, b, c, d, mod));
	}

}