{ }codelper

学习 › C++

算法 · 30

最大公约数、素数与进制

用余数求最大公约数,用筛法筛出素数,把数写成别的进制。

这是数字类题目里经常出现的三件工具。
下面的例子求出 84 和 36 的最大公约数,输出 30 以内的所有素数,再把 255 写成 16 进制。

#include <iostream>#include <string>#include <vector> const std::string DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"; int gcd(int a, int b) {    while (b != 0) {        int r = a % b; // (a, b)를 (b, a를 b로 나눈 나머지)로 바꾼다        a = b;        b = r;    }    return a;} std::string toBase(int x, int base) {    if (x == 0) return "0";    std::string s = "";    while (x > 0) {        s = DIGITS[x % base] + s; // 나머지는 낮은 자리부터 나오므로 앞에 붙인다        x /= base;    }    return s;} int main() {    int a, b, n, x, base;    std::cin >> a >> b >> n >> x >> base;     std::cout << gcd(a, b) << '\n';     // 체: 소수를 찾을 때마다 그 배수를 지운다. 끝까지 안 지워진 수가 소수    std::vector<bool> isPrime(n + 1, true);    isPrime[0] = isPrime[1] = false;    for (int i = 2; i * i <= n; i++) {        if (isPrime[i]) {            for (int j = i * i; j <= n; j += i) isPrime[j] = false;        }    }    std::vector<int> primes;    for (int i = 2; i <= n; i++) if (isPrime[i]) primes.push_back(i);    for (size_t k = 0; k < primes.size(); k++) std::cout << (k ? " " : "") << primes[k];    std::cout << '\n';     std::cout << toBase(x, base) << '\n';}
stdin
84 36
30
255 16
stdout
12
2 3 5 7 11 13 17 19 23 29
ff

要点

  • 最大公约数是能同时整除两个数的最大的数。
    不断把 (a, b) 换成 (b, a % b),直到 b 变成 0,剩下的 a 就是答案。
    (84, 36) → (36, 12) → (12, 0),所以是 12。
  • 这个方法正确,是因为能同时整除 a 和 b 的数,也能同时整除 b 和 a % b,反过来也成立。
    数会很快变小:用最费时的那类数来试,两个接近一万亿的数也只转了 57 圈。
  • 素数是大于 1、只能被 1 和它自己整除的数。
    要找出 n 以内的所有素数,就用筛法。
    从 2 开始依次看,遇到还没被划掉的数,它就是素数,然后把它的倍数全部划掉。
  • 倍数从 i * i 开始划就够了。
    比它小的倍数已经被更小的素数划掉了。
    出于同样的原因,外层循环只在 i * i <= n 时继续。
  • 要把数写成别的进制,就记下除以 base 的余数,再把数换成商,一直重复到 0。
    余数是从低位开始出来的,所以要接在前面。
    255 得到余数 15(f)、商 15,再得到余数 15(f)、商 0,所以是 ff。
  • 例子里没有这一步,不过要把这样写出的字符串反过来读成数,就从第一个字符开始重复 x = x * base + 数字。
    ff 是 0 → 15 → 15 × 16 + 15 = 255。
    10 及以上的数字从 a(10)开始用字母表示。

各语言的写法

Python有 math.gcd(a, b)。
写成 2、8、16 进制用 format(255, "b")、"o"、"x"(→ ff),读用 int("ff", 16)。
写成其他进制要像例子那样自己实现。
求商用 //
JavaScript没有最大公约数函数,要自己写。
写用 (255).toString(16)(→ ff),读用 parseInt("ff", 16)。
求商用 Math.floor(x / base)
TypeScript和 JavaScript 一样
Java没有用于 int 的最大公约数函数,要自己写(BigInteger 有 gcd)。
写用 Integer.toString(255, 16),读用 Integer.parseInt("ff", 16)
C#有 BigInteger.GreatestCommonDivisor(a, b)(System.Numerics),不过 int 之间自己写就行。
Convert.ToString(255, 16) 和 Convert.ToInt32("ff", 16) 只接受 2、8、10、16 进制(传 3 会抛出 ArgumentException)
C++<numeric> 里的 std::gcd(a, b)。
读用 std::stoi("ff", nullptr, 16),写要像例子那样自己实现。
筛子用 std::vector<bool>
C没有最大公约数函数,要自己写。
读用 strtol("ff", NULL, 16),写用 printf 的 %x(16 进制)和 %o(8 进制)。
其他进制先把数字倒着收集起来,再反转
Craykon没有最大公约数和进制函数,都要自己写。
求商用 数学.商(x, base),数字字符用 DIGITS[d],字符的值用 DIGITS.查找位置(字符)

常见错误

转换 0 得到了空字符串

def to_base(x, base):
    s = ""
    while x > 0:
        s = str(x % base) + s
        x //= base
    return s

print("[" + to_base(0, 2) + "]")

→ [] — 方括号之间什么都没有。x 为 0 时循环一次也不执行

x 为 0 时单独返回 "0"。

筛法中 i * i 超出了 intJava · C# · C++ · C

int n = 100000;
boolean[] isPrime = new boolean[n + 1];
Arrays.fill(isPrime, true);
for (int i = 2; i <= n; i++)
    if (isPrime[i])
        for (int j = i * i; j <= n; j += i) isPrime[j] = false;

→ ArrayIndexOutOfBoundsException: Index -2146737495 out of bounds for length 100001 — i 为 46349 时,i * i 超过约 21 亿,变成了负数。C# 抛出 IndexOutOfRangeException,C 和 C++ 因 SIGSEGV 崩溃

外层循环只在 i * i <= n 时继续。
n 为 10 万时,i 只会走到 316。

用 / 求商JavaScript · TypeScript · Craykon

let x = 5, s = "";
while (x > 0) {
  s = (x % 2) + s;
  x = x / 2;
}
console.log(s.length, s.slice(-12));

→ 23085 .6251.250.51 — 本该是 101,却得到了两万多个字符的字符串。/ 会一直除出小数(2.5、1.25、0.625……),x 要过很久才变成 0

只取商:JavaScript 用 Math.floor(x / 2),Craykon 用 数学.商(x, 2)。

各语言的差异
有内置函数就用,但有的语言没有,有的只支持 2、8、16 进制,所以要知道怎么自己写。
最小公倍数是 a / gcd(a, b) * b(84 和 36 是 252)。
先除后乘,中间值不容易变得太大。

练习题