【数论】中国剩余定理(CRT) 扩展中国剩余定理(EXCRT)
文章目录
- 预备知识
- 一、线性同余方程
- 二、中国剩余定理(CRT)
- 1. 求解过程
- 2. 示例
- 3.【模板】中国剩余定理 ⭐⭐⭐
- 三、扩展中国剩余定理(EXCRT)
- 1. 求解过程
- 2. 示例
- 3.【模板】扩展中国剩余定理 ⭐⭐⭐⭐
- 四、练习
- 1. 猜数字 ⭐⭐⭐
- (1) 解题思路
- (2) 代码实现
- 2. 屠龙勇士 ⭐⭐⭐⭐⭐
- (1) 解题思路
- (2) 代码实现
预备知识
- 乘法逆元
一、线性同余方程
南北朝时期《孙子算经》中有个问题:有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?
用数学公式翻译过来就是一个方程组:
{ x ≡ 2 ( m o d 3 ) x ≡ 3 ( m o d 5 ) x ≡ 2 ( m o d 7 ) \begin{cases} x\equiv 2\pmod 3 \\ \\ x\equiv 3\pmod 5 \\ \\ x\equiv 2\pmod 7 \end{cases} ⎩ ⎨ ⎧x≡2(mod3)x≡3(mod5)x≡2(mod7)
像这样一些一次同余方程构成的方程组叫做线性同余方程组。最常见的线性同余方程组为:
{ x ≡ r 1 ( m o d m 1 ) x ≡ r 2 ( m o d m 2 ) ⋯ x ≡ r n ( m o d m n ) \begin{cases} x\equiv r_1\pmod {m_1} \\ \\ x\equiv r_2\pmod {m_2} \\ \\ \cdots \\ \\ x\equiv r_n\pmod {m_n} \end{cases} ⎩ ⎨ ⎧x≡r1(modm1)x≡r2(modm2)⋯x≡rn(modmn)
如果模数 m 1 , m 2 , ⋯ , m n m_1, m_2, \cdots, m_n m1,m2,⋯,mn 两两互质,我们该如何求解这样一个方程组的通解呢?这就需要用到中国剩余定理了。
二、中国剩余定理(CRT)
1. 求解过程
中国剩余定理(CRT)可以求解线性同余方程,前提条件是这些同余方程的模数两两之间必须互质。CRT 求解同余方程组的过程如下:
设 m 1 , m 2 , ⋯ , m n m_1, m_2, \cdots, m_n m1,m2,⋯,mn 两两互质,对于一个线性同余方程组
{ x ≡ r 1 ( m o d m 1 ) x ≡ r 2 ( m o d m 2 ) ⋯ x ≡ r n ( m o d m n ) \begin{cases} x\equiv r_1\pmod {m_1} \\ \\ x\equiv r_2\pmod {m_2} \\ \\ \cdots \\ \\ x\equiv r_n\pmod {m_n} \end{cases} ⎩ ⎨ ⎧x≡r1(modm1)x≡r2(modm2)⋯x≡rn(modmn)
记 M = m 1 × m 2 × ⋯ × m n M = m_1\times m_2\times\cdots\times m_n M=m1×m2×⋯×mn,构造:
C 1 = r 1 × M m 1 × ( M m 1 ) − 1 C 2 = r 2 × M m 2 × ( M m 2 ) − 1 ⋯ C n = r n × M m n × ( M m n ) − 1 C_1 = r_1\times \frac{M}{m_1}\times (\frac{M}{m_1})^{-1} \\ C_2 = r_2\times \frac{M}{m_2}\times (\frac{M}{m_2})^{-1} \\ \cdots \\ C_n = r_n\times \frac{M}{m_n}\times (\frac{M}{m_n})^{-1} \\ C1=r1×m1M×(m1M)−1C2=r2×m2M×(m2M)−1⋯Cn=rn×mnM×(mnM)−1
其中 ( M m i ) − 1 (\frac{M}{m_i})^{-1} (miM)−1 表示 M m i \frac{M}{m_i} miM 在模 m i m_i mi 意义下的乘法逆元,则通解 x x x 满足
x ≡ ( C 1 + C 2 + ⋯ + C n ) ( m o d M ) x \equiv (C_1 + C_2 + \cdots + C_n)\pmod M x≡(C1+C2+⋯+Cn)(modM)
因此可以发现,该同余方程组的最小正整数解为
( C 1 + C 2 + ⋯ + C n ) m o d M (C_1 + C_2 + \cdots + C_n) \bmod M (C1+C2+⋯+Cn)modM
2. 示例
求解线性同余方程
{ x ≡ 2 ( m o d 3 ) x ≡ 3 ( m o d 5 ) x ≡ 2 ( m o d 7 ) \begin{cases} x\equiv 2\pmod 3 \\ \\ x\equiv 3\pmod 5 \\ \\ x\equiv 2\pmod 7 \end{cases} ⎩ ⎨ ⎧x≡2(mod3)x≡3(mod5)x≡2(mod7)
的最小非负整数解。
由于 3 , 5 , 7 3, 5, 7 3,5,7 三个数两两互质,因此满足中国剩余定理的使用条件。
首先计算出 M = m 1 × m 2 × m 3 = 3 × 5 × 7 = 105 M = m_1\times m_2\times m_3 = 3\times 5\times 7 = 105 M=m1×m2×m3=3×5×7=105,接下来计算
-
M m 1 = 105 3 = 35 \frac{M}{m_1} = \frac{105}{3} = 35 m1M=3105=35;由 35 x ≡ 1 ( m o d 3 ) 35x\equiv 1\pmod 3 35x≡1(mod3),解得 x = 2 x = 2 x=2,因此 ( M m 1 ) − 1 = 2 (\frac{M}{m_1})^{-1} = 2 (m1M)−1=2;
-
M m 2 = 105 5 = 21 \frac{M}{m_2} = \frac{105}{5} = 21 m2M=5105=21;由 21 x ≡ 1 ( m o d 5 ) 21x\equiv 1\pmod 5 21x≡1(mod5),解得 x = 1 x = 1 x=1,因此 ( M m 2 ) − 1 = 1 (\frac{M}{m_2})^{-1} = 1 (m2M)−1=1;
-
M m 3 = 105 7 = 15 \frac{M}{m_3} = \frac{105}{7} = 15 m3M=7105=15;由 15 x ≡ 1 ( m o d 7 ) 15x\equiv 1\pmod 7 15x≡1(mod7),解得 x = 1 x = 1 x=1,因此 ( M m 2 ) − 1 = 1 (\frac{M}{m_2})^{-1} = 1 (m2M)−1=1;
则有
C 1 = 2 × 35 × 2 = 140 C 2 = 3 × 21 × 1 = 63 C 3 = 2 × 15 × 1 = 30 C_1 = 2\times 35\times 2 = 140 \\ C_2 = 3\times 21\times 1 = 63 \\ C_3 = 2\times 15\times 1 = 30 C1=2×35×2=140C2=3×21×1=63C3=2×15×1=30
所以该同余方程组的最小非负整数解为
( 140 + 63 + 30 ) m o d 105 = 23 (140 + 63 + 30) \bmod 105 = 23 (140+63+30)mod105=23
3.【模板】中国剩余定理 ⭐⭐⭐
【题目链接】
P1495 【模板】中国剩余定理(CRT)/ 曹冲养猪 - 洛谷
【题目描述】
自从曹冲搞定了大象以后,曹操就开始捉摸让儿子干些事业,于是派他到中原养猪场养猪,可是曹冲满不高兴,于是在工作中马马虎虎,有一次曹操想知道母猪的数量,于是曹冲想狠狠耍曹操一把。举个例子,假如有 16 16 16 头母猪,如果建了 3 3 3 个猪圈,剩下 1 1 1 头猪就没有地方安家了。如果建造了 5 5 5 个猪圈,但是仍然有 1 1 1 头猪没有地方去,然后如果建造了 7 7 7 个猪圈,还有 2 2 2 头没有地方去。你作为曹总的私人秘书理所当然要将准确的猪数报给曹总,你该怎么办?
【输入格式】
第一行包含一个整数 n n n —— 建立猪圈的次数,接下来 n n n 行,每行两个整数 a i , b i a_i, b_i ai,bi,表示建立了 a i a_i ai 个猪圈,有 b i b_i bi 头猪没有去处。你可以假定 a 1 ∼ a n a_1 \sim a_n a1∼an 互质。
【输出格式】
输出包含一个正整数,即为曹冲至少养母猪的数目。
【示例一】
输入
3 3 1 5 1 7 2输出
16
【说明/提示】
1 ≤ n ≤ 10 1 \leq n\le10 1≤n≤10, 0 ≤ b i < a i ≤ 100000 0 \leq b_i\lt a_i\le100000 0≤bi<ai≤100000, 1 ≤ ∏ a i ≤ 1 0 18 1 \leq \prod a_i \leq 10^{18} 1≤∏ai≤1018
非常明显的一个用中国剩余定理求解的问题。
- 计算所有模数的乘积 M M M;
- 计算 c i = M m i c_i = \frac{M}{m_i} ci=miM;
- 计算 c i c_i ci 在模 m i m_i mi 意义下的乘法逆元 c i − 1 c_i^{-1} ci−1,可利用扩展欧几里得算法求解;
- 最终结果为 x = ∑ i = 1 n r i c i c i − 1 m o d M x = \sum\limits_{i = 1}^{n}r_ic_ic_i^{-1}\bmod M x=i=1∑nricici−1modM。
注意第四步在相乘的过程中,为了防止溢出,我们通常会采用龟速乘的方式计算。
#include<iostream>using namespace std;typedef long long LL;const int N = 1e5 + 10;
int m[N], r[N]; // 存储模数和余数// 龟速乘
LL qmul(LL a, LL b, LL p)
{LL ret = 0;while(b){if(b & 1) ret = (ret + a) % p;b >>= 1;a = (a + a) % p;}return ret;
}// 扩展欧几里得
void exgcd(LL a, LL b, LL& x, LL& y)
{if(b == 0){x = 1, y = 0;return;}LL x1, y1;exgcd(b, a % b, x1, y1);x = y1;y = x1 - a / b * y1;
}// 中国剩余定理
LL crt(int n)
{// 累乘求 MLL M = 1;for(int i = 1; i <= n; i++) M *= m[i];LL ret = 0;for(int i = 1; i <= n; i++){LL c = M / m[i];LL x, y;exgcd(c, m[i], x, y); // 扩展欧几里得求逆元x = (x % m[i] + m[i]) % m[i]; // 把 x 补成最小正整数// x * c * r[i]ret = (ret + qmul(qmul(x, c, M), r[i], M)) % M;}return ret;
}int main()
{int n;cin >> n;for(int i = 1; i <= n; i++) cin >> m[i] >> r[i];cout << crt(n) << endl;return 0;
}
三、扩展中国剩余定理(EXCRT)
1. 求解过程
前面我们了解到中国剩余定理(CRT)的使用条件是线性同余方程组的模数必须两两互质,那么如果模数不互质,CRT 就不适用了,此时想要求解模数不两两互质的线性同余方程组,就可以使用扩展中国剩余定理。
扩展中国剩余定理的核心思想:迭代。它的求解过程如下:
对于一个线性同余方程组
{ x ≡ r 1 ( m o d m 1 ) x ≡ r 2 ( m o d m 2 ) ⋯ x ≡ r n ( m o d m n ) \begin{cases} x\equiv r_1\pmod {m_1} \\ \\ x\equiv r_2\pmod {m_2} \\ \\ \cdots \\ \\ x\equiv r_n\pmod {m_n} \end{cases} ⎩ ⎨ ⎧x≡r1(modm1)x≡r2(modm2)⋯x≡rn(modmn)
我们首先补充一个方程: x ≡ r 0 ( m o d m 0 ) x\equiv r_0\pmod {m_0} x≡r0(modm0),其中 r 0 = 0 r_0 = 0 r0=0, m 0 = 1 m_0 = 1 m0=1。显然这对于任意整数都成立,所以我们把它添加进这个同余方程组中并不会影响最终结果。假设最终的通解为 r e t = x × m + r ret = x\times m + r ret=x×m+r,初始 r = 0 r = 0 r=0, m = 1 m = 1 m=1。然后通过迭代后面的方程,使得最终 r e t ret ret 依次满足后续的所有方程。
假设当前取得方程为 x ≡ r i ( m o d m i ) x\equiv r_i\pmod{m_i} x≡ri(modmi),即现在我们要让 r e t ret ret 满足 r e t = y × m i + r i ret = y\times m_i + r_i ret=y×mi+ri 。
现在 r e t ret ret 既满足 r e t = x × m + r ret = x\times m + r ret=x×m+r,又满足 r e t = y × m i + r i ret = y\times m_i + r_i ret=y×mi+ri,所以有
x × m + r = y × m i + r i x\times m + r = y\times m_i + r_i x×m+r=y×mi+ri
于是有
x × m − y × m i = r i − r x\times m-y\times m_i = r_i - r x×m−y×mi=ri−r
这样得形式类似于 a x + b y = c ax + by = c ax+by=c,所以我们可以根据扩展欧几里得算法求出特解 x 0 x_0 x0 和 y 0 y_0 y0,如果无解,则该线性同余方程组无解。设 d = gcd ( a , b ) d = \gcd(a, b) d=gcd(a,b),根据 exgcd,通解为
x = x 0 + b d × k x = x_0 + \frac{b}{d}\times k x=x0+db×k
带入 r e t = x × m + r ret = x\times m + r ret=x×m+r 中得
r e t = x 0 × m + b d × k × m + r ret = x_0 \times m + \frac{b}{d}\times k \times m + r ret=x0×m+db×k×m+r
整理得到
r e t = k × b d × m + x 0 × m + r ret = k\times\frac{b}{d}\times m + x_0\times m + r ret=k×db×m+x0×m+r
这样的 r e t ret ret,既满足之前的方程,也满足当前的方程。并且这样的 r e t ret ret 又可以看作 r e t = x × m + r ret = x\times m + r ret=x×m+r,其中
m ← m × b d r ← x 0 × m + r m \leftarrow m \times\frac{b}{d} \\r\leftarrow x_0\times m + r m←m×dbr←x0×m+r
接下来我们只需要再选取一个新的方程继续迭代出一个新的 r e t ret ret,这样不断迭代,就可以让 r e t ret ret 满足方程组中的所有方程,也就得到了最终解。
2. 示例
求解线性同余方程
{ x ≡ 2 ( m o d 3 ) x ≡ 3 ( m o d 5 ) x ≡ 2 ( m o d 7 ) \begin{cases} x\equiv 2\pmod 3 \\ \\ x\equiv 3\pmod 5 \\ \\ x\equiv 2\pmod 7 \end{cases} ⎩ ⎨ ⎧x≡2(mod3)x≡3(mod5)x≡2(mod7)
的最小非负整数解。
这里虽然模数两两互质,但是依然可以使用扩展中国剩余定理来求解,我们重点掌握其思想。
首先补充一个方程 x ≡ 0 ( m o d 1 ) x \equiv 0\pmod 1 x≡0(mod1),最终解 r e t = x × 1 + 0 ret = x\times 1 + 0 ret=x×1+0;
迭代第一个方程 x ≡ 2 ( m o d 3 ) x\equiv 2\pmod 3 x≡2(mod3) 得到: r e t = y × 3 + 2 ret = y \times3 + 2 ret=y×3+2;
于是有
r e t = x × 1 + 0 = y × 3 + 2 ret = x\times 1 + 0 = y\times 3 + 2 ret=x×1+0=y×3+2
即
x − 3 y = 2 x - 3y = 2 x−3y=2
可以看作 x + 3 y = 2 x + 3y = 2 x+3y=2,之后利用扩展欧几里得求出
x = 2 + 3 × k y = 0 − k x = 2 + 3 \times k\\ y = 0 - k x=2+3×ky=0−k
带入 r e t = x × 1 + 0 ret = x\times 1 + 0 ret=x×1+0 中得
r e t = k × 3 + 2 ⇓ r e t = x × 3 + 2 ret = k\times 3 + 2 \\ \Downarrow \\ ret = x\times 3 + 2 ret=k×3+2⇓ret=x×3+2
此时 r e t ret ret 满足第一个方程。继续迭代第二个方程 x ≡ 3 ( m o d 5 ) x\equiv 3\pmod 5 x≡3(mod5),有
r e t = y × 5 + 3 ret = y\times 5 + 3 ret=y×5+3
于是
r e t = x × 3 + 2 = y × 5 + 3 ret = x\times 3 + 2 = y\times 5 + 3 ret=x×3+2=y×5+3
即
3 x + 5 y = 1 3x + 5y = 1 3x+5y=1
利用扩展欧几里得求出
x = 2 + 5 × k y = − 1 − 3 × k x = 2 + 5\times k \\ y = -1 - 3\times k x=2+5×ky=−1−3×k
带入 r e t = x × 3 + 2 ret = x\times 3 + 2 ret=x×3+2 中得
r e t = k × 15 + 8 ⇓ r e t = x × 15 + 8 ret = k\times 15 + 8 \\ \Downarrow \\ ret = x\times 15 + 8 ret=k×15+8⇓ret=x×15+8
此时 r e t ret ret 满足第一、二个方程。继续迭代第三个方程 x ≡ 2 ( m o d 7 ) x\equiv 2\pmod 7 x≡2(mod7),有
r e t = y × 7 + 2 ret = y\times 7 + 2 ret=y×7+2
于是
r e t = x × 15 + 8 = y × 7 + 2 ret = x\times 15 + 8 = y\times 7 + 2 ret=x×15+8=y×7+2
即
15 x + 7 y = − 6 15x + 7y = -6 15x+7y=−6
利用扩展欧几里得算法求出
x = − 6 + 7 × k y = 12 − 15 × k x = -6 + 7\times k\\ y = 12 - 15\times k x=−6+7×ky=12−15×k
带入 r e t = x × 15 + 8 ret = x\times 15 + 8 ret=x×15+8 中得
r e t = k × 105 − 82 = k × 105 + 23 ret = k\times 105 - 82 = k\times 105 + 23 ret=k×105−82=k×105+23
此时 r e t ret ret 满足所有方程,并且 23 23 23 为最小非负整数解。
3.【模板】扩展中国剩余定理 ⭐⭐⭐⭐
【题目链接】
P4777 【模板】扩展中国剩余定理(EXCRT) - 洛谷
【题目描述】
给定 n n n 组非负整数 a i , b i a_i, b_i ai,bi ,求解关于 x x x 的方程组的最小非负整数解。
{ x ≡ b 1 ( m o d a 1 ) x ≡ b 2 ( m o d a 2 ) … x ≡ b n ( m o d a n ) \begin{cases}x\equiv b_1\pmod{a_1}\\x\equiv b_2\pmod{a_2}\\\dots\\x\equiv b_n\pmod{a_n}\end{cases} ⎩ ⎨ ⎧x≡b1(moda1)x≡b2(moda2)…x≡bn(modan)
【输入格式】
输入第一行包含整数 n n n。
接下来 n n n 行,每行两个非负整数 a i , b i a_i, b_i ai,bi。
【输出格式】
输出一行,为满足条件的最小非负整数 x x x。
【示例一】
输入
3 11 6 25 9 33 17输出
809
【说明/提示】
对于 100 % 100 \% 100% 的数据, 1 ≤ n ≤ 10 5 1 \le n \le {10}^5 1≤n≤105, 1 ≤ a i ≤ 10 12 1 \le a_i \le {10}^{12} 1≤ai≤1012, 0 ≤ b i ≤ 1 0 12 0\leq b_i \leq 10^{12} 0≤bi≤1012,保证所有 a i a_i ai 的最小公倍数不超过 10 18 {10}^{18} 1018。
请注意程序运行过程中进行乘法运算时结果可能有溢出的风险。
数据保证有解。
#include<iostream>using namespace std;typedef long long LL;const int N = 1e5 + 10;LL m[N], r[N]; // 存模数和余数// 龟速乘,防止乘法溢出
LL qmul(LL a, LL b, LL p)
{LL ret = 0;while(b){if(b & 1) ret = (ret + a) % p;b >>= 1;a = (a + a) % p;}return ret;
}// 扩展欧几里得
LL exgcd(LL a, LL b, LL& x, LL& y)
{if(b == 0){x = 1, y = 0;return a;}LL x1, y1, d;d = exgcd(b, a % b, x1, y1);x = y1;y = x1 - a / b * y1;return d;
}LL excrt(int n)
{// 补充的方程初始模数为 1,余数为 0LL M = 1, ret = 0;for(int i = 1; i <= n; i++){LL a = M, b = m[i], c = r[i] - ret;// 构造出 ax + by = c// c 有可能为负数或者很大的数,需要把它补成模 b 下的最小非负整数c = (c % b + b) % b; // 防溢出// 解 ax + by = c 中的 xLL x, y, d;d = exgcd(a, b, x, y);if(c % d) return -1;LL k1 = b / d;// ax + by = c 的特解 x0 = x * (c / d)x = qmul(x, c / d, k1); // 防止直接乘会溢出x = (x % k1 + k1) % k1;// 迭代出下一个方程的系数ret = ret + x * M;M = k1 * M;ret = (ret % M + M) % M; }return ret;
}int main()
{int n; cin >> n;for(int i = 1; i <= n; i++) cin >> m[i] >> r[i];cout << excrt(n) << endl;return 0;
}
四、练习
1. 猜数字 ⭐⭐⭐
【题目链接】
[P3868 TJOI2009] 猜数字 - 洛谷
【题目描述】
现有两组数字,每组 k k k 个。
第一组中的数字分别用 a 1 , a 2 , ⋯ , a k a_1,a_2,\cdots ,a_k a1,a2,⋯,ak 表示,第二组中的数字分别用 b 1 , b 2 , ⋯ , b k b_1,b_2,\cdots ,b_k b1,b2,⋯,bk 表示。
其中第二组中的数字是两两互素的。求最小的 n ∈ N n\in \mathbb{N} n∈N,满足对于 ∀ i ∈ [ 1 , k ] \forall i\in [1,k] ∀i∈[1,k],有 b i ∣ ( n − a i ) b_i | (n-a_i) bi∣(n−ai)。
【输入格式】
第一行一个整数 k k k。
第二行 k k k 个整数,表示: a 1 , a 2 , ⋯ , a k a_1,a_2,\cdots ,a_k a1,a2,⋯,ak。
第三行 k k k 个整数,表示: b 1 , b 2 , ⋯ , b k b_1,b_2,\cdots ,b_k b1,b2,⋯,bk。
【输出格式】
输出一行一个整数,为所求的答案 n n n。
【示例一】
输入
3 1 2 3 2 3 5输出
23
【说明/提示】
对于 100 % 100\% 100% 的数据:
1 ≤ k ≤ 10 1\le k \le 10 1≤k≤10, ∣ a i ∣ ≤ 1 0 9 |a_i|\le 10^9 ∣ai∣≤109, 1 ≤ b i ≤ 6 × 1 0 3 1\le b_i\le 6\times 10^3 1≤bi≤6×103, ∏ i = 1 k b i ≤ 1 0 18 \prod_{i=1}^k b_i\le 10^{18} ∏i=1kbi≤1018。
每个测试点时限 1 1 1 秒。
注意:对于
C/C++语言,对 64 64 64 位整型数应声明为long long。若使用
scanf,printf函数(以及fscanf,fprintf等),应采用%lld标识符。
(1) 解题思路
中国剩余定理模板题变形,需要注意的是余数可能为负数,可以在读入余数之后处理为正数,也可以在龟速乘的时候避免将负数传入参数 b b b 中。
(2) 代码实现
#include<iostream>
using namespace std;typedef long long LL;const int N = 20;
LL m[N], r[N];LL qmul(LL a, LL b, LL p)
{LL ret = 0;while (b) {if (b & 1) ret = (ret + a) % p;b >>= 1;a = (a + a) % p;}return ret;
}void exgcd(LL a, LL b, LL& x, LL& y)
{if (b == 0) {x = 1, y = 0;return;}LL x1, y1;exgcd(b, a % b, x1, y1);x = y1;y = x1 - a / b * y1;
}LL crt(int n)
{LL M = 1;for (int i = 1; i <= n; i++) M *= m[i];LL ret = 0;for (int i = 1; i <= n; i++) {LL c = M / m[i];LL x, y;exgcd(c, m[i], x, y);x = (x % m[i] + m[i]) % m[i];// 龟速乘时避免将负数传给参数 b,否则可能死循环ret = (ret + qmul(r[i], qmul(c, x, M), M)) % M;}return ret;
}int main()
{LL k;scanf("%lld", &k);for (int i = 1; i <= k; i++) scanf("%lld", r + i);for (int i = 1; i <= k; i++) scanf("%lld", m + i);printf("%lld", crt(k));return 0;
}
2. 屠龙勇士 ⭐⭐⭐⭐⭐
【题目链接】
[P4774 NOI2018] 屠龙勇士 - 洛谷
【题目描述】
小 D 最近在网上发现了一款小游戏。游戏的规则如下:
- 游戏的目标是按照编号 1 → n 1 \rightarrow n 1→n 顺序杀掉 n n n 条巨龙,每条巨龙拥有一个初始的生命值 a i a_i ai 。同时每条巨龙拥有恢复能力,当其使用恢复能力时,它的生命值就会每次增加 p i p_i pi ,直至生命值非负。只有在攻击结束后且当生命值 恰好 为 0 0 0 时它才会死去。
- 游戏开始时玩家拥有 m m m 把攻击力已知的剑,每次面对巨龙时,玩家只能选择一把剑,当杀死巨龙后这把剑就会消失,但作为奖励,玩家会获得全新的一把剑。
小 D 觉得这款游戏十分无聊,但最快通关的玩家可以获得 ION2018 的参赛资格,于是小 D 决定写一个笨笨的机器人帮她通关这款游戏,她写的机器人遵循以下规则:
- 每次面对巨龙时,机器人会选择当前拥有的,攻击力不高于巨龙初始生命值中攻击力最大的一把剑作为武器。如果没有这样的剑,则选择 攻击力最低 的一把剑作为武器。
- 机器人面对每条巨龙,它都会使用上一步中选择的剑攻击巨龙固定的 x x x 次,使巨龙的生命值减少 x × A T K x \times ATK x×ATK 。
- 之后,巨龙会不断使用恢复能力,每次恢复 p i p_i pi 生命值。若在使用恢复能力前或某一次恢复后其生命值为 0 0 0 ,则巨龙死亡,玩家通过本关。
那么显然机器人的攻击次数是决定能否最快通关这款游戏的关键。小 D 现在得知了每条巨龙的所有属性,她想考考你,你知道应该将机器人的攻击次数 x x x 设置为多少,才能用最少的攻击次数通关游戏吗?
当然如果无论设置成多少都无法通关游戏,输出 − 1 -1 −1 即可。
【输入格式】
第一行一个整数 T T T,代表数据组数。
接下来 T T T 组数据,每组数据包含 5 5 5 行。
- 每组数据的第一行包含两个整数, n n n 和 m m m ,代表巨龙的数量和初始剑的数量;
- 接下来一行包含 n n n 个正整数,第 i i i 个数表示第 i i i 条巨龙的初始生命值 a i a_i ai;
- 接下来一行包含 n n n 个正整数,第 i i i 个数表示第 i i i 条巨龙的恢复能力 p i p_i pi;
- 接下来一行包含 n n n 个正整数,第 i i i 个数表示杀死第 i i i 条巨龙后奖励的剑的攻击力;
- 接下来一行包含 m m m 个正整数,表示初始拥有的 m m m 把剑的攻击力。
【输出格式】
一共 T T T 行。
第 i i i 行一个整数,表示对于第 i i i 组数据,能够使得机器人通关游戏的最小攻击次数 x x x ,如果答案不存在,输出 − 1 -1 −1。
【示例一】
输入
2 3 3 3 5 7 4 6 10 7 3 9 1 9 1000 3 2 3 5 6 4 8 7 1 1 1 1 1输出
59 -1
【说明】
更多样例
更多样例请在附加文件中下载。
样例 2
见附加文件中的
dragon2.in与dragon2.ans。样例 1 解释
第一组数据:
开始时拥有的剑的攻击力为 { 1 , 9 , 1000 } \{1,9,1000\} {1,9,1000},第 1 1 1 条龙生命值为 3 3 3,故选择攻击力为 1 1 1 的剑,攻击 59 59 59 次,造成 59 59 59 点伤害,此时龙的生命值为 − 56 -56 −56,恢复 14 次后生命值恰好为 0 0 0,死亡。
攻击力为 1 1 1 的剑消失,拾取一把攻击力为 7 7 7 的剑,此时拥有的剑的攻击力为
{ 7 , 9 , 1000 } \{7,9,1000\} {7,9,1000},第 2 条龙生命值为 5 5 5,故选择攻击力为 7 7 7 的剑,攻击 59 59 59 次,造成 413 413 413 点伤害,此时龙的生命值为 − 408 -408 −408,恢复 68 68 68 次后生命值恰好为 0 0 0,死亡。此时拥有的剑的攻击力为 { 3 , 9 , 1000 } \{3,9,1000\} {3,9,1000},第 3 3 3 条龙生命值为 7 7 7,故选择攻击力为 3 3 3 的剑,攻击 59 59 59 次,造成 177 177 177 点伤害,此时龙的生命值为 − 170 -170 −170,恢复 17 17 17 次后生命值恰好为 0,死亡。
没有比 59 59 59 次更少的通关方法,故答案为 59 59 59。
第二组数据:
不存在既能杀死第一条龙又能杀死第二条龙的方法,故无法通关,输出 − 1 -1 −1。
【提示】
你所用到的中间结果可能很大,注意保存中间结果的变量类型。
(1) 解题思路
假设巨龙的血量为 r r r,剑的攻击力为 a a a,巨龙的恢复能力为 m m m,恢复次数为 y y y。因为攻击次数为 x x x,所以当
r − a x + m y = 0 r - ax + my = 0 r−ax+my=0
的时候巨龙死亡。移项得到
a x = m y + r ax = my + r ax=my+r
这不就是一个同余方程变来的嘛!也就是
a x ≡ r ( m o d m ) ax\equiv r\pmod m ax≡r(modm)
那么对于每一条巨龙,我们都可以列出一个同余方程,组合起来就是一个同余方程组:
{ a 1 x ≡ r 1 ( m o d m 1 ) a 2 x ≡ r 2 ( m o d m 2 ) ⋯ a n x ≡ r n ( m o d m n ) \begin{cases} a_1x\equiv r_1\pmod {m_1} \\ \\ a_2x\equiv r_2\pmod {m_2} \\ \\ \cdots \\ \\ a_nx\equiv r_n\pmod {m_n} \end{cases} ⎩ ⎨ ⎧a1x≡r1(modm1)a2x≡r2(modm2)⋯anx≡rn(modmn)
我们要求的最小攻击次数就是该线性同余方程组的最小正整数解,由于 m i m_i mi 之间不一定互质,所以采用扩展中国剩余定理来求解。
和模板题不一样的是,这里 x x x 的前面多了一个系数 a i a_i ai,不过基本的迭代思想依旧是不变的。
我们首先还是补充一个方程: x ≡ r 0 ( m o d m 0 ) x\equiv r_0\pmod {m_0} x≡r0(modm0),其中 r 0 = 0 r_0 = 0 r0=0, m 0 = 1 m_0 = 1 m0=1。
假设最终的通解为 r e t = x ⋅ m + r ret = x\cdot m + r ret=x⋅m+r,初始 r = 0 r = 0 r=0, m = 1 m = 1 m=1。然后开始迭代后面的方程,使得最终 r e t ret ret 依次满足后续的所有方程。
假设当前取得方程为 a i x ≡ r i ( m o d m i ) a_ix\equiv r_i\pmod{m_i} aix≡ri(modmi),即现在我们要让 r e t ret ret 满足
a i ⋅ r e t = y m i + r i a_i \cdot ret = y m_i + r_i ai⋅ret=ymi+ri
我们把 r e t = x × m + r ret = x\times m + r ret=x×m+r 两边同时乘以 a i a_i ai,得到
a i ⋅ r e t = a i x m + a i r a_i\cdot ret = a_i x m + a_i r ai⋅ret=aixm+air
所以有
a i x m + a i r = y m i + r i a_i x m + a_i r = y m_i + r_i aixm+air=ymi+ri
整理得
a i x m − y m i = r i − a i r a_ix m-y m_i = r_i - a_ir aixm−ymi=ri−air
形式类似于 a x + b y = c ax + by = c ax+by=c,可以用扩展欧几里得算法求出特解 x 0 x_0 x0 和 y 0 y_0 y0,如果无解,则该线性同余方程组无解,说明无法通关。
设 d = gcd ( a , b ) d = \gcd(a, b) d=gcd(a,b),根据 exgcd,通解为
x = x 0 + b d k x = x_0 + \frac{b}{d} k x=x0+dbk
带入 r e t = x ⋅ m + r ret = x\cdot m + r ret=x⋅m+r 中得
r e t = b d m k + x 0 m + r ret = \frac{b}{d} mk + x_0 m + r ret=dbmk+x0m+r
这样的 r e t ret ret,既满足之前的方程,也满足当前的方程。并且这样的 r e t ret ret 又可以看作新的 r e t = x × m + r ret = x\times m + r ret=x×m+r,其中
m ← m b d r ← x 0 m + r m \leftarrow m \frac{b}{d} \\r\leftarrow x_0 m + r m←mdbr←x0m+r
接下来我们只需要再选取一个新的方程继续迭代出一个新的 r e t ret ret,这样不断迭代,就可以让 r e t ret ret 满足方程组中的所有方程,也就得到了最终解。
- 细节问题
由于我们打每条龙要使用的剑的要求是 “攻击力不高于巨龙初始生命值中攻击力最大的一把剑作为武器。如果没有这样的剑,则选择 攻击力最低 的一把剑作为武器”,因此需要采用二分取找的,并且杀死每条龙之后要选择另一把剑,所以这里我们采用
multiset去存储剑的信息(攻击力),选择剑的时候调用lower_bound或者upper_bound接口即可。注意打完每条龙之后要erase掉。我们最开始推导的时候列出了一个等式 r − a x + m y = 0 r - ax + my = 0 r−ax+my=0, y y y 表示恢复次数,它必须大于 0 0 0,但是如果像上面这样直接写代码的话 y y y 有可能小于 0 0 0,计算出的 x x x 是不正确的。因此为了确保回复次数 y > 0 y > 0 y>0,我们还需要额外维护一个变量
limit,记录杀死这些龙所需要的最大攻击次数,只有最终的 x x x 大于等于limit,才是正确的结果,其中l i m i t = max ( l i m i t , ⌈ r i a i ⌉ ) limit = \max(limit, \ \lceil\frac{r_i}{a_i}\rceil) limit=max(limit, ⌈airi⌉)
r r r 为巨龙的血量, a a a 为剑的攻击力。
- 涉及乘法运算时,能用龟速乘则用龟速乘,防止溢出
(2) 代码实现
#include<iostream>
#include<set>using namespace std;typedef long long LL;const int N = 1e5 + 10;// 所有攻击次数中最大的那个
LL limit;
// 恢复能力, 生命值, 奖励剑的攻击力
LL mod[N], r[N], atk_rwd[N];
// 存储剑的攻击力
multiset<LL> atk;// 获取攻击力不高于 hp 的攻击力最大的剑, 没有选择攻击力最低的一把
LL get_atk(LL hp)
{auto it = atk.upper_bound(hp);if(it != atk.begin()) --it;return *it;
}// 龟速乘
LL qmul(LL a, LL b, LL p)
{LL ret = 0;while(b){if(b & 1) ret = (ret + a) % p;b >>= 1;a = (a + a) % p;}return ret;
}// 扩展欧几里得
LL exgcd(LL a, LL b, LL& x, LL& y)
{if(b == 0){x = 1, y = 0;return a;}LL x1, y1, d;d = exgcd(b, a % b, x1, y1);x = y1;y = x1 - a / b * y1;return d;
}// 扩展中国剩余定理
LL excrt(int n)
{LL M = 1, ret = 0;for(int i = 1; i <= n; i++){// 获取剑的攻击力LL t = get_atk(r[i]);LL a = qmul(t, M, mod[i]), b = mod[i], c = r[i] - ret * t;c = (c % b + b) % b;LL x, y, d;d = exgcd(a, b, x, y);if(c % d) return -1;LL k1 = b / d;x = qmul(x, c / d, k1);x = (x % k1 + k1) % k1;ret = ret + M * x;M = k1 * M;ret = (ret % M + M) % M;// 使用了一把剑之后这把剑就没了, 并奖励一把新的剑atk.erase(atk.find(t));atk.insert(atk_rwd[i]);// 维护 limitlimit = max(limit, (r[i] + t - 1) / t);}// 根据 limit 和 ret 的差值把 ret 补成大于 limit 的最小合法解if(ret < limit) ret = ret + (limit - ret + M - 1) / M;return ret;
}int main()
{int t; cin >> t;while(t--){// 多组测试用例记得清空上一组的数据atk.clear();limit = 0;int n, m;cin >> n >> m;for(int i = 1; i <= n; i++) cin >> r[i];for(int i = 1; i <= n; i++) cin >> mod[i];for(int i = 1; i <= n; i++) cin >> atk_rwd[i];for(int i = 1; i <= m; i++){LL t; cin >> t;atk.insert(t);}cout << excrt(n) << endl;}return 0;
}
