下面的所有知识均属数论范畴。
LucasLucasLucas 定理
当 ppp 为质数时有:
Cnm mod p=Cn/pm/p∗Cn mod pm mod p mod p C_n^m \bmod p= C_{n / p}^{m / p} * C_{n~\bmod~p}^{m~\bmod~p} \bmod p Cnmmodp=Cn/pm/p∗Cn mod pm mod pmodp
代码就不贴了 (实际上是懒)。
如果 ppp 不为质数呢?
那么我们先用唯一分解定理将 ppp 分解:
p=∑ki∈Primekiαi
p = \sum_{k_i \in Prime} k_i^{\alpha_i}
p=ki∈Prime∑kiαi
然后把原式拆分为若干个同余方程,最后用中国剩余定理合并即可。
现在我们讨论如何求 Cnm mod kiαiC_n^m \bmod k_i^{\alpha_i}Cnmmodkiαi,而 Cnm=n!m!(n−m)!C_n^m = \frac{n!}{m!(n - m)!}Cnm=m!(n−m)!n!,所以问题本质上是求阶乘的取模。
设我们要求的是 P! mod kiαiP! \bmod~k_i^{\alpha_i}P!mod kiαi,那么我们可以把 P!P!P! 包含的所有 kik_iki 质因子取出,由于剩下的数与模数互质,所以我们就可以直接求逆元了。
所以最终要求的问题是求:
n!kiβ1m!kiβ2⋅(n−m)!kiβ3⋅kiβ1−β2−β3 mod kiαi \frac{\frac{n!}{k_i^{\beta_1}}}{\frac{m!}{k_i^{\beta_2}} \cdot \frac{(n - m)!}{k_i^{\beta_3}}} \cdot k_i^{\beta_1 - \beta_2 - \beta_3}\bmod k_i^{\alpha_i} kiβ2m!⋅kiβ3(n−m)!kiβ1n!⋅kiβ1−β2−β3modkiαi
不过我们还是先从研究 P! mod kiαiP! \bmod~k_i^{\alpha_i}P!mod kiαi 开始。
同 OI−wikiOI-wikiOI−wiki,我们用 22! mod 3222! \bmod 3^222!mod32 举例。
我们知道:
22!=1×2×3×4×5×6×7×8×9×10×11×12×13×14×15×16×17×18×19×20×21×22.
22! = 1\times2\times3\times4\times5\times6\times7\times8\times9\times10\times11\times12\times13\times14\times15\times16\times17\times18\times19\times20\times21\times22.
22!=1×2×3×4×5×6×7×8×9×10×11×12×13×14×15×16×17×18×19×20×21×22.
我们把 333 的倍数提出来,化简一下有:
22!=37×(1×2×3×4×5×6×7)×(1×2×4×5×7×8×10×11×13×14×16×17×19×20×22)
22! = 3^7\times(1\times2\times3\times4\times5\times6\times7) \times(1\times2\times4\times5\times7\times8\times10\times11\times13\times14\times16\times17\times19\times20\times22)
22!=37×(1×2×3×4×5×6×7)×(1×2×4×5×7×8×10×11×13×14×16×17×19×20×22)
第一个部分是 kik_iki 的 ⌊Pki⌋\lfloor\frac{P}{k_i}\rfloor⌊kiP⌋ 次幂,这个部分只需要记录指数,不需要求具体的值;
第二个部分是 ⌊Pki⌋!\lfloor\frac{P}{k_i}\rfloor!⌊kiP⌋!,可以递归求解;
第三个部分是 1×2×4×5×7×8×10×11×13×14×16×171\times2\times4\times5\times7\times8\times10\times11\times13\times14\times16\times171×2×4×5×7×8×10×11×13×14×16×17,仔细观察可以发现:
1×2×4×5×7×8≡10×11×13×14×16×17 ( mod kiαi)
1\times2\times4\times5\times7\times8 \equiv10\times11\times13\times14\times16\times17~(\bmod~k_i^{\alpha_i})
1×2×4×5×7×8≡10×11×13×14×16×17 (mod kiαi)
形式化地,有:(t∈Z+t \in Z_+t∈Z+)
∏i=1,gcd(i,ki)=1kiαii≡∏i=1,gcd(i,ki)=1kiαi(i+tkiαi) ( mod kiαi)
\prod_{i = 1,\gcd(i, k_i) = 1}^{k_i^{\alpha_i}} i \equiv \prod_{i = 1,\gcd(i, k_i) = 1}^{k_i^{\alpha_i}} (i + tk_i^{\alpha_i}) ~(\bmod k_i^{\alpha_i})
i=1,gcd(i,ki)=1∏kiαii≡i=1,gcd(i,ki)=1∏kiαi(i+tkiαi) (modkiαi)
这个循环节一共循环了 ⌊Pkiαi⌋\lfloor\frac{P}{k_i^{\alpha_i}}\rfloor⌊kiαiP⌋ 次,我们直接求出来之后快速幂即可。
最后是 19×20×2219 \times 20\times 2219×20×22,循环节的余项,表示出来即为:
∏i=1,gcd(i,ki)=1P mod kiαii
\prod_{i = 1,\gcd(i, k_i) = 1}^{P~\bmod~k_i^{\alpha_i}} i
i=1,gcd(i,ki)=1∏P mod kiαii
分步求解即可。
如果多测并且模数相同,可以对模数的每一个质因子 kik_iki,求出所有 P≤kiαiP \le k_i^{\alpha_i}P≤kiαi 的 ∏i=1,gcd(i,ki)=1Pi\prod_{i = 1, \gcd(i, k_i) = 1}^P i∏i=1,gcd(i,ki)=1Pi。
贴上代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
using LL = long long;
using namespace std;
LL n, m, p;
inline LL Get(LL k, LL p) { if(k >= p) k -= p; return k; }
inline void Exgcd(LL a, LL b, LL &x, LL &y)
{
if(b == 0LL) return x = 1LL, y = 0LL, void();
Exgcd(b, a % b, y, x);
return y -= a / b * x, void();
}
inline LL Pow(LL a, LL b, LL p)
{
LL res = 1LL;
while(b)
{
if(b & 1) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
inline LL Fac(LL k, LL pi, LL pk)
{
if(!k) return 1LL;
LL res = 1LL;
for(register LL i = 2LL; i <= pk; i++) if(i % pi) res = res * i % pk;
res = Pow(res, k / pk, pk);
for(register LL i = 2LL; i <= k % pk; i++) if(i % pi) res = res * i % pk;
return res * Fac(k / pi, pi, pk) % pk;
}
inline LL Inv(LL k, LL p)
{
LL x, y; Exgcd(k, p, x, y);
return Get(x % p + p, p);
}
inline LL CRT(LL k, LL t) { return k * (p / t) % p * Inv(p / t, t) % p; }
inline LL C(LL n, LL m, LL pi, LL pk)
{
LL v1 = Fac(n, pi, pk), v2 = Fac(m, pi, pk), v3 = Fac(n - m, pi, pk);
LL k = 0LL;
for(register LL i = n; i; i /= pi) k += i / pi;
for(register LL i = m; i; i /= pi) k -= i / pi;
for(register LL i = n - m; i; i /= pi) k -= i / pi;
return v1 * Inv(v2, pk) % pk * Inv(v3, pk) % pk * Pow(pi, k, pk) % pk;
}
inline LL ExLucas(LL n, LL m, LL p)
{
LL ans = 0LL, t = p, pt = 1;
for(register int i = 2; i * i <= p; i++) if(t % i == 0)
{
while(t % i == 0) pt *= i, t /= i;
ans = Get(ans + CRT(C(n, m, i, pt), pt), p);
pt = 1;
}
if(t > 1) ans = Get(ans + CRT(C(n, m, t, t), t), p);
return ans;
}
int main()
{
#ifdef FILE
freopen("Input.in", "r", stdin);
#endif
scanf("%lld%lld%lld", &n, &m, &p);
printf("%lld\n", ExLucas(n, m, p));
return 0;
}
BSGSBSGSBSGS 算法
当 a⊥pa \perp pa⊥p 时有如下算法:
设 t=⌊p⌋t = \lfloor\sqrt{p}\rfloort=⌊p⌋,则方程可以化为:
ait−k≡b ( mod p)
a^{it - k} \equiv b ~(\bmod~p)
ait−k≡b (mod p)
进一步有:(如果 a⊥ba \perp ba⊥b 不成立,这一步得出的结论不能推至上一步)
(at)i≡bak ( mod p) (a^t)^i \equiv b a^k~(\bmod~p) (at)i≡bak (mod p)
因为 k∈[0,t)k \in [0, t)k∈[0,t),所以可以把同余式右侧的式子用哈希表存储(如有重复选择最大的 kkk 即可),然后从小到大枚举同余式左侧的 iii,如果对于某个 iii,等式左边的值可以在哈希表中查到,那么此时的 it−kit - kit−k 就是最小的 xxx。
代码不贴 ,因为懒。
如果 a⊥pa \perp pa⊥p 不成立怎么办?
根据模的分配律,我们设 g=gcd(a,p)g = \gcd(a, p)g=gcd(a,p),那么原方程可以变成:
ax−1⋅ag≡bg ( mod pg)
a^{x - 1}\cdot\frac{a}{g} \equiv \frac{b}{g} ~(\bmod~\frac{p}{g})
ax−1⋅ga≡gb (mod gp)
所以当 b≠1b \ne 1b=1 且 g∤bg \nmid bg∤b 时,该方程无解。
如果保证一直有解,那么我们将这个方程一直化开,直到 a⊥p∑i=1kgia \perp \frac{p}{\sum_{i = 1}^k g_i}a⊥∑i=1kgip。
此时有:(设 q=∑i=1kagiq = \sum_{i = 1}^k\frac{a}{g_i}q=∑i=1kgia)
ax−k≡bq∑i=1kgi ( mod p∑i=1kgi)
a^{x - k} \equiv \frac{b}{q \sum_{i = 1}^k g_i} ~(\bmod~\frac{p}{\sum_{i = 1}^k g_i})
ax−k≡q∑i=1kgib (mod ∑i=1kgip)
这个时候就可以用 BSGSBSGSBSGS 求解了,最后的答案记得要加上 kkk。
当然,算法过程中还需要特判 q=b∑i=1kgiq = \frac{b}{\sum_{i = 1}^k g_i}q=∑i=1kgib 的情况,此时左侧 aaa 的次数为 000,答案即为 kkk。
贴上代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<cctype>
#include<map>
using namespace std;
int a, p, b;
map<int, int> Hash;
inline int Read()
{
int v = 0; char c = getchar();
while(!isdigit(c)) c = getchar();
while(isdigit(c)) v = (v << 3) + (v << 1) + c - '0', c = getchar();
return v;
}
inline int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
inline void Exgcd(int a, int b, int &x, int &y)
{
if(b == 0) return x = 1, y = 0, void();
Exgcd(b, a % b, y, x);
return y -= a / b * x, void();
}
inline int Inv(int a, int b)
{
int x, y; Exgcd(a, b, x, y);
return ((x % b) + b) % b;
}
inline int Pow(int a, int b, int p)
{
int res = 1;
while(b)
{
if(b & 1) res = 1LL * res * a % p;
a = 1LL * a * a % p;
b >>= 1;
}
return res;
}
inline int BSGS(int a, int b, int p)
{
Hash.clear(), b %= p;
int t = (int)(sqrt(p) + 1), w = 1;
for(register int i = 0; i < t; i++)
{
int val = 1LL * b * w % p;
Hash[val] = i;
w = 1LL * w * a % p;
}
int val = 1; a = Pow(a, t, p), val = 1;
if(!a) return b ? -1 : 1;
for(register int i = 1; i <= t; i++)
{
val = 1LL * val * a % p;
int j = Hash.find(val) == Hash.end() ? -1 : Hash[val];
if((~j) && i * t - j >= 0) return i * t - j;
}
return -1;
}
inline int ExBSGS(int a, int b, int p)
{
if(b == 1 || p == 1) return 0;
int g = gcd(a, p), k = 0, q = 1;
while(g ^ 1)
{
if(b % g) return -1;
++k; b /= g, p /= g;
q = 1LL * q * (a / g) % p;
if(q == b) return k;
g = gcd(a, p);
}
int res = BSGS(a, 1LL * b * Inv(q, p) % p, p);
return (~res) ? res + k : res;
}
int main()
{
#ifdef FILE
freopen("Input.in", "r", stdin);
#endif
a = Read(), p = Read(), b = Read();
while(a || p || b)
{
a %= p, b %= p;
int ans = ExBSGS(a, b, p);
if(ans == -1) puts("No Solution");
else printf("%d\n", ans);
a = Read(), p = Read(), b = Read();
}
return 0;
}
中国剩余定理
如果保证所有的 pip_ipi 之间两两互质,有如下算法:
设 M=∏i=1mpiM = \prod_{i = 1}^mp_iM=∏i=1mpi,Mi=MpiM_i = \frac{M}{p_i}Mi=piM,Mi−1M_i^{-1}Mi−1 为 MiM_iMi 在模 pip_ipi 意义下的逆元。
设每个方程的特解为 xix_ixi,则方程组特解为 ∑i=1mxiMiMi−1\sum_{i = 1}^m x_iM_iM_i^{-1}∑i=1mxiMiMi−1。
代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#define MAXN 1005
using namespace std;
int n;
long long a[MAXN], b[MAXN], sum = 1, ans;
void exgcd(long long a, long long b, long long &x, long long &y)
{
if(b == 0)
{
x = 1;
y = 0;
return;
}
exgcd(b, a % b, x, y);
int z = x;
x = y;
y = z - (a / b) * y;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%lld%lld", &a[i], &b[i]);
sum *= a[i];
}
for(int i = 1; i <= n; i++)
{
long long m = sum / a[i], x = 0, y = 0;
exgcd(m, a[i], x, y);
ans += b[i] * m * (x < 0 ? x + a[i] : x);
}
printf("%lld", ans % sum);
return 0;
}
如果不保证 pip_ipi 两两互质该怎么办?
假设现在我们已经得出了前 i−1i - 1i−1 个方程的通解 x+kM (k∈Z)x + kM~(k \in Z)x+kM (k∈Z),其中 MMM 显然为 LCMj=1i−1 pjLCM_{j = 1}^{i - 1}~p_jLCMj=1i−1 pj,那么对于第 iii 个方程,我们就是要找到一个 kkk 满足:
x+kM≡ai ( mod pi) x + kM \equiv a_i ~(\bmod~p_i) x+kM≡ai (mod pi)
显然这可以化为一个二元一次方程,用 exgcdexgcdexgcd 求解即可(同时可以用裴蜀定理判断无解情况)。
代码:(用上了龟速乘避免溢出)
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cctype>
#define MAXN 100005
#define up(i, l, r) for(int i = (l); i <= (r); i = -~i)
#define down(i, l, r) for(int i = (l); i >= (r); i--)
using i64 = long long;
using namespace std;
namespace IO
{
int f; char c;
template<typename T> inline void _Read(T &v)
{
v = 0; f = 1; c = getchar();
while(!isdigit(c)) { if(c == '-') f = -1; c = getchar(); }
while(isdigit(c)) { v = (v << 3) + (v << 1) + (int)(c - '0'); c = getchar(); }
v *= f;
return;
}
template<typename T> inline void _Write(T k)
{
if(k < 0) { putchar('-'); k = -k; }
if(k > 9) _Write(k / 10);
putchar((char)(k % 10 + '0'));
return;
}
inline int Read() { int v; _Read(v); return v; }
inline i64 Read64() { i64 v; _Read(v); return v; }
inline void Write(int v, char ed = '\n') { _Write(v); putchar(ed); return; }
inline void Write64(i64 v, char ed = '\n') { _Write(v); putchar(ed); return; }
}
using IO :: Read;
using IO :: Read64;
using IO :: Write;
using IO :: Write64;
int n;
i64 b[MAXN], a[MAXN], mul, ans;
inline void Mul(i64 &res, i64 a, i64 b, i64 p)
{
res = 0LL;
while(b)
{
if(b & 1LL) res = (res + a) % p;
a = (a << 1) % p;
b >>= 1;
}
return;
}
inline i64 Exgcd(i64 a, i64 b, i64 &x, i64 &y)
{
if(b == 0LL) return x = 1LL, y = 0LL, a;
i64 c = Exgcd(b, a % b, y, x);
return y -= (a / b) * x, c;
}
int main()
{
n = Read();
up(i, 1, n) a[i] = Read64(), b[i] = Read64();
mul = a[1]; ans = b[1];
up(i, 2, n)
{
i64 A = mul, B = a[i], C = ((b[i] - ans) % B + B) % B, x, y;
i64 D = Exgcd(A, B, x, y), E = B / D;
Mul(x, x, C / D, E);
ans += mul * x; mul *= E;
ans = (ans % mul + mul) % mul;
}
Write64(ans);
return 0;
}
原根
- 定义部分
设 a,pa,pa,p 互质且 p>1p > 1p>1,则使得同余式 an≡1 ( mod p)a^n \equiv 1~(\bmod~p)an≡1 (mod p) 成立的最小的 nnn 被称为 aaa 模 ppp 的阶,记为 δp(a)\delta_p(a)δp(a)。
由欧拉定理 aφ(p)≡1 ( mod p)a^{\varphi(p)} \equiv 1~(\bmod p)aφ(p)≡1 (modp),可知 δp(a) ∣ φ(p)\delta_p(a)~|~\varphi(p)δp(a) ∣ φ(p)。
若存在 g⊥pg \perp pg⊥p 使得 δp(g)=φ(p)\delta_p(g) = \varphi(p)δp(g)=φ(p),则称 ggg 为模 ppp 的原根。
- 原根存在定理
一个大于 111 的整数 nnn 存在原根,当且仅当 n=2,4,pα,2pαn = 2,4,p^\alpha, 2p^\alphan=2,4,pα,2pα(其中 ppp 为奇素数,α\alphaα 为正整数)。
(由于笔者太菜,这里的证明省去。事实上,这里的证明的确十分复杂,并不建议深究。)
- 原根个数
若 nnn 有原根,其个数为 φ(φ(n))\varphi(\varphi(n))φ(φ(n))。
如何证明?
设任一原根为 ggg,而阶存在性质 δn(gk)=δn(g)gcd(δn(g),k)\delta_n(g^k) = \frac{\delta_n(g)}{\gcd(\delta_n(g), k)}δn(gk)=gcd(δn(g),k)δn(g) (这里也略去证明,感兴趣的可以上网查找),说明如果 k⊥φ(n)k \perp \varphi(n)k⊥φ(n),那么 δn(gk)=δn(g)=φ(n)\delta_n(g^k) = \delta_n(g) = \varphi(n)δn(gk)=δn(g)=φ(n),即 gkg^kgk 也为 nnn 的原根。
可以发现这样的 kkk 恰好有 φ(φ(n))\varphi(\varphi(n))φ(φ(n)) 个,并且由于原根的定义,这些数互不相同。
而对于这样的一些 kkk,所有的 gkv mod n (v∈[1,φ(n)])g^{kv} \bmod n~(v \in [1, \varphi(n)])gkvmodn (v∈[1,φ(n)]) 也会互不相同,因为 gkg^kgk 模 nnn 的阶为 φ(n)\varphi(n)φ(n)。
所以,这些数确实为 nnn 的所有原根。
得证。
- 原根求法
我们先枚举求出 nnn 的最小原根 ggg。
如何判定一个数 ggg 是否为 nnn 的原根呢?
如果 n=2n = 2n=2,原根只能为 111,可以直接判定。
否则,ggg 为模 nnn 的原根的充要条件是:对于 φ(n)\varphi(n)φ(n) 的每个质因数 ddd,都不满足 gφ(n)d≡1 ( mod n)g^{\frac{\varphi(n)}{d}} \equiv 1 ~(\bmod~n)gdφ(n)≡1 (mod n)。(再次略去证明)
这样,我们就可以先把 φ(n)\varphi(n)φ(n) 分解质因数,然后暴力枚举判断了。
为什么暴力枚举不会超时?
- 我国数学家王元于 195919591959 年证明了若 nnn 有原根,其最小原根是不超过 n0.25n^{0.25}n0.25 级别的。所以枚举的时间复杂度在可承受范围内。
然后对于所有与 φ(n)\varphi(n)φ(n) 互质的 k∈[1,φ(n))k \in [1, \varphi(n))k∈[1,φ(n)), gk mod ng^k \bmod ngkmodn 均为 nnn 的原根。
贴上洛谷模板题代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cassert>
#include<algorithm>
#define MAXN 1000001
using namespace std;
int t, n, d, phi[MAXN], len, pr[MAXN], cnt, ans[MAXN];
bool np[MAXN], rt[MAXN];
inline void Sieve()
{
for(register int i = 2; i < MAXN; i++) phi[i] = i;
for(register int i = 2; i < MAXN; i++)
{
if(!np[i])
{
rt[i] = true; if((i << 1) < MAXN) rt[i << 1] = true; phi[i] = i - 1;
for(register int j = 2; i * j < MAXN; j++) np[i * j] = true, phi[i * j] = phi[i * j] / i * (i - 1);
if(i & 1 && i <= 1000)
{
int tmp = i * i;
while(tmp < MAXN) { rt[tmp] = true; if((tmp << 1) < MAXN) rt[tmp << 1] = true; tmp *= i; }
}
}
}
return;
}
inline void Divide(int k)
{
len = 0;
for(register int i = 2; i * i <= k; i++)
{
if(k % i == 0)
{
pr[++len] = i;
while(k % i == 0) k /= i;
}
}
if(k > 1) pr[++len] = k;
return;
}
inline int pw(int a, int b, int p)
{
int res = 1;
while(b)
{
if(b & 1) res = 1LL * res * a % p;
a = 1LL * a * a % p;
b >>= 1;
}
return res;
}
inline bool Check(int a, int b, int p)
{
for(register int i = 1; i <= len; i++) if(pw(a, b / pr[i], p) == 1) return false;
return true;
}
inline int gcd(int a, int b) { if(b == 0) return a; return gcd(b, a % b); }
int main()
{
#ifdef FILE
freopen("Input.in", "r", stdin);
freopen("Output.out", "w", stdout);
#endif
Sieve();
scanf("%d", &t);
while(t--)
{
cnt = 0;
scanf("%d%d", &n, &d);
if(!rt[n]) { puts("0\n"); continue; }
Divide(phi[n]);
int g; for(register int i = 1; i < n; i++) if(pw(i, phi[n], n) == 1 && Check(i, phi[n], n)) { g = i; break; }
int val = 1;
for(register int i = 1; i <= phi[n]; i++)
{
val = 1LL * val * g % n;
if(gcd(i, phi[n]) == 1) ans[++cnt] = val;
}
printf("%d\n", cnt), sort(ans + 1, ans + cnt + 1);
for(register int i = d; i <= cnt; i += d) printf("%d ", ans[i]);
putchar('\n');
}
return 0;
}
二次剩余
下文仅介绍 ppp 为奇素数时的 CipollaCipollaCipolla 算法。
如无特殊情况,所有的定义、运算都是在 mod p\bmod~pmod p 意义下的。
- 定义部分
如果对于非 000 整数 nnn,使得 x2≡n ( mod p)x ^ 2 \equiv n ~(\bmod~p)x2≡n (mod p) 无解,称 nnn 为二次非剩余,否则为二次剩余。
否则,我们来讨论一下解的情况。
设有一组不等的解为 x1,x2x_1,x_2x1,x2,则有 x12≡x22x_1^2 \equiv x_2^2x12≡x22,移项后有 (x1−x2)(x1+x2)≡0(x_1 - x_2)(x_1 + x_2) \equiv 0(x1−x2)(x1+x2)≡0,而前者不等于 000,所以 x1,x2x_1, x_2x1,x2 必为相反数。
并且模意义下,由于 ppp 为奇素数,因此它们互相之间是不相等的。
也就是说,方程最多有两互异解,并且互为相反数。这样,我们就可以判断唯一解的情况了:直接判断 n=0n = 0n=0 是否成立,成立则 x=0x=0x=0 为唯一解。
进一步地,我们发现:任意一对相反数对应唯一一个二次剩余,所以二次剩余的个数为 p−12\frac{p - 1}{2}2p−1,其它的非 000 整数即为二次非剩余,数量与其相等。
- 欧拉判别准则
若 x⊥px \perp px⊥p ,当 xp−12≡1 ( mod p)x ^ {\frac{p - 1}{2}} \equiv 1~(\bmod~p)x2p−1≡1 (mod p) 成立时,xxx 为模 ppp 的二次剩余,否则为二次非剩余。
即 “xxx 是模 ppp 的二次剩余” 是 “xp−12≡1 ( mod p)x ^ {\frac{p - 1}{2}} \equiv 1~(\bmod~p)x2p−1≡1 (mod p) 成立” 的充要条件。
充分性:设 x≡k2 ( mod p)x \equiv k ^ 2 ~(\bmod~p)x≡k2 (mod p),由费马小定理可知 xp−12≡kp−1≡1 ( mod p)x ^ {\frac{p - 1}{2}} \equiv k ^ {p - 1} \equiv 1 ~(\bmod~p)x2p−1≡kp−1≡1 (mod p)。
必要性:设 ggg 为 ppp 的一个原根,由于 x⊥px \perp px⊥p,则必定存在一个正整数 kkk 使得 x≡gk ( mod p)x \equiv g^k ~(\bmod~p)x≡gk (mod p) 。又因为 xp−12≡gk(p−1)2≡1 ( mod p)x^{\frac{p - 1}{2}} \equiv g^{\frac{k(p - 1)}{2}} \equiv 1 ~(\bmod~p)x2p−1≡g2k(p−1)≡1 (mod p),所以 k(p−1)2{\frac{k(p - 1)}{2}}2k(p−1) 一定是 φ(p)=p−1\varphi(p) = p - 1φ(p)=p−1 的倍数,说明 kkk 是一个偶数。所以令 m=gk2m = g^{\frac{k}{2}}m=g2k 就有 x≡m2 ( mod p)x \equiv m^2 ~(\bmod~p)x≡m2 (mod p)。
综上,我们只要判断 xp−12≡1 ( mod p)x ^ {\frac{p - 1}{2}} \equiv 1~(\bmod~p)x2p−1≡1 (mod p) 是否成立,就可以求出 xxx 是否为二次剩余。
- 求法
判定是否为二次剩余后,我们怎样求出方程的解呢?
所以,下面介绍 CipollaCipollaCipolla 算法。
对于二次剩余方程 x2≡n ( mod p)x^2 \equiv n ~(\bmod~p)x2≡n (mod p),我们先随机出一个 kkk 使得 k2−nk ^ 2 - nk2−n 为二次非剩余,由于其个数与二次剩余相等,因此期望 222 次就能找到。
然后我们类似虚数,定义 i2≡k2−n ( mod p)i ^ 2 \equiv k ^ 2 - n ~(\bmod~p)i2≡k2−n (mod p)。
接下来摆定理:x=(k+i)p+12x = (k + i) ^ {\frac{p + 1}{2}}x=(k+i)2p+1 为原方程的一个解。
证明:
首先通过二项式定理展开,再用 LucasLucasLucas 定理化简得 (k+i)p≡kp+ip ( mod p)(k + i) ^ p \equiv k^p + i ^ p ~(\bmod~p)(k+i)p≡kp+ip (mod p)。
根据欧拉判别法有:
(i2)p−12≡ip−1≡−1 ( mod p)ip≡−i ( mod p)
\begin{aligned}
&(i ^ 2) ^ {\frac{p - 1}{2}} \equiv i ^ {p - 1} \equiv -1~(\bmod~p)
\\
&i ^ p \equiv -i ~(\bmod~p)
\end{aligned}
(i2)2p−1≡ip−1≡−1 (mod p)ip≡−i (mod p)
由费马小定理可知 kp≡k ( mod p)k ^ p \equiv k ~(\bmod~p)kp≡k (mod p)。
所以:
x2≡(k+i)p+1≡(k+i)p(k+i)≡(kp+ip)(k+i)≡(k−i)(k+i)=k2−i2=n
\begin{aligned}
x ^ 2 &\equiv (k + i)^{p + 1}
\\
& \equiv (k + i) ^ p (k + i)
\\
& \equiv (k ^ p + i ^ p)(k + i)
\\
& \equiv (k - i)(k + i)
\\
& = k ^ 2 - i ^ 2 = n
\end{aligned}
x2≡(k+i)p+1≡(k+i)p(k+i)≡(kp+ip)(k+i)≡(k−i)(k+i)=k2−i2=n
得证。
贴上代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cassert>
#include<ctime>
using LL = long long;
using namespace std;
int t;
LL si, n, p;
struct Complex
{
LL a, b;
Complex operator * (const Complex &t) const
{
Complex res = (Complex){0LL, 0LL};
res.a = (a * t.a % p + b * t.b % p * si % p) % p;
res.b = (a * t.b % p + b * t.a % p) % p;
return res;
}
};
inline LL Pow_Real(LL a, LL b)
{
LL res = 1LL;
while(b)
{
if(b & 1LL) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
inline LL Pow_Complex(Complex a, LL b)
{
Complex res = (Complex){1LL, 0LL};
while(b)
{
if(b & 1LL) res = res * a;
a = a * a;
b >>= 1;
}
return res.a;
}
inline LL Solve(LL n, LL p)
{
if(Pow_Real(n, (p - 1LL) / 2LL) != 1LL) return -1LL;
LL k;
while(true)
{
k = 1LL * rand() * rand() % p;
si = ((k * k % p - n) % p + p) % p;
if(Pow_Real(si, (p - 1LL) / 2LL) != 1LL) break;
}
Complex val = (Complex){k, 1};
return Pow_Complex(val, (p + 1LL) / 2LL);
}
int main()
{
#ifdef FILE
freopen("Input.in", "r", stdin);
#endif
scanf("%d", &t);
while(t--)
{
scanf("%lld%lld", &n, &p), n %= p;
if(n == 0LL) { puts("0"); continue; }
LL ans1 = Solve(n, p), ans2;
if(ans1 == -1LL) { puts("Hola!"); continue; }
else
{
ans2 = p - ans1;
if(ans1 > ans2) swap(ans1, ans2);
printf("%lld %lld\n", ans1, ans2);
continue;
}
}
return 0;
}
2367



被折叠的 条评论
为什么被折叠?



