专栏文章

同余方程-5天从入门到入土

算法·理论参与者 47已保存评论 52

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
52 条
当前快照
1 份
快照标识符
@mhz5sikp
此快照首次捕获于
2025/11/15 01:55
4 个月前
此快照最后确认于
2025/11/29 05:24
3 个月前
查看原文
upd: 应评论区,我自己查了一些资料之后发现原来写的东西确实有些不对的地方,造成的误解请各位海涵,本人蒟蒻,如果对于某个描述还是有疑问的话欢迎继续指出,谢谢各位 ^_^
特别鸣谢 NaVi_Awson\color{red}\mathsf{NaVi\_Awson} 学长 以及 落汐\color{orange}\mathsf{\text{落汐}} 大佬/qq

目录

  • 概念讲解
  • 基础算法-解单个线性同余方程
  • 进阶-解模数互质的线性同余方程组
  • 再进阶-解一般的线性同余方程组
  • 入坑-解未知数在指数位置且模数为质数的同余方程
  • 入土-解未知数在指数位置的一般模数同余方程
(为什么没了呢?因为我不会了TAT)

同步题单戳这里\color{brown}\texttt{同步题单戳这里}

概念

1.带余除法

实际上就是小学学的那种“33 除以 221111”的除法,书面一点的描述就是 a=q×b+ra=q\times b+r 其中 a,b,q,ra,b,q,r 都是整数,aa 叫做被除数,bb 叫做除数 (b0)(b\ne 0)qq 是商,rr 是余数(0r<b0\le r <b),C++中,整型之间的除法是整数除法,设 a=q×b+ra=q\times b+r ,那么 a/b=qa/b=q
本文中的除法都是整数除法

2.同余方程

