{ }codelper

学ぶ › Python

アルゴリズム · 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))
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" を返します。

篩で 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 までしか進みません。

商を / で求めた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)、Craykon は 数学.商(x, 2) です。

言語ごとの違い
組み込みの関数があればそれを使いますが、ない言語もあり、2・8・16進数しか扱えないものもあるので、自分で作る方法を知っておきましょう。
最小公倍数は a / gcd(a, b) * b です(84 と 36 なら 252)。
先に割ってから掛けると、途中の値が大きくなりにくくなります。

練習問題