ریشه اولیه¶
تعریف¶
در حساب پیمانهای، یک عدد $g$ را «ریشه اولیه به پیمانه n» مینامند اگر هر عدد متباین (نسبت به هم اول) با $n$ با توانی از $g$ به پیمانه $n$ همنهشت باشد. به زبان ریاضی، $g$ یک «ریشه اولیه به پیمانه n» است اگر و تنها اگر برای هر عدد صحیح $a$ که $\gcd(a, n) = 1$ باشد، یک عدد صحیح $k$ وجود داشته باشد به طوری که:
$g^k \equiv a \pmod n$.
در این صورت، $k$ «اندیس» یا «لگاریتم گسسته» $a$ در پایه $g$ به پیمانه $n$ نامیده میشود. $g$ همچنین «مولد» گروه ضربی اعداد صحیح به پیمانه $n$ نامیده میشود.
به طور خاص، در حالتی که $n$ یک عدد اول باشد، توانهای ریشه اولیه تمام اعداد از $1$ تا $n-1$ را تولید میکنند.
وجود¶
ریشه اولیه به پیمانه $n$ وجود دارد اگر و تنها اگر:
- $n$ برابر با ۱، ۲، ۴ باشد، یا
- $n$ توانی از یک عدد اول فرد باشد $(n = p^k)$، یا
- $n$ دو برابر توانی از یک عدد اول فرد باشد $(n = 2 \cdot p^k)$.
این قضیه توسط گاوس در سال ۱۸۰۱ اثبات شد.
ارتباط با تابع فی اویلر¶
فرض کنید $g$ یک ریشه اولیه به پیمانه $n$ باشد. آنگاه میتوان نشان داد که کوچکترین عدد $k$ که در رابطه $g^k \equiv 1 \pmod n$ صدق میکند، برابر با $\phi (n)$ است. علاوه بر این، عکس این قضیه نیز درست است، و از این واقعیت در این مقاله برای یافتن ریشه اولیه استفاده خواهیم کرد.
همچنین، تعداد ریشههای اولیه به پیمانه $n$، در صورت وجود، برابر با $\phi (\phi (n) )$ است.
الگوریتم یافتن ریشه اولیه¶
یک الگوریتم ساده این است که تمام اعداد در بازه $[1, n-1]$ را در نظر بگیریم. سپس برای هر کدام بررسی کنیم که آیا ریشه اولیه است یا نه، این کار با محاسبه تمام توانهای آن و بررسی اینکه آیا همگی متفاوت هستند، انجام میشود. این الگوریتم پیچیدگی زمانی $O(g \cdot n)$ دارد که بسیار کند خواهد بود. در این بخش، یک الگوریتم سریعتر با استفاده از چند قضیه شناختهشده پیشنهاد میکنیم.
از بخش قبل میدانیم که اگر کوچکترین عدد $k$ که در رابطه $g^k \equiv 1 \pmod n$ صدق میکند برابر با $\phi (n)$ باشد، آنگاه $g$ یک ریشه اولیه است. از آنجایی که برای هر عدد $a$ متباین با $n$ از قضیه اویلر میدانیم که $a ^ { \phi (n) } \equiv 1 \pmod n$ است، برای بررسی اینکه آیا $g$ ریشه اولیه است یا نه، کافی است بررسی کنیم که برای تمام $d$ های کوچکتر از $\phi (n)$، رابطه $g^d \not \equiv 1 \pmod n$ برقرار باشد. با این حال، این الگوریتم هنوز هم بسیار کند است.
بر اساس قضیه لاگرانژ، میدانیم که مرتبه هر عدد به پیمانه $n$ باید مقسومعلیهی از $\phi (n)$ باشد. بنابراین، کافی است بررسی کنیم که برای تمام مقسومعلیههای سره $d \mid \phi (n)$، رابطه $g^d \not \equiv 1 \pmod n$ برقرار باشد. این الگوریتم به مراتب سریعتر است، اما هنوز هم میتوانیم آن را بهبود ببخشیم.
مقدار $\phi (n)$ را به عوامل اول تجزیه میکنیم: $\phi (n) = p_1 ^ {a_1} \cdots p_s ^ {a_s}$. اثبات میکنیم که در الگوریتم قبلی، کافی است فقط مقادیر $d$ را در نظر بگیریم که به شکل $\frac { \phi (n) } {p_j}$ هستند. در واقع، فرض کنید $d$ یک مقسومعلیه سره دلخواه از $\phi (n)$ باشد. آنگاه، بدیهی است که یک $j$ وجود دارد به طوری که $d \mid \frac { \phi (n) } {p_j}$، یعنی $d \cdot k = \frac { \phi (n) } {p_j}$. با این حال، اگر $g^d \equiv 1 \pmod n$ باشد، خواهیم داشت:
$g ^ { \frac { \phi (n)} {p_j} } \equiv g ^ {d \cdot k} \equiv (g^d) ^k \equiv 1^k \equiv 1 \pmod n$.
یعنی در میان اعداد به شکل $\frac {\phi (n)} {p_i}$، حداقل یکی وجود خواهد داشت که شرایط را برآورده نمیکند.
اکنون یک الگوریتم کامل برای یافتن ریشه اولیه داریم:
- ابتدا $\phi (n)$ را پیدا کرده و آن را تجزیه میکنیم.
-
سپس روی تمام اعداد $g \in [1, n]$ پیمایش میکنیم و برای هر عدد، جهت بررسی اینکه آیا ریشه اولیه است یا نه، کارهای زیر را انجام میدهیم:
- تمام مقادیر $g ^ { \frac {\phi (n)} {p_i}} \pmod n$ را محاسبه میکنیم.
- اگر تمام مقادیر محاسبهشده مخالف $1$ باشند، آنگاه $g$ یک ریشه اولیه است.
زمان اجرای این الگوریتم $O(Ans \cdot \log \phi (n) \cdot \log n)$ است (با فرض اینکه $\phi (n)$ دارای $\log \phi (n)$ مقسومعلیه است).
شوپ (Shoup) (۱۹۹۰، ۱۹۹۲) با فرض فرضیه ریمان تعمیمیافته، ثابت کرد که $g$ از مرتبه $O(\log^6 p)$ است.
پیادهسازی¶
کد زیر فرض میکند که پیمانه p
یک عدد اول است. برای اینکه کد برای هر مقدار p
کار کند، باید محاسبه $\phi (p)$ را به آن اضافه کنیم.
int powmod (int a, int b, int p) {
int res = 1;
while (b)
if (b & 1)
res = int (res * 1ll * a % p), --b;
else
a = int (a * 1ll * a % p), b >>= 1;
return res;
}
int generator (int p) {
vector<int> fact;
int phi = p-1, n = phi;
for (int 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 (int res=2; res<=p; ++res) {
bool ok = true;
for (size_t i=0; i<fact.size() && ok; ++i)
ok &= powmod (res, phi / fact[i], p) != 1;
if (ok) return res;
}
return -1;
}