アルゴリズム概要

問題の説明

原始根(Primitive Root)とは、素数 p に対して、g の累乗 g1, g2, ..., gp-1 を p で割った余りが、すべて異なる値になるような整数 g のことです。

例えば p = 7 の場合、g = 3 は以下のように原始根になります:

  • 31 mod 7 = 3
  • 32 mod 7 = 2
  • 33 mod 7 = 6
  • 34 mod 7 = 4
  • 35 mod 7 = 5
  • 36 mod 7 = 1

これらはすべて異なる値(1, 2, 3, 4, 5, 6)になるため、3 は 7 の原始根です。

入出力例

入力:
7
出力:
3 2

7 の原始根は 3 と 5 の2つあり、最小は 3 です。

制約条件

  • p は素数
  • 2 ≤ p ≤ 109

解法の戦略

原始根を効率的に判定するために、数学的な性質を活用します:

  1. 原始根の判定条件: g が原始根である ⇔ すべての (p-1) の素因数 q に対して、g(p-1)/q ≢ 1 (mod p)
  2. (p-1) の素因数分解: まず (p-1) を素因数分解します(O(√p) 時間)
  3. 最小原始根の探索: g = 2 から順に上記の判定条件をチェック
  4. 原始根の総数: オイラーのトーシェント関数 φ(p-1) で計算

主要ポイント

  • 時間計算量: O(√p + k·d·log p)
    k = 最小原始根の値、d = (p-1)の素因数の個数
  • 💾
    空間計算量: O(d)
    素因数リストの保存のみ
  • 🔧
    最適化手法: Pythonの組み込み関数 pow(base, exp, mod) を使用した高速累乗剰余演算

ステップバイステップ解説

Python実装

#!/bin/python3

import math
import os
import random
import re
import sys

from typing import List

def prime_factors(n: int) -> List[int]:
    """
    nの素因数をリストで返す(重複なし)

    Time Complexity: O(√n)
    """
    factors = []
    # 2で割り切れる場合
    if n % 2 == 0:
        factors.append(2)
        while n % 2 == 0:
            n //= 2

    # 3以降の奇数でチェック
    i = 3
    while i * i <= n:
        if n % i == 0:
            factors.append(i)
            while n % i == 0:
                n //= i
        i += 2

    # nが1より大きければ、それ自体が素数
    if n > 1:
        factors.append(n)

    return factors

def is_primitive_root(g: int, p: int, prime_divisors: List[int]) -> bool:
    """
    gがpの原始根かどうかを判定

    Args:
        g: 判定対象の整数
        p: 素数
        prime_divisors: (p-1)の素因数リスト

    Returns:
        gが原始根ならTrue

    Time Complexity: O(d·log p) where d = len(prime_divisors)
    """
    phi = p - 1

    # 各素因数qについて、g^((p-1)/q) ≢ 1 (mod p) を確認
    for q in prime_divisors:
        if pow(g, phi // q, p) == 1:
            return False

    return True

def euler_phi(n: int) -> int:
    """
    オイラーのトーシェント関数 φ(n) を計算

    Time Complexity: O(√n)
    """
    result = n
    p = 2
    while p * p <= n:
        if n % p == 0:
            while n % p == 0:
                n //= p
            result -= result // p
        p += 1

    if n > 1:
        result -= result // n

    return result

def solve_competitive(p: int) -> tuple:
    """
    競技プログラミング向け実装

    Args:
        p: 素数

    Returns:
        (最小原始根, 原始根の総数)

    Time Complexity: O(√p + k·d·log p)
        where k = 最小原始根の値, d = (p-1)の素因数の個数
    Space Complexity: O(d)
    """
    # (p-1)の素因数を求める
    prime_divisors = prime_factors(p - 1)

    # 最小原始根を探索
    smallest_root = 0
    for g in range(2, p):
        if is_primitive_root(g, p, prime_divisors):
            smallest_root = g
            break

    # 原始根の総数 = φ(p-1)
    total_count = euler_phi(p - 1)

    return smallest_root, total_count

if __name__ == '__main__':
    p = int(input().strip())

    smallest, total = solve_competitive(p)
    print(f"{smallest} {total}")

フローチャート

1 2 3 4 5 6 7 8 開始 入力: 素数 p 素数 p を読み込む (p-1) の素因数分解 関数: prime_factors(p-1) 結果: [q₁, q₂, ..., qₐ] の素因数リスト g = 2 に初期化 原始根の候補を2から開始 【ループA: 候補gをチェック】 原始根判定 【ループB: 各素因数qで確認】 すべてのqについて g^((p-1)/q) mod p ≠ 1 をチェック 原始根? すべてのqで≠1 はい 最小原始根発見! smallest_root = g いいえ g = g + 1 次の候補に進む ループバック φ(p-1) を計算 関数: euler_phi(p-1) 結果: 原始根の総数 結果出力 (smallest_root, total_count) 終了

📋 ステップバイステップの流れ:

1 開始 - アルゴリズムの実行を開始
2 入力 - 素数 p を読み込む
3 素因数分解 - (p-1) の素因数を求める → [q₁, q₂, ..., qₐ]
4 初期化 - 候補 g を 2 に初期化
5 原始根判定 - 各素因数 q について g^((p-1)/q) mod p ≠ 1 をチェック(ループB)
6 判定結果 - 原始根なら発見して次へ、そうでなければ g++ してループA に戻る
7 総数計算 - φ(p-1) を計算して原始根の総数を求める
8 出力と終了 - (最小原始根, 総数) を出力してアルゴリズム終了

💡 初学者向けポイント:
ループAは候補 g を順番にチェックする外側のループ
ループBは各素因数 q で判定する内側のループ
• ループバックの矢印は g++ 後に判定ステップに戻ることを示す
• ステップ番号で処理の順序が一目でわかる

計算量分析

項目 本実装 全探索(素朴) 備考
時間計算量 O(√p + k·d·log p) O(p²·log p) k = 最小原始根(通常小さい)
d = (p-1)の素因数個数
空間計算量 O(d) O(p) 素因数リストのみ保存
実装コスト 素因数分解が必要
p=10⁹での実用性 ◎ 高速 × TLE 制約の上限で性能差が顕著

最適化のポイント

  • 素因数分解の活用: (p-1) の素因数だけをチェックすることで、判定回数を大幅削減
  • 高速累乗剰余: Python の組み込み関数 pow(g, exp, p) を使用(C実装で高速)
  • 早期終了: 最小原始根が見つかった時点で探索を終了
  • 数学的性質: オイラーのトーシェント関数で総数を O(√p) で計算