同余方程就是长成这样的柿子: axc(modb)ax\equiv c\pmod b
这个式子什么意思? 就是字面意思,同余即指余数相同(axmodb=cmodbax\bmod b=c\bmod b

算法

Day 1.Exgcd\mathsf{Exgcd} (扩展欧几里得算法)(求解二元一次不定方程/单个线性同余方程)

不定方程 ax+by=cax+by=c (或者 axc(modb)ax\equiv c\pmod{b}ax=c+(y)×bax=c+(-y)\times b 移项可得不定方程 ax+by=cax+by=c )
我们先看一下简单一点的一个方程 ax+by=gcd(a,b)ax+by=\gcd(a,b)

证明 gcd(a,b)=gcd(b,amodb)\boxed{\gcd(a,b)=\gcd(b,a\bmod b)}
g=gcd(a,b)g=\gcd(a,b)
a=g×k1a=g\times k_1 , b=g×k2b=g\times k_2
amodb=(g×k1)mod(g×k2)=(k1modk2)×ga\bmod b=(g\times k_1)\bmod(g\times k_2)=(k_1\bmod k_2)\times gb=g×k2b=g\times k_2
为了使 gcd(a,b)=gcd(b,amodb)\gcd(a,b)=\gcd(b,a\bmod b)
k1modk2k_1\bmod k_2k2k_2 应该没有公因子
假设 k1modk2k_1\bmod k_2k2k_2 有公因子为 rr
k1modk2=n1×r,k2=n2×rk_1\bmod k_2=n_1\times r ,k_2=n_2\times r
所以 k1=x×k2+n1×r=(x×n2+n1)×rk_1=x\times k_2+n_1\times r=(x\times n_2+n_1)\times r
此时 gcd(k1,k2)=r\gcd(k_1,k_2)=r 不符合 gcd(a,b)=g\gcd(a,b)=g (否则gcd(a,b)=r×g\gcd(a,b)=r\times g)
所以 k1modk2k_1\bmod k_2k2k_2 没有公因子
gcd(a,b)=gcd(b,amodb)\gcd(a,b)=\gcd(b, a\bmod b)
证毕

证明 a,b不全为0时ax+by=gcd(a,b)一定有解\boxed{\texttt{当} a,b\texttt{不全为0时}ax+by=\gcd(a,b)\texttt{一定有解}}
已证:gcd(a,b)=gcd(b,amodb)\gcd(a,b)=\gcd(b,a\bmod b)
所以我们可以得到推论: ax+by=gcd(a,b)ax+by=\gcd(a,b) 可以转化为 bx+(amodb)y=gcd(b,amodb)bx+(a\bmod b)y=\gcd(b,a\bmod b)
所以当 b=0b=0 时(欧几里得算法最后一步)
方程变成 ax=gcd(a,b)=aax=\gcd(a,b)=a
所以一定有一组解 x=1,y=0x=1,y=0
证毕

利用这个方法我们可以递归到边界(b=0b=0),然后将 xx 设为 11yy 设为 00
然后在递归返回的时候更新 xxyy 的值
由于方程 ax+by=gcd(a,b)ax+by=\gcd(a,b) 转化为了方程 bx+(amodb)y=gcd(b,amodb)bx+(a\bmod b)y=\gcd(b,a\bmod b)
amodb=ab×(a/b)a\bmod b=a-b\times (a/b)除号当然是整除啦
方程左边 bx+(ab×(a/b))y=ay+(x(a/b)×y)bbx+(a-b\times (a/b))y=ay+(x-(a/b)\times y)b
所以转化后新的 x=y\boxed{x=y},新的 y=x(a/b)×y\boxed{y=x-(a/b)\times y}
但是代码实现可以更简单一些,利用传引用在访问下一层递归的时候就交换 xxyy
Code
CPP
int g,x,y;
void Exgcd(int a,int b,int &x,int &y)
{
	if(!b)
	{
		x=1;y=0;g=a;
		return ;
	}
	Exgcd(b,a%b,y,x);//交换
	y-=(a/b)*x;
	return ;
}
那么解决了方程 ax+by=gcd(a,b)ax+by=\gcd(a,b) 之后 ax+by=cax+by=c 也很好解决了
我们解出 ax+by=gcd(a,b)ax+by=\gcd(a,b) 后,如果 cc 不是 gcd(a,b)\gcd(a,b) 的整数倍,那么这个同余方程是没有解的。
如果有解,那么解为 x×(c/gcd(a,b))x\times (c/\gcd(a,b))
通解为 xi=x×(c/gcd(a,b))+(b/gcd(a,b))×kx_i=x\times (c/\gcd(a,b))+(b/\gcd(a,b))\times k (kZk \in \mathbb Z)
利用 Exgcd 可以解决较为基本的不定方程和同余方程问题
时间复杂度 O(logn)O(\log n) (证明可以自己去了解)

Day 2.CRT\mathsf{CRT}(中国剩余定理/孙子定理)(求解模数两两互质的线性同余方程组)

(线性的意思就是一次的方程) 同余方程组长这样: {xr1(modm1)xr2(modm2)xri(modmi)xrn(modmn)\begin{cases} x\equiv r_1\pmod {m_1}\\x\equiv r_2\pmod {m_2} \\ \cdots \\x\equiv r_i\pmod {m_i}\\ \cdots \\x\equiv r_n\pmod {m_n}\end{cases} 其中 m1,m2,,mnm_1,m_2,\cdots,m_n 两两互质 中国剩余定理:对于如上的线性同余方程组,若其中 m1,m2,,mnm_1,m_2,\cdots,m_n 两两互质 设 M=i=1nmiM=\prod\limits_{i=1}^{n}m_iMi=M/miM_i=M/m_iKiK_iMiKi1(modmi)M_iK_i\equiv 1\pmod {m_i}的解 则对于任意的 r1,r2,,rnr_1,r_2,\cdots,r_n 方程组有整数解 x=i=1nriMiKi\boxed{x=\sum\limits_{i=1}^{n}r_iM_iK_i}

证明中国剩余定理
因为 Mi=M/miM_i=M/m_i 是除了 mim_i 以外的所有的 mm 的倍数(模数两两互质)
所以 ki\forall k \ne i, riMiKi0(modmk)r_iM_iK_i\equiv 0 \pmod {m_k} (都说了是倍数那么右边肯定是 00 了)
又因为 KiK_iMiKi1(modmi)M_iK_i\equiv 1\pmod {m_i}的解,左右同时乘以 rir_i 得到 riMiKiri(modmi)r_iM_iK_i \equiv r_i\pmod {m_i}
所以当 x=i=1nriMiKix=\sum\limits_{i=1}^{n}r_iM_iK_i 时,对于任意的 xri(modmi)x\equiv r_i\pmod {m_i},因为ki\forall k \ne iriMiKi0(modmk)r_iM_iK_i\equiv 0 \pmod {m_k} 而且 riMiKiri(modmi)r_iM_iK_i \equiv r_i\pmod {m_i},所以方程成立。
即方程组的解为 x=i=1nriMiKix=\sum\limits_{i=1}^{n}r_iM_iK_i
证毕

当题目保证给出的模数两两互质的时候,CRT it is!\mathsf{CRT\text{ }it\text{ }is!}
步骤:
  1. 求出M=i=1nmiM=\prod\limits_{i=1}^{n}m_i
  2. 利用 Exgcd 求出 MiKi1(modmi)M_iK_i\equiv 1\pmod {m_i}的解 KiK_i
  3. 逐步累加 riMiKir_iM_iK_i (即利用 x=i=1nriMiKix=\sum\limits_{i=1}^{n}r_iM_iK_i)得到解(MM 意义下)
Code:
CPP
typedef long long LL;
LL CRT()
{
	LL pro=1;
	for(LL i=1;i<=N;i++)
		pro*=m[i];
	LL x,y,Mi;LL ans=0;
	for(LL i=1;i<=N;i++)
	{
		Mi=pro/m[i];
		Exgcd(Mi,m[i],x,y);//x即为求解的K_i
		ans=(ans+Mi*r[i]*x)%pro;
	}
	return (ans+pro)%pro;
}

时间复杂度 O(nlogn)O(n\log n)

Day 3.ExCRT\mathsf{ExCRT}(扩展中国剩余定理/孙子定理)(求解一般的线性同余方程组)

ExCRT\mathsf{ExCRT} 实际上 superset\mathsf{superset}CRT.  By connect\mathsf{CRT.}\space\space\mathcal{By\space connect}
不是所有的方程组模数都两两互质(正如不是所有牛奶...算了) 那么现在所有的 mm 之间都没有什么关系了
这样的话方程 MiKi1(modmi)M_iK_i\equiv 1\pmod {m_i} 不一定有解,无法通过 x=i=1nriMiKix=\sum\limits_{i=1}^{n}r_iM_iK_i 得到解
好的那么现在你面对的是?
{xr1(modm1)xr2(modm2)xri(modmi)xrn(modmn)\begin{cases} x\equiv r_1\pmod {m_1}\\x\equiv r_2\pmod {m_2} \\ \cdots \\x\equiv r_i\pmod {m_i}\\ \cdots \\x\equiv r_n\pmod {m_n}\end{cases}
Q:现在你会什么? A:解一个方程
假设我们解出来了第一个方程,将其解设为 ansans ,我们知道第一个方程的通解 xv=ans+k×m1,kZx_v=ans+k\times m_1,k\in \mathbb{Z}
我们现在的目的就是要求一个同时满足第一第二两个方程的 xx ,那么我们把第一个方程的通解代入第二个方程
ans+k×m1r2(modm2)ans+k\times m_1\equiv r_2\pmod {m_2} 移项得k×m1r2ans(modm2)k\times m_1\equiv r_2-ans\pmod {m_2}
即求解不定方程 m1×k+m2×y=r2ansm_1\times k+m_2\times y=r_2-ans
解出来 kk 之后得到第一第二两个方程的公共解 ans=ans+k×lcm(m1,m2)ans^\prime=ans+k\times \text{lcm}(m_1,m_2)
那么前两个同余方程等价为: xans(modlcm(m1,m2))x\equiv ans^\prime\pmod {\text{lcm}(m_1,m_2)}
为什么等价? 因为向方程中代入 xv=ans+k×lcm(m1,m2),kZx_v=ans^\prime+k\times \text{lcm}(m_1,m_2 ),k\in \mathbb{Z}
你会发现这两个方程和上面的等价方程同时成立,所以这里可以等价
那么你就会发现一件事情,你成功地把两个方程整成了一个方程
那么下一步,继续从方程组里面拿一个方程出来,这时候你手上又有了两个方程
{xans(modlcm)xri(modmi)\begin{cases} x\equiv ans\pmod{\text{lcm}}\\x\equiv r_i\pmod{m_i} \end{cases} 由于第一个方程的通解是 xv=ans+k×lcm,kZx_v=ans+k\times \text{lcm},k\in \mathbb{Z}
所以我们就是要求一个满足条件的 xvx_v 使 xvri(modmi)x_v\equiv r_i\pmod{m_i} 成立
所以就是求解 ans+k×lcmri(modmi)ans+k\times \text{lcm}\equiv r_i\pmod{m_i}
变形得 k×lcmrians(modmi)k\times \text{lcm}\equiv r_i-ans\pmod{m_i}
即求解不定方程 lcm×k+mi×y=rians\text{lcm}\times k+m_i\times y=r_i-ans
如果方程无解,则同余方程组无解
方程有解,那么解 ans=ans+k×lcmans^\prime=ans+k\times \text{lcm}
循环求解即可。
形式化描述过程就是:
ansans 为前 i1i-1 个同余方程的公共解,lcm\text{lcm} 是前 i1i-1 个方程模数的最小公倍数
那么对于第 ii 个方程我们求解不定方程 lcm×k+mi×y=rians\text{lcm}\times k+m_i\times y=r_i-ans
不定方程无解则同余方程组无解
得到 kk 后我们知道了前 ii 个同余方程的公共解 ans=ans+lcm×kans^\prime =ans+\text{lcm}\times k
ii 个方程模数的最小公倍数 lcm=lcm×(mi/gcd(lcm,mi))\text{lcm}^\prime =\text{lcm}\times(m_i/\gcd(\text{lcm},m_i))
此时可求解第 i+1i+1 个方程,循环即可。
Code\mathsf{Code}
CPP
inline LL ExCRT()
{
    LL ans=0,lcm=1;
    LL k1,k2,c;
    for(int i=1;i<=N;i++)
    {
        Exgcd(lcm,m[i],k1,k2);
        c=((r[i]-ans)%m[i]+m[i])%m[i]; // c=r2-r1(顺便调整为正的)
        if(c%g) return -1; //无解
        k1=((k1*c/g)%(m[i]/g)+(m[i]/g))%(m[i]/g);
        ans=ans+k1*lcm;
        lcm=lcm*m[i]/g; //lcm=lcm(lcm,m_i)
        ans=(ans%lcm+lcm)%lcm;		
    }
    return (ans%lcm+lcm)%lcm;
}
时间复杂度 O(nlogn)O(n\log n) (常数比 CRT\mathsf{CRT} 大一点)

Day 4.BSGS\mathsf{BSGS}(BabyStepGiantStep\mathsf{Baby Step Giant Step})(解未知数在指数位置且模数为质数的同余方程)

有时候事情并不会都是那么简单,经过之前几个小节内容的学习你应该已经可以熟练地解决线性同余方程的相关问题 但是啊 AxB(modC)A^x\equiv B\pmod{C}
其中 AA,BB,CC 已知 且 A,CA,C 互质,需要求解最小的 xx 这个该怎么办呢?
BSGS it is!\mathsf{BSGS\text{ }it\text{ }is!}
首先我们想一下对于这个问题暴力怎么搞
当然直接枚举指数啊qwq
但是这样枚举的尽头是什么呢?
首先我们知道 A01(modC)A^0\equiv 1\pmod{C} ,然后又由费马小定理(不知道这个的同学可以先去了解一下逆元再了解费马小定理)
AC11(modC)A^{C-1}\equiv 1\pmod{C}
所以我们发现实际上枚举指数的时候枚举 CC 个就会开始重复,那么暴力枚举 [0,C1)\left[0, C-1\right) 即可。
CPP
int a,m,r;
scanf("%d%d%d",&a,&m,&r);
int base=1;
for(int i=0;i<m-1;i++)
{
	if(base==r) {printf("%d",i);return 0;}
	base=(base*a)%m;
}
printf("no solution");
return 0;
但是假如现在 C=19260817C=19260817 但是只给你 200ms200ms 这个暴力算法是会超时的(而且题目要是想卡暴力质数可以出的很大)
那么我们要对暴力算法进行改进,核心肯定是缩小枚举的范围
这就是 BSGS\mathsf{BSGS} 的雏形了
x=i×Cjx=i\times \sqrt{C}-j
对于同余方程 Ai×CjB(modC)A^{i\times \sqrt{C}-j}\equiv B\pmod{C}
左右两边同时除以 BBAi×CAj×B1(modC)\dfrac{A^{i\times \sqrt{C}}}{A^j\times B}\equiv 1\pmod{C}
预处理分母,然后当模 CC 意义下有 Ai×C=Aj×BA^{i\times \sqrt{C}}=A^j\times BAi×CAj×B=1\dfrac{A^{i \times \sqrt{C}}}{A^j\times B}=1, 方程有解 i×Cji\times \sqrt{C}-j
分母怎么预处理呢?
因为我们的解形式是 i×Cji\times \sqrt{C}-j ,所以 j[0,C)j\in \left[0, \sqrt{C}\right)
预处理出 (Aj×B)modC,j[0,C)(A^j\times B)\bmod C,j\in\left[0, \sqrt{C}\right) ,将其幂指数存在哈希表里面
暴力枚举 i,i[1,C]i,i\in\left[ 1,\sqrt{C}\right] ,对于每一个 Ai×CA^{i\times \sqrt{C}} 在哈希表里面查询是否有对应的 Aj×BA^j\times B
若存在那么最小解就是 i×C哈希表中存储的幂指数\boxed{i\times \sqrt{C}-\small\texttt{哈希表中存储的幂指数}}
那么现在是不是能理解 BabyStepGiantStep\mathsf{BabyStepGiantStep} 这个名字了呢?
我们在预处理哈希表的时候就是在走 BabyStep\mathsf{BabyStep} (一次加 11)
枚举 i×Ci\times\sqrt{C} 中的 ii 时就是在走 GiantStep\mathsf{GiantStep} (一次加 C\sqrt{C} )
Code\mathsf{Code}
CPP
LL BSGS()
{
	Hash.clear();
	//很多选手会使用map但是手写Hash的效率比map高了好几倍
	LL D=B%C,sqrtm=ceil(sqrt(C));
	for(int i=0;i<sqrtm;i++)
	{
        //预处理幂值,建立幂值到幂指数的映射
		Hash.insert(D,i);
		D=D*A%C;
	}
	LL t=ti=fast_pow(A,sqrtm),ans;
    //快速幂求出A^sqrtm
	for(int i=1;i<=sqrtm;i++)
	{
		if((ans=Hash.find(ti))!=-1)
         //如果找到了
			return i*sqrtm-ans;
		ti=ti*t%C;
	}
	return -1;
}
时间复杂度 O(C)O(\sqrt{C}) (快速幂的 logn\log nC\sqrt{C} 下忽略不计)

Day5.ExBSGS\mathsf{ExBSGS}(ExtendedBabyStepGiantStep\mathsf{ExtendedBabyStepGiantStep})(解未知数在指数位置的一般模数同余方程)

就像我们在 ExCRT\mathsf{ExCRT} 的时候说过的一样,不是所有的模数都那么美好让 A,CA,C 互质
不互质的时候分式 Ai×CAj×B\dfrac{A^{i \times \sqrt{C}}}{A^j\times B} 会因为 AjA^j 没得逆元导致无意义
我们只会互质的解法,怎么办?
没有条件创造条件!
对于同余方程 AxB(modC),A,CA^x\equiv B\pmod{C},A,C 不互质
那么我们让他互质!
g=gcd(A,C)g=\gcd(A,C) ,我们需要对方程执行消去因子
A=a×g,C=c×gA=a\times g,C=c\times g 我们必须要把方程中的幂底数和模数化为互质的,如果途中发现 Bmodg0B\bmod g\ne 0 那么这个方程无解

证明 A=a×g,C=c×g,g1.Bmodg0,方程无解\boxed {\texttt{若}A=a\times g,C=c\times g,g\ne 1.\texttt{而}B\bmod g\ne0,\texttt{方程无解}}
D=Ax1D=A^{x-1} 即有同余方程 A×DB(modC)A\times D\equiv B\pmod{C}
化为不定方程 ax+by=cax+by=c 形式:A×D+C×y=BA\times D+C\times y=B
在学习 Exgcd\mathsf{Exgcd} 的时候我们就已经知道了 上述方程有解的充分必要条件就是 BB 可以被 gcd(A,C)\gcd(A,C) 也就是 gg 整除,而现在 Bmodg0B\bmod g\ne 0 所以方程无解
证毕

为了使模数互质我们执行消去因子达到 gcd(A,C)=1\gcd(A,C)=1 为止
依据 BSGS\mathsf{BSGS} 的经验我们再来
对于方程 AxB(modC)A^x\equiv B\pmod{C}
A=a×g,C=c×g,g1A=a\times g,C=c\times g,g\ne 1 ,全除以 gg 得到
a×(a×g)x1B/g(modC)a\times (a\times g)^{x-1}\equiv B/g\pmod{C} 假如 gcd(A,c)1\gcd(A,c)\ne 1 继续消去因子
最后可以得到这样一个柿子 : D×AxcntB(modC)D\times A^{x-cnt}\equiv B^\prime \pmod{C^\prime}
其中的 DD 是每消去一次因子左边的值除掉一个 gg 后得到的商的乘积 (即 D=i=1cnt(A/gi),giD=\prod\limits_{i=1}^{cnt}(A/g_i), g_i为第 ii 次消去因子时的 gcd(A,C)\gcd(A,C) )
x=i×Cj+cntx=i\times\sqrt{C^\prime}-j+cnt ,两边同时除以 BB^\primeD×Ai×CAj×B1(modC)\dfrac{D\times A^{i\times \sqrt{C^\prime}}}{A^j\times B^\prime}\equiv 1\pmod{C^\prime}
现在 A,CA,C^\prime 互质了,套上 BSGS\mathsf{BSGS} 求解出对应的 i×Cji\times\sqrt{C^\prime}-j 然后加上 cntcnt 即可
但是这样得到的 x>cntx>cnt 我们需要对小于 cntcnt 的解特判,消去因子的时候做即可
Code\mathsf{Code}
CPP
inline LL BSGS()
{
	LL sqrtm=ceil(sqrt(C));
	H.clear();//手写Hash很香的qwq
	LL t=B%C;
	for(int i=0;i<sqrtm;i++)
	{
		H.insert(t,i);
		t=t*A%C;
	}
	t=fast_pow(A,sqrtm,C);
	LL ti=D*t%C,ans;//初始化成D*A^(i*sqrtm)
	for(int i=1;i<=sqrtm;i++)
	{
		if((ans=H.find(ti))!=-1&&i*sqrtm-ans!=0)
			return i*sqrtm-ans;
		ti=ti*t%C;
	}
	return -1;
}
inline LL ExBSGS()
{
	LL cnt=0;D=1;
	if(B==1) return 0;
	while((g=gcd(A,C))!=1)//消去因子
	{
		if(B%g) return -1;
		B/=g;C/=g;
		D=(D*A/g)%C;
		cnt++;
		if(D==B) return cnt;
        //每消去一次g,D就会乘以A/g 在模C意义下出现D==B时消去了cnt次
        //所以解为cnt
	}
	LL ans=BSGS();
	if(ans==-1) return -1;
	else return ans+cnt;
}
时间复杂度 O(C)O(\sqrt{C}) (消去因子部分复杂度是 log\log 级别,忽略)

Congratulations!Learning Completed\huge\mathsf{Congratulations!Learning\text{ }Completed}
如果还有任何对算法不理解的问题珂以私信窝,力所能及的范围内窝一定尽力解答 ^_^

评论

52 条评论,欢迎与作者交流。

正在加载评论...