原根
為了簡單起見,只考慮素數的情況。(並不是只有素數才有原根
定義:對於素數 $p$,如果存在一個正整數 $1<a<p$,使得 $a^1, a^2, ..., a^{p-1}$ 模 $p$ 的值取遍 $1,2,...,p-1$ 的所有整數,稱 $a$ 是 $p$ 的一個原根(primitive root),其實就是循環群的生成元。
如果 $a^j \equiv a^i(mod \ p)$,則 $i \equiv j(mod \ {p-1})$。這里有兩個例子:
- 3是7的原根,因為3-->2-->6-->4-->5-->1,然后開始循環
- 2不是7的原根,因為2-->4-->1-->2-->4...,過早的循環了
注意到 $a^{p-1} \equiv 1(mod \ p)$,這個生成序列一定會包含1,且在此之前不會有循環——要是在出現1之前就循環了,就永遠不會出現1了。
也就是說,原根的循環節為 $p-1$,非原根有較小的循環節,且是 $p-1$ 的約數(因為元素的階整除群的階)。
這就是判斷原根的方法:枚舉小循環長度$b$ (它一定是 $p-1$ 的真因子),判斷是否有 $m^b \equiv 1(mod \ p)$(如果是,則表示 $m$ 不是原根)。雖然這個方法理論上並不是很優秀,但在算法競賽中已經夠用。
通俗地說,如果是原根,群的階次方才為1;如果不是原根,群的階的約數次方就會出現1.
#include<bits/stdc++.h> using namespace std; typedef long long ll; ll qpow(ll a, ll b, ll p) { ll res = 1; while (b > 0) { if (b & 1) res = res * a % p; a = a * a % p, b >>= 1; } return res; } ll generator(ll p) { vector<ll> fact; ll phi = p - 1, n = phi; for (ll i = 2; i * i <= n; ++i) { if (n % i == 0) { fact.push_back(i); while (n % i == 0) n /= i; } } if (n > 1) fact.push_back(n); for (ll res = 2; res <= p; ++res) { bool ok = true; for (ll factor : fact) { if (qpow(res, phi / factor, p) == 1) { ok = false; break; } } if (ok) return res; } return -1; } int main() { printf("%d\n", generator(998244353)); }
值得注意的是,原根並不是唯一的。
有結論:設群 $G=(a)$,若 $|G|=n$,則 $G=(a^r)$ 當且僅當 $(r, n)=1$,即生成元有 $\varphi (n)$ 個。
在上文中,群為 ${1, 2, ..., p-1}$ 模 $p$ 的乘法群。
例如,$p=7$ 時,3為原根,(5, 6)=1,所以 3^5 %7=5 也是原根。