{ }codelper

学ぶ › Java

アルゴリズム · 30

最大公約数・素数・進数

余りで最大公約数を求め、篩で素数をふるい分け、数を別の進数で書きます。

数を扱う問題によく出てくる三つの道具です。
下の例は 84 と 36 の最大公約数を求め、30 以下の素数をすべて表示し、255 を 16進数で書きます。

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));    }}
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)。
先に割ってから掛けると、途中の値が大きくなりにくくなります。

練習問題