二次剩余
定义
我们定义一个数 在模 意义下是二次剩余,当且仅当 。
例如 时有 个数是二次剩余,分别为 。
有一个基本的观察,这个同余方程只会有两个解 ,并且 ,证明是平凡的。
Legendre 符号
定义 Legendre 符号表示:
下面是 Wikipedia 上给出的 Legendre 符号表:

Euler 准则
定理(Euler's Criterion):对于奇素数 和一个整数 ,有:
也就是说,若 ,那么 是一个模 意义下的二次剩余。
证明
若 不互质,则必有 ,进而 。故 符合 Euler 准则。故下面只考虑 的情况。
首先根据费马小定理,有 ,故 。因此 。
因此只需要证明 是 为模 意义下的二次剩余的充要条件即可。
必要性:
若 为模 意义下的二次剩余,则任取一个 满足 。代入并应用费马小定理,得到 ,证毕。
充分性:
根据原根存在定理,可取模 意义下的一原根 ,则一定存在 的以一个离散对数 ,即 。
代入,得到 。
根据费马小定理,得到 。那么 ,也就是 为偶数。
那么取 ,则可得到 。故 是模 意义下的一个二次剩余,证毕。
Cipolla 算法
Cipolla 算法用于找到同余方程 的一组解,其中 为奇素数。
先利用 Euler 准则判定是否存在解。故下面只讨论有解的情况。
我们任意选择一个 ,使得 非二次剩余(可以用 Euler 准则判定)。
那么令 。那么有 (为什么呢?详见下文【正确性证明】),故 是一组解。
注意到一定是不存在整数 满足条件。不妨扩充数域(可以类比复数域),定义这个数域中的数 形如 ,其中 。并且类比复数定义乘法即可。
于是你发现这个解虚部总是 ,于是便得到了一个解。
正确性证明
容易发现只需要证明 ,即可证明得到的解虚部总为 (因为 是模 意义下的二次剩余),并且是一组合法的解。
注意到 。
然后我们需要知道一个引理:
证明:考虑应用二项式定理:
注意到若 ,则 ,故这些项在模 意义下都为 ,证毕。
于是 。由于费马小定理,有 。
对于 ,有 。
根据 Euler 准则,有 ,所以 。
因此我们得到 。
所以 ,证毕。
时间复杂度证明
在算法流程中有一行:
我们任意选择一个 ,使得 非二次剩余(可以用 Euler 准则判定)。
那么需要证明 非二次剩余的概率。
首先给出一个引理:
对于奇素数 ,模 意义下的二次剩余共有 个。
证明非常简单。由于形如 的解有 个,并且对于不同的 (),不会有公共解。所以只需要取遍 ,每两个取出的数会构成一个有解同余方程的所有解,所以一共有 个有解方程,证毕。
那么任意取一个数 ,它为模 意义下的(非)二次剩余的概率为 。
因此,可以近似看成每一次选取 , 非二次剩余的概率为 ,因此期望 次就可以找到合适的 。
所以时间复杂度为 ,瓶颈在快速幂。
代码实现
下面给出模板题的实现:
#include <bits/stdc++.h>
using namespace std;
struct cipolla_complex {
int x, y, us, mod;
cipolla_complex(int _x, int _y, int _us, int _mod): x(_x), y(_y), us(_us), mod(_mod) {}
cipolla_complex operator*(const cipolla_complex &rhs){
return cipolla_complex(
(1ll * x * rhs.x + (1ll * y * rhs.y % mod) * us) % mod,
(1ll * x * rhs.y + 1ll * y * rhs.x) % mod,
us, mod
);
}
};
cipolla_complex fastpow(cipolla_complex a, int n){
cipolla_complex res = {1, 0, a.us, a.mod};
while(n){
if(n & 1) res = res * a;
a = a * a, n >>= 1;
}
return res;
}
int fastpow(int x, int y, int mod){
int ret = 1;
while(y){
if(y & 1) ret = 1ll * ret * x % mod;
x = 1ll * x * x % mod, y >>= 1;
}
return ret;
}
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
int cipolla(int n, int mod){
if(fastpow(n, (mod - 1) >> 1, mod) != 1) return -1;
int a, us;
do {
a = rnd() % mod, us = ((1ll * a * a + mod) - n) % mod;
} while(fastpow(us, (mod - 1) >> 1, mod) != mod - 1);
return fastpow(cipolla_complex(a, 1, us, mod), (mod + 1) >> 1).x;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t; cin >> t;
while(t--){
int n, mod; cin >> n >> mod;
if(n == 0) {cout << 0 << '\n'; continue; }
int ans = cipolla(n, mod);
if(!(~ans)) cout << "Hola!" << '\n';
else cout << min(ans, mod - ans) << ' ' << max(ans, mod - ans) << '\n';
}
return 0;
}
// Written by xiezheyuan