算法 · 30
最大公约数、素数与进制
用余数求最大公约数,用筛法筛出素数,把数写成别的进制。
这是数字类题目里经常出现的三件工具。
下面的例子求出 84 和 36 的最大公约数,输出 30 以内的所有素数,再把 255 写成 16 进制。
DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz" def gcd(a, b): while b != 0: a, b = b, a % b # (a, b)를 (b, a를 b로 나눈 나머지)로 바꾼다 return a def to_base(x, base): if x == 0: return "0" s = "" while x > 0: s = DIGITS[x % base] + s # 나머지는 낮은 자리부터 나오므로 앞에 붙인다 x //= base return s a, b = map(int, input().split())n = int(input())x, base = map(int, input().split()) print(gcd(a, b)) # 체: 소수를 찾을 때마다 그 배수를 지운다. 끝까지 안 지워진 수가 소수is_prime = [True] * (n + 1)is_prime[0] = is_prime[1] = Falsei = 2while i * i <= n: if is_prime[i]: for j in range(i * i, n + 1, i): is_prime[j] = False i += 1primes = [i for i in range(2, n + 1) if is_prime[i]]print(*primes) print(to_base(x, base))const DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"; function gcd(a, b) { while (b !== 0) { [a, b] = [b, a % b]; // (a, b)를 (b, a를 b로 나눈 나머지)로 바꾼다 } return a;} function toBase(x, base) { if (x === 0) return "0"; let s = ""; while (x > 0) { s = DIGITS[x % base] + s; // 나머지는 낮은 자리부터 나오므로 앞에 붙인다 x = Math.floor(x / base); } return s;} const [a, b, n, x, base] = require("fs").readFileSync(0, "utf8").trim().split(/\s+/).map(Number); console.log(gcd(a, b)); // 체: 소수를 찾을 때마다 그 배수를 지운다. 끝까지 안 지워진 수가 소수const isPrime = new Array(n + 1).fill(true);isPrime[0] = isPrime[1] = false;for (let i = 2; i * i <= n; i++) { if (isPrime[i]) { for (let j = i * i; j <= n; j += i) isPrime[j] = false; }}const primes = [];for (let i = 2; i <= n; i++) if (isPrime[i]) primes.push(i);console.log(primes.join(" ")); console.log(toBase(x, base));import { readFileSync } from "node:fs";const DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"; function gcd(a: number, b: number): number { while (b !== 0) { [a, b] = [b, a % b]; // (a, b)를 (b, a를 b로 나눈 나머지)로 바꾼다 } return a;} function toBase(x: number, base: number): string { if (x === 0) return "0"; let s = ""; while (x > 0) { s = DIGITS[x % base] + s; // 나머지는 낮은 자리부터 나오므로 앞에 붙인다 x = Math.floor(x / base); } return s;} const [a, b, n, x, base] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number); console.log(gcd(a, b)); // 체: 소수를 찾을 때마다 그 배수를 지운다. 끝까지 안 지워진 수가 소수const isPrime: boolean[] = new Array(n + 1).fill(true);isPrime[0] = isPrime[1] = false;for (let i = 2; i * i <= n; i++) { if (isPrime[i]) { for (let j = i * i; j <= n; j += i) isPrime[j] = false; }}const primes: number[] = [];for (let i = 2; i <= n; i++) if (isPrime[i]) primes.push(i);console.log(primes.join(" ")); console.log(toBase(x, base));import java.util.Arrays;import java.util.Scanner; public class Main { static final String DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"; static int gcd(int a, int b) { while (b != 0) { int r = a % b; // (a, b)를 (b, a를 b로 나눈 나머지)로 바꾼다 a = b; b = r; } return a; } static String toBase(int x, int base) { if (x == 0) return "0"; String s = ""; while (x > 0) { s = DIGITS.charAt(x % base) + s; // 나머지는 낮은 자리부터 나오므로 앞에 붙인다 x /= base; } return s; } public static void main(String[] args) { Scanner in = new Scanner(System.in); int a = in.nextInt(), b = in.nextInt(), n = in.nextInt(), x = in.nextInt(), base = in.nextInt(); System.out.println(gcd(a, b)); // 체: 소수를 찾을 때마다 그 배수를 지운다. 끝까지 안 지워진 수가 소수 boolean[] isPrime = new boolean[n + 1]; Arrays.fill(isPrime, 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; } } StringBuilder primes = new StringBuilder(); for (int i = 2; i <= n; i++) { if (isPrime[i]) primes.append(primes.length() > 0 ? " " : "").append(i); } System.out.println(primes); System.out.println(toBase(x, base)); }}using System;using System.Collections.Generic; class Program{ const string DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"; static int Gcd(int a, int b) { while (b != 0) { (a, b) = (b, a % b); // (a, b)를 (b, a를 b로 나눈 나머지)로 바꾼다 } return a; } static string ToBase(int x, int b) { if (x == 0) return "0"; string s = ""; while (x > 0) { s = DIGITS[x % b] + s; // 나머지는 낮은 자리부터 나오므로 앞에 붙인다 x /= b; } return s; } static void Main() { string[] ab = Console.ReadLine().Split(); int a = int.Parse(ab[0]), b = int.Parse(ab[1]); int n = int.Parse(Console.ReadLine()); string[] xb = Console.ReadLine().Split(); int x = int.Parse(xb[0]), numBase = int.Parse(xb[1]); // base는 C#의 예약어라 이름을 바꿨다 Console.WriteLine(Gcd(a, b)); // 체: 소수를 찾을 때마다 그 배수를 지운다. 끝까지 안 지워진 수가 소수 bool[] isPrime = new bool[n + 1]; Array.Fill(isPrime, 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; } } List<int> primes = new List<int>(); for (int i = 2; i <= n; i++) if (isPrime[i]) primes.Add(i); Console.WriteLine(string.Join(" ", primes)); Console.WriteLine(ToBase(x, numBase)); }}#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';}#include <stdio.h>#include <string.h> const char *DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz";char isPrime[10001]; int gcd(int a, int b) { while (b != 0) { int r = a % b; // (a, b)를 (b, a를 b로 나눈 나머지)로 바꾼다 a = b; b = r; } return a;} // s에 x를 base진법으로 적는다void toBase(int x, int base, char *s) { char buf[40]; int len = 0; if (x == 0) buf[len++] = '0'; while (x > 0) { buf[len++] = DIGITS[x % base]; // 나머지는 낮은 자리부터 나온다 x /= base; } for (int i = 0; i < len; i++) s[i] = buf[len - 1 - i]; // 그래서 거꾸로 옮긴다 s[len] = '\0';} int main(void) { int a, b, n, x, base; scanf("%d %d %d %d %d", &a, &b, &n, &x, &base); printf("%d\n", gcd(a, b)); // 체: 소수를 찾을 때마다 그 배수를 지운다. 끝까지 안 지워진 수가 소수 memset(isPrime, 1, n + 1); isPrime[0] = isPrime[1] = 0; for (int i = 2; i * i <= n; i++) { if (isPrime[i]) { for (int j = i * i; j <= n; j += i) isPrime[j] = 0; } } int count = 0; for (int i = 2; i <= n; i++) { if (isPrime[i]) printf(count++ ? " %d" : "%d", i); } printf("\n"); char s[40]; toBase(x, base, s); printf("%s\n", s); return 0;}DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"; gcd(a, b){ 当 (b != 0) { r = a % b; // (a, b)를 (b, a를 b로 나눈 나머지)로 바꾼다 a = b; b = r; } 返回 a;} toBase(x, base){ 如果 (x == 0) { 返回 "0"; } s = ""; 当 (x > 0) { s = DIGITS[x % base] + s; // 나머지는 낮은 자리부터 나오므로 앞에 붙인다 x = 数学.商(x, base); } 返回 s;} ab = 控制台.读数字();n = 控制台.读行().转数字();xb = 控制台.读数字(); 控制台.输出(gcd(ab[0], ab[1])); // 체: 소수를 찾을 때마다 그 배수를 지운다. 끝까지 안 지워진 수가 소수isPrime = 新列表(n + 1, 真);isPrime[0] = 假;isPrime[1] = 假;循环 (i = 2; i * i <= n; i++){ 如果 (isPrime[i]) { 循环 (j = i * i; j <= n; j += i) { isPrime[j] = 假; } }}primes = [];循环 (i = 2; i <= n; i++){ 如果 (isPrime[i]) { primes.添加(i); }}控制台.输出(primes); 控制台.输出(toBase(xb[0], xb[1]));84 36 30 255 16
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"。
用 / 求商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)。
筛法中 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。
各语言的差异
有内置函数就用,但有的语言没有,有的只支持 2、8、16 进制,所以要知道怎么自己写。
最小公倍数是
先除后乘,中间值不容易变得太大。
有内置函数就用,但有的语言没有,有的只支持 2、8、16 进制,所以要知道怎么自己写。
最小公倍数是
a / gcd(a, b) * b(84 和 36 是 252)。先除后乘,中间值不容易变得太大。