{ }codelper

배우기 › JavaScript

알고리즘 · 30

최대공약수, 소수, 진법

나머지로 최대공약수를 구하고, 체로 소수를 걸러 내고, 수를 다른 진법으로 써요.

수를 다루는 문제에 자주 나오는 도구 세 가지예요.
아래 예제는 84와 36의 최대공약수를 구하고, 30 이하의 소수를 모두 찍고, 255를 16진법으로 써요.

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));
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도 모두 나누고, 거꾸로도 그렇기 때문이에요.
    수가 금방 작아져서, 가장 오래 걸리는 수로 해 봐도 1조에 가까운 두 수가 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)부터 글자로 써요.

언어별로 쓰는 법

Pythonmath.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)
TypeScriptJavaScript와 같아요
Javaint끼리의 최대공약수 함수가 없어서 직접 만들어요(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만 글자가 넘는 문자열이 나왔어요. /가 2.5, 1.25, 0.625…로 소수까지 나눠서 x가 한참 뒤에야 0이 돼요

몫만 구해요.
JavaScript는 Math.floor(x / 2), 크레이콘은 수학.몫(x, 2)예요.

체에서 i * i가 int를 넘었어요Java · 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진법만 되는 것도 있으니 직접 만드는 법을 알아 두세요.
최소공배수는 a / gcd(a, b) * b예요(84와 36이면 252).
먼저 나누고 곱해야 중간 값이 덜 커져요.

연습 문제