前置知识:无
求最大公约数
欧几里得算法的过程:辗转相除法
// 递归
func gcd(a, b int) int {
if b == 0 {
return a
}
return gcd(b, a%b)
}
// 迭代
func gcd(a, b int) int {
for b != 0 {
a, b = b, a%b
}
return a
}
正确性证明
证明辗转相除法就是证明
设 ,即需要证明的关系为
证明:
因为 ,所以如下两个等式必然成立( 为自然数)
设 是 和 的公因子,则有:,其中
把 和 带入 得到:
这说明: 如果是 和 的公因子,那么 也是 的因子
设 是 和 的公因子,则有:,其中
把 和 带入 得到:
这说明: 如果是 和 的公因子,那么 也是 的公因子
综上: 和 的每一个公因子也是 和 的一个公因子,反之亦然
所以: 和 的全体公因子集合 和 的全体公因子集合,即
证毕
复杂度
求 gcd(a, b)
,其中 a > b
,时间复杂度为
时间复杂度证明略,这个复杂度足够好了
求最小公倍数
简单转化就可以求最小公倍数:如果 a
和 b
的最大公约数是 c
,则 a
和 b
的最小公倍数为 a/c*b
func lcm(a, b int) int {
return a / gcd(a, b) * b
}
备注
更高效求最大公约数的 Stein 算法、由最大公约数扩展出的“裴蜀定理”,比赛同学有兴趣可以继续研究
不比赛的同学,哪怕你的目标是最顶级的公司应聘、还是考研,掌握这个只有一行的函数已经足够!
同余原理
背景
某些题目计算(加减乘除)过程中的数值会很大,可能导致溢出,一般这种题会要求将答案模一个大质数得到一个非负结果返回(像 这样)
怎么做能够避免计算导致的溢出呢?
- 使用
bigint
或其他一些大数计算库 - 同余原理
方法 1 是不太好的,因为数值太大的话,加减乘除的时间复杂度不再是 ,k
位(比特位)的大数加减时间复杂度是 ,乘除时间复杂度是
同余原理
- 加法、乘法:每一步计算完后直接取模
- 减法:
(a-b+mod)%mod
(避免得到负数结果) - 要确保过程中不溢出,往往乘法运算的用
long
类型做中间变量 - 除法的同余需要求逆元,会在【扩展】课程里讲述,较难的题目才会涉及
验证
package main
import (
"fmt"
"math"
"math/big"
"math/rand"
)
const (
MAX_INT64 = math.MaxInt64
MOD = 1e9 + 7
TEST_TIME = 100000
)
func main() {
for i := 0; i < TEST_TIME; i++ {
a := random()
b := random()
c := random()
d := random()
r1 := f1(a, b, c, d)
r2 := f2(a, b, c, d)
if r1 != r2 {
panic("error")
}
}
fmt.Println("success")
}
// big.Int
// 计算 ((a + b) * (c - d) + (a * c - b * d)) % mod 的非负结果
func f1(a, b, c, d int64) int32 {
// 将参数转换为 big.Int 类型
bigA := big.NewInt(a)
bigB := big.NewInt(b)
bigC := big.NewInt(c)
bigD := big.NewInt(d)
bigMod := big.NewInt(MOD)
// 计算 (a + b) * (c - d)
tmp1 := new(big.Int).Add(bigA, bigB)
tmp2 := new(big.Int).Sub(bigC, bigD)
tmp3 := new(big.Int).Mul(tmp1, tmp2)
// 计算 a * c
tmp4 := new(big.Int).Mul(bigA, bigC)
// 计算 b * d
tmp5 := new(big.Int).Mul(bigB, bigD)
// 计算 (a * c - b * d)
tmp6 := new(big.Int).Sub(tmp4, tmp5)
// 计算 ((a + b) * (c - d) + (a * c - b * d))
tmp7 := new(big.Int).Add(tmp3, tmp6)
// 计算最终结果并取模
result := new(big.Int).Mod(tmp7, bigMod)
// 将结果转换为 int32
return int32(result.Int64())
}
// 同余原理
// 计算 ((a + b) * (c - d) + (a * c - b * d)) % mod 的非负结果
func f2(a, b, c, d int64) int32 {
aa := a % MOD
bb := b % MOD
cc := c % MOD
dd := d % MOD
o1 := (aa + bb) % MOD // a + b
o2 := (cc - dd + MOD) % MOD // c - d
o3 := (aa * cc) % MOD // a * c
o4 := (bb * dd) % MOD // b * d
o5 := (o1 * o2) % MOD // (a + b) * (c - d)
o6 := (o3 - o4 + MOD) % MOD // (a * c - b * d)
o7 := (o5 + o6) % MOD // (a + b) * (c - d) + (a * c - b * d)
return int32(o7)
}
func random() int64 {
return rand.Int63n(MAX_INT64)
}
求最大公约数相关的经典题目
题目描述
一个正整数如果能被 a
或 b
整除,那么它是神奇的。给定三个整数 n
, a
, b
,返回第 n
个神奇的数字
因为答案可能很大,所以返回答案 对 取模 后的值
测试链接
思路
答案肯定在 之间,假设有一个函数 f(x)
可以返回 有几个神奇的数字,则可以使用“二分答案法”求解
f(x)
如何实现?
x/a
可以得到 能被 a
整除的数字个数;x/b
可以得到 能被 b
整除的数字个数
有数字被重复计数了,就是既能被 a
整除又能被 b
整除的数字,即:x/lcm(a, b)
个,减去即可(“容斥原理”)
答案
const (
mod = 1e9 + 7
)
func nthMagicalNumber(n int, a int, b int) int {
ans := 0
minn := min(a, b)
maxx := n * minn
lcmVal := lcm(a, b)
for minn <= maxx {
mid := minn + (maxx-minn)>>1
if mid/a+mid/b-mid/lcmVal >= n {
ans = mid
maxx = mid - 1
} else {
minn = mid + 1
}
}
return ans % mod
}
func gcd(a, b int) int {
for b != 0 {
a, b = b, a%b
}
return a
}
func lcm(a, b int) int {
return a / gcd(a, b) * b
}
备注
本题用到“二分答案法”和“容斥原理”两个重要的算法,不过用的非常浅,之前没有接触过也能理解
“二分答案法”非常巧妙可以解决很多问题,整套内容会在后续的【必备】课程里做成专题视频讲述
“容斥原理”可以考的非常难,也会在后续的【扩展】课程里做成专题视频讲述