Post

GCD

Leetcode上的780.到达终点, 是gcd(最大公约数)的变种, 没看出, 深入了解了一下, 发现我根本不懂gcd, 因此来学一学

gcd(最大公约数)

先从求gcd开始

暴力

暴力法咯, 这还用多说, 写就完事儿

1
2
3
4
5
6
7
8
9
10
int gcd(int a, int b)
{
    if(a < b)
        swap(a, b);
    int res = 1;
    for(int i = 2; i < b/ 2; ++i)
        if(a % i == 0 && b % i == 0)
            res = i;
    return res;
}

辗转相除法(欧几里得算法)

原理如下: $gcd(a, b) = gcd(b, a \% b); (a > b \And a \% b\neq 0)$

证明? 我就不为难自己了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 迭代
int gcd(int a, int b)
{
    if(a < b)
        swap(a, b);
    int temp;
    while(b)
    {
        temp = a % b;
        a = b;
        b = temp;
    }
    return a;
}
// 递归
int gcd(int a, int b)
{
    return b > 0 ? gcd(b, a % b) : a;
}

更相减损术

原理: $gcd(a,b) = gcd(a-b,c); (a>b)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 迭代
int gcd(int a, int b)
{
    while(a != b)
    {
        if(a > b)
            a -= b;
        else
            b -= a;
    }
    return a;
}
// 递归
int gcd(int a, int b)
{
    return a == b ? a : (a > b ? gcd(a - b, b) : gcd(a, b - a));
}

性能分析: 辗转相除会在两个数较大时在取模运算中性能下降, 然而, 若两者差较大, 更相减损术时间复杂度会退化至$O(n)$, 辗转相除法稳定于$O(\log{n})$

更深入一点可以借用斐波那契数列对辗转相除法取模次数进行分析, 从而进一步分析其最差时间复杂度, 然而我顶不住, 这篇博客有数学证明

Stein算法

更相减损术的改进版, 通过除法减少迭代次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 迭代
int gcd(int a, int b)
{
    int multiply = 1;
    while(a != b)
    {
        if(a & 1 == 0 && b & 1 == 0)
        {
            multiply *= 2;
            a /= 2;
            b /= 2;
        }
        else if(a & 1 == 0)
            a /= 2;
        else if(b & 1 == 0)
            b /= 2;
        else
        {
            if(a > b)
                a -= b;
            else
                b -= a;
        }
    }
    return a * multiply;
}
// 递归
int gcd(int a, int b)
{
    if(a == b)
        return a;
    if(a < b)
        gcd(b, a);
    else
    {
        if(a & 1 == 0 && b & 1 == 0)
            // 一些没必要的写法(相信现代编译器)
            // gcd(a >> 1, b >> 1) << 1;
            return 2 * gcd(a / 2, b / 2);
        else if(a & 1 == 0)
            return gcd(a / 2, b);
        else if(b & 1 == 0)
            return gcd(a, b / 2);
        else
            return gcd(a - b, b);
    }
}

780.到达终点

其中的快速逼近策略类似于辗转相除, 可以相互借鉴, 但是题目本身以及题解和求gcd是没关系的, 原题解

逆向思维还是很好想到的, 直接从起点出发路径选择太多, 而从终点出发路径是唯一的(大减小)

进一步来说, 我们一直用大的减小的如果到达起点返回true, 否则false. 这种线性探测会超时, 因此要将多次相减合并. 若我们合并了$k$次操作, 那么有 $tx-k\times ty=m \Rightarrow tx=m+k\times ty$, 最终剩下的余数则是我们快速逼近的结果

1
2
3
4
Accepted
195/195 cases passed (0 ms)
Your runtime beats 100 % of cpp submissions
Your memory usage beats 50.33 % of cpp submissions (5.8 MB)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution
{
public:
    bool reachingPoints(int sx, int sy, int tx, int ty)
    {
        while (tx > sx && ty > sy)
            if (tx < ty)
                ty %= tx;
            else
                tx %= ty;
        // 越界则不可达
        if (tx < sx || ty < sy)
            return false;
        // 那边达到了, 固定一边检查另一边
        return tx == sx ? (ty - sy) % tx == 0 : (tx - sx) % ty == 0;
    }
};
This post is licensed under CC BY 4.0 by the author.