数论------同余问题全家桶3 __int128和同余方程组
- 快速读写和__int128
- 中国剩余定理和线性同余方程组
-
- 中国剩余定理(CRT)
- 中国剩余定理OJ示例
-
- [模板题曹冲养猪 - 洛谷](#模板题曹冲养猪 - 洛谷)
- [模板题猜数字 - 洛谷](#模板题猜数字 - 洛谷)
- 扩展中国剩余定理
- 扩展中国剩余定理OJ示例
-
- [模板题扩展中国剩余定理(EXCRT) - 洛谷](#模板题扩展中国剩余定理(EXCRT) - 洛谷)
- [NOI2018 屠龙勇士](#NOI2018 屠龙勇士)
快速读写和__int128
这块算是补充知识点,但仅作为高精度算法的临时替代,在有的题若使用
__int128
也会溢出,则只能用高精度算法。
虽然在 99%
的题目中,scanf
以及 printf
来处理输入和输出已经足够快了。但是,如果输入和输出的规模特别庞大,或者出题人卡常,scanf
和 printf
也是会超时的,这个时候就需要更加快速的读写方式。
同样的,虽然在 99%
的题目中,long long
已经足够我们应付较大的整数。但是,如果题目中数的范围过大,相乘也是有可能超过 long long
的最大范围。如果只是大一点点,此时可以用 __int128
来存储。
快速读写
快速读写有很多种版本,接下来要介绍一种容易实现且够用的版本 - getchar
/putchar
结合秦九韶算法。
前置知识:
-
在计算机的视角,所有的整数其实是一个一个的字符串,每个数拆开看就是一个一个字符。因此,对于一个整数,可以当成字符串,一个一个字符的输入进来。同理,输出一个整数的时候,也可以当成字符串,一个一个字符的输出。
-
getchar
/putchar
这种输入输出方式相较于scanf
/printf
而言,速度更快。
于是,就可以利用 getchar
将字符转换成整数输入,利用 putchar
将整数转换成字符输出。
如果这种方式还是超时,那就把 getchar
换成 getchar_unlocked
。如果换完之后还是超时,那就是毒瘤题,可以忽视,或者就是算法本身的时间复杂度有问题。
但是,getchar_unlocked
只能在 Linux 系统下使用,因此要慎用。
这里通过模板题进行检测:
cpp
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int flag=1,ans=0;
//这个函数只有在linux操作系统下的gcc系列编译器才能使用
//当然,洛谷也能使用,不然最后一个样例无法通过
char ch=getchar_unlocked();
//一般编译器用这个
// char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')
flag=-1;
ch=getchar_unlocked();
// ch=getchar();
}
while(ch>='0'&&ch<='9'){
ans=ans*10+ch-'0';
ch=getchar_unlocked();
// ch=getchar();
}
return ans*flag;
}
inline void print(int a){
if(a<0){
putchar('-');
a=-a;
}
if(a>9)
print(a/10);
putchar(a%10+'0');
}
int main() {
int n=read();
int sum=0,x;
while(n--){
x=read();
sum+=x;
}
print(sum);
return 0;
}
__int128
【更大的整数__int128】
__int128
就是占用 128 字节的整数存储类型,范围就是 − 2 127 ∼ 2 127 − 1 -2^{127} \sim 2^{127} - 1 −2127∼2127−1。
即
− 170141183460469231731687303715884105728 ∼ 170141183460469231731687303715884105727 -170141183460469231731687303715884105728\\\sim170141183460469231731687303715884105727 −170141183460469231731687303715884105728∼170141183460469231731687303715884105727。
170 1411 8346 0469 2317 3168 7303 7158 8410 5727//39位
-
对比
int
的范围 − 2 32 ∼ 2 32 − 1 -2^{32} \sim 2^{32} - 1 −232∼232−1,即 − 2147483648 ∼ 2147483647 -2147483648\sim2147483647 −2147483648∼2147483647。无符号情况下 0 ∼ 4294967295 0\sim 4294967295 0∼4294967295。
-
long long
: − 2 64 ∼ 2 64 − 1 -2^{64} \sim 2^{64} - 1 −264∼264−1,即 − 9223372036854775808 ∼ 9223372036854775807 -9223372036854775808\sim9223372036854775807 −9223372036854775808∼9223372036854775807。无符号情况下 0 ∼ 18446744073709551615 0\sim18446744073709551615 0∼18446744073709551615。
- 如果使用了
unsigned __int128
,则范围变成 0 ∼ 2 128 0 \sim 2^{128} 0∼2128,即约 39 位数。
0 ∼ 340282366920938463463374607431768211455 0\sim 340282366920938463463374607431768211455 0∼340282366920938463463374607431768211455。
当数据范围超过 long long
,但是还不足以用上高精度算法的时候,用 __int128
是个不错的选择。
但是,__int128
是不能直接用 cin
、cout
、scanf
以及 printf
输入或者输出的。只能按照字符的方式输入输出,也就是我们刚刚学习的快速读写的方式。不过,当读入进来之后,用法和普通的整型变量一致。
__int128
的使用示例(需要在gcc系IDE使用,例如Devc++):
cpp
#include<bits/stdc++.h>
using namespace std;
typedef unsigned __int128 uint;
typedef __int128 _int;
inline void print(uint n){
if(n>9)
print(n/10);
putchar(n%10+'0');
}
inline void print(_int n){
if(n<0){
putchar('-');
n=-n;
}
if(n>9)
print(n/10);
putchar(n%10+'0');
}
inline _int read(){
_int flag=1,ans=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')
flag=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
ans=ans*10+ch-'0';
ch=getchar();
}
return ans*flag;
}
void f1(){
_int mmax=(_int(1)<<127)-1;
print(mmax);
cout<<endl;
_int mmin=(_int(1)<<127);
print(mmin+1);//无法直接输出-2^128
cout<<endl;
}
void f2(){
uint mmax=~uint(0);
print(mmax);
cout<<endl;
}
void f3(){
_int a=114514;
print(a);
cout<<endl;
a=read();
print(a);
cout<<endl;
a+=3;
print(a);
cout<<endl;
a-=486;
print(a);
cout<<endl;
a*=1435;
print(a);
cout<<endl;
a/=1435;
print(a);
cout<<endl;
a%=10007;
print(a);
cout<<endl;
}
int main() {
// f1();
// f2();
f3();
return 0;
}
__int128
再好用,缺陷也很多:
-
不通用。
__int128
并没有在任何一个 c++ 标准中严格定义,所以目前它只是 GCC 系列编译器的专属。目前测试,只在 Linux 系统下能够正常使用,在其他编译器例如MSVC不能使用。因此如果要使用,就要看比赛中的编译器,是否是 Linux。 -
不方便。
__int128
目前是不支持直接读入、输出的。也就是无法用cin
、cout
、scanf
以及printf
输入或者输出这种类型的数。只能按照字符的方式输入输出,也就是我们刚刚学习的快速读写的方式。 -
空间大,速度慢。
__int128
占用了 16 个字节来存,空间超限的风险显著增加。
但是,在有些题目中,__int128
还是足够我们使用的。
中国剩余定理和线性同余方程组
引入线性同余方程组:南北朝时期《孙子算经》中有个问题:有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?
用数学公式翻译过来就是一个线性同余方程组:
{ x ≡ 2 ( mod 3 ) x ≡ 3 ( mod 5 ) x ≡ 2 ( mod 7 ) \begin{cases} x \equiv 2(\text{mod } 3) \\ x \equiv 3(\text{mod } 5) \\ x \equiv 2(\text{mod } 7) \end{cases} ⎩ ⎨ ⎧x≡2(mod 3)x≡3(mod 5)x≡2(mod 7)
线性同余方程组:求这个方程组的最小非负整数解。
{ x ≡ r 1 ( mod m 1 ) x ≡ r 2 ( mod m 2 ) ⋮ x ≡ r n ( mod m n ) \begin{cases} x \equiv r_1(\text{mod } m_1) \\ x \equiv r_2(\text{mod } m_2) \\ \vdots \\ x \equiv r_n(\text{mod } m_n) \end{cases} ⎩ ⎨ ⎧x≡r1(mod m1)x≡r2(mod m2)⋮x≡rn(mod mn)
中国剩余定理(CRT)
前提:所有的模数 m 1 , m 2 , . . . , m n m_1, m_2, ..., m_n m1,m2,...,mn 两两互质。因此中国剩余定理的使用比较局限。
原理:中国剩余定理是基于"构造法"得出的结果。以下给出 n = 3 n = 3 n=3 的构造过程, n n n 等于任意数的构造过程是一样的。
对于这个方程组:
{ x ≡ r 1 ( mod m 1 ) x ≡ r 2 ( mod m 2 ) x ≡ r 3 ( mod m 3 ) \begin{cases} x \equiv r_1 (\text{mod } m_1) \\ x \equiv r_2 (\text{mod } m_2)\\x \equiv r_3 (\text{mod } m_3) \end{cases} ⎩ ⎨ ⎧x≡r1(mod m1)x≡r2(mod m2)x≡r3(mod m3)
或
{ x m o d m 1 = r 1 x m o d m 2 = r 2 x m o d m 3 = r 3 \begin{cases}x\bmod m_1=r_1\\x\bmod m_2=r_2\\x\bmod m_3=r_3\end{cases} ⎩ ⎨ ⎧xmodm1=r1xmodm2=r2xmodm3=r3
记 M = m 1 × m 2 × m 3 M = m_1 \times m_2 \times m_3 M=m1×m2×m3,构造解: x = C 1 + C 2 + C 3 x = C_1 + C_2 + C_3 x=C1+C2+C3,其中:
-
C 1 mod m 1 = r 1 , C 1 mod m 2 = 0 , C 1 mod m 3 = 0 C_1 \text{ mod } m_1 = r_1,\ C_1 \text{ mod } m_2 = 0,\ C_1 \text{ mod } m_3 = 0 C1 mod m1=r1, C1 mod m2=0, C1 mod m3=0
-
C 2 mod m 1 = 0 , C 2 mod m 2 = r 2 , C 2 mod m 3 = 0 C_2 \text{ mod } m_1 = 0, \ C_2 \text{ mod } m_2 = r_2, \ C_2 \text{ mod } m_3 = 0 C2 mod m1=0, C2 mod m2=r2, C2 mod m3=0
-
C 3 mod m 1 = 0 , C 3 mod m 2 = 0 , C 3 mod m 3 = r 3 C_3 \text{ mod } m_1 = 0, \ C_3 \text{ mod } m_2 = 0, \ C_3 \text{ mod } m_3 = r_3 C3 mod m1=0, C3 mod m2=0, C3 mod m3=r3
这样 x m o d m 1 = C 1 m o d m 1 + C 2 m o d m 1 + C 3 m o d m 1 = r 1 x\bmod m1 = C_1\bmod m1+C_2\bmod m1+C_3\bmod m_1=r_1 xmodm1=C1modm1+C2modm1+C3modm1=r1,其他 m 2 m2 m2、 m 3 m3 m3同理。
因此只要能构造出这样的 x x x,那么 x x x 就是我们要的解。
C 1 C_1 C1、 C 2 C_2 C2和 C 3 C_3 C3的构造方式:
-
C 1 = r 1 × m 2 × m 3 × ( m 2 × m 3 ) − 1 , C_1 = r_1 \times m_2 \times m_3 \times (m_2 \times m_3)^{-1}, C1=r1×m2×m3×(m2×m3)−1,
(m_2 \\times m_3)\^{-1} 为模 为模 为模m_1 意义下的逆元, 意义下的逆元, 意义下的逆元,m_2 \\times m_3 \\times (m_2 \\times m_3)\^{-1}\\bmod m_1 最后的结果是 1 。所以 最后的结果是1。所以 最后的结果是1。所以C_1\\bmod m_1=r_1 , , ,C_1\\bmod m_2=C_1\\bmod m_3=0。
-
C 2 = r 2 × m 1 × m 3 × ( m 1 × m 3 ) − 1 , C_2 = r_2 \times m_1 \times m_3 \times (m_1 \times m_3)^{-1}, C2=r2×m1×m3×(m1×m3)−1, 其中 (m_1 \\times m_3)\^{-1} 为模 为模 为模m_1意义下的逆元。
-
C 3 = r 3 × m 1 × m 2 × ( m 1 × m 2 ) − 1 , C_3 = r_3 \times m_1 \times m_2 \times (m_1 \times m_2)^{-1}, C3=r3×m1×m2×(m1×m2)−1, 其中 (m_1 \\times m_2)\^{-1} 为模 为模 为模m_1意义下的逆元。
当 x x x加减 M M M的若干倍时,依旧是满足方程,因此最小非负整数解就是
( x mod M + M ) mod M (x \text{ mod } M + M) \text{ mod } M (x mod M+M) mod M。
整个方程的通解是 x + k M x+kM x+kM。
以《孙子算经》中的问题为例:
{ x ≡ 2 ( mod 3 ) x ≡ 3 ( mod 5 ) x ≡ 2 ( mod 7 ) \begin{cases} x \equiv 2(\text{mod } 3) \\ x \equiv 3(\text{mod } 5) \\ x \equiv 2(\text{mod } 7) \end{cases} ⎩ ⎨ ⎧x≡2(mod 3)x≡3(mod 5)x≡2(mod 7)
-
计算每一个方程的 C i C_i Ci:
C 1 = r 1 × m 2 × m 3 × ( m 2 × m 3 ) − 1 = 2 × 5 × 7 × 35 − 1 ( mod 3 ) = 2 × 5 × 7 × 2 = 140 C_1 = r_1 \times m_2 \times m_3 \times (m_2 \times m_3)^{-1} = 2 \times 5 \times 7 \times 35^{-1}(\text{mod } 3) = 2 \times 5 \times 7 \times 2 = 140 C1=r1×m2×m3×(m2×m3)−1=2×5×7×35−1(mod 3)=2×5×7×2=140
C 2 = r 2 × m 1 × m 3 × ( m 1 × m 3 ) − 1 = 3 × 3 × 7 × 21 − 1 ( mod 5 ) = 3 × 3 × 7 × 1 = 63 C_2 = r_2 \times m_1 \times m_3 \times (m_1 \times m_3)^{-1} = 3 \times 3 \times 7 \times 21^{-1}(\text{mod } 5) = 3 \times 3 \times 7 \times 1 = 63 C2=r2×m1×m3×(m1×m3)−1=3×3×7×21−1(mod 5)=3×3×7×1=63
C 3 = r 3 × m 1 × m 2 × ( m 1 × m 2 ) − 1 = 2 × 3 × 5 × 15 − 1 ( mod 7 ) = 2 × 3 × 5 × 1 = 30 C_3 = r_3 \times m_1 \times m_2 \times (m_1 \times m_2)^{-1} = 2 \times 3 \times 5 \times 15^{-1}(\text{mod } 7) = 2 \times 3 \times 5 \times 1 = 30 C3=r3×m1×m2×(m1×m2)−1=2×3×5×15−1(mod 7)=2×3×5×1=30 -
计算结果: x = ( 140 + 63 + 30 ) mod 105 = 233 mod 105 = 23 x = (140 + 63 + 30) \text{ mod } 105 = 233 \text{ mod } 105 = 23 x=(140+63+30) mod 105=233 mod 105=23
推广 : n n n取任意数的时候,构造方式也是同理。
- C 1 = r 1 × m 2 × m 3 × ... × m n × ( m 2 × m 3 × ... × m n ) − 1 C_1 = r_1 \times m_2 \times m_3 \times \ldots \times m_n \times (m_2 \times m_3 \times \ldots \times m_n)^{-1} C1=r1×m2×m3×...×mn×(m2×m3×...×mn)−1,
- C 2 = r 2 × m 1 × m 3 × ... × m n × ( m 1 × m 3 × ... × m n ) − 1 C_2 = r_2 \times m_1 \times m_3 \times \ldots \times m_n \times (m_1 \times m_3 \times \ldots \times m_n)^{-1} C2=r2×m1×m3×...×mn×(m1×m3×...×mn)−1,
- ... \ldots ...
- C n = r n × m 1 × m 2 × ... × m n − 1 × ( m 1 × m 2 × ... × m n − 1 ) − 1 C_n = r_n \times m_1 \times m_2 \times \ldots \times m_{n-1} \times (m_1 \times m_2 \times \ldots \times m_{n-1})^{-1} Cn=rn×m1×m2×...×mn−1×(m1×m2×...×mn−1)−1。
当 m 1 , m 2 , ... , m n m_1, m_2, \ldots, m_n m1,m2,...,mn 两两互质时,一定存在这样的 C 1 , C 2 , ... , C n C_1, C_2, \ldots, C_n C1,C2,...,Cn,因为对应的逆元是一定存在的。且通解为: X = k × lcm + x X = k \times \text{lcm} + x X=k×lcm+x,其中 lcm \text{lcm} lcm因为 m i m_i mi全是质数,所以实际相当于整体的乘积。
总结:在算法竞赛中,中国剩余定理应用的算法流程:
-
计算所有模数的乘积 M = m 1 × m 2 × ... × m n M = m_1 \times m_2 \times \ldots \times m_n M=m1×m2×...×mn;
-
计算第 i i i 个方程的 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 ( mod M ) x = \sum_{i=1}^n r_i c_i c_i^{-1} (\text{mod } M) x=∑i=1nricici−1(mod M)。
中国剩余定理OJ示例
模板题曹冲养猪 - 洛谷
P1495 【模板】中国剩余定理(CRT)/ 曹冲养猪 - 洛谷
CRT的模板题,因为数据量大,需要使用快速幂改装的龟速乘算法来进行最后的求乘。
龟速乘是能一步出结果的乘法,拆分成若干步乘完,这样做能最大程度保证不溢出,但牺牲了速度。
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
//扩展欧几里得算法
void exgcd(LL a, LL b, LL& x, LL& y) {
if (!b) {
x = 1; y = 0;
return;
}
LL x1, y1;
exgcd(b, a % b, x1, y1);
x = y1; y = x1 - a / b * y1;
}
//龟速乘,防溢出
LL qmul(LL a, LL b, LL m) {
LL ans = 0;
while (b) {
if (b % 2)
ans = (ans%m + a%m) % m;
a = (a + a) % m;
b /= 2;
}
return ans;
}
void ac() {
LL n;
cin >> n;
vector<LL>r(n + 1, 0), m(n + 1, 1);
for (LL i = 1; i <= n; i++)
cin >> m[i] >> r[i];
//中国剩余定理
LL M = 1;
for (auto& x : m)
M *= x;
LL ans = 0;
for (LL i = 1; i <= n; i++) {
LL ci = M / m[i];
LL x, y;
exgcd(ci, m[i], x, y);
x = (x % m[i] + m[i]) % m[i];
//快速幂改装的鬼速乘算法
ans = (ans % M + qmul(qmul(r[i], ci, M), x, M)) % M;
}
cout << ans << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T = 1;
//cin >> T;
while (T--)
ac();
return 0;
}
模板题猜数字 - 洛谷
P3868 [TJOI2009\] 猜数字 - 洛谷](https://www.luogu.com.cn/problem/P3868)
中国剩余定理的推广模板。题目要求的是 b i ∣ ( n − a i ) b_i\\mid (n-a_i) bi∣(n−ai),即 ( n − a i ) m o d b i = 0 (n-a_i)\\bmod b_i=0 (n−ai)modbi=0,将等式拆分开就是 n m o d b i − a i m o d b i = 0 n\\bmod b_i-a_i\\bmod b_i=0 nmodbi−aimodbi=0,即 n ≡ a i ( m o d b i ) n\\equiv a_i\\pmod {b_i} n≡ai(modbi)。
因此题目要求的就是同余方程组 n ≡ a i ( m o d b i ) n\\equiv a_i\\pmod {b_i} n≡ai(modbi)的解, n n n是未知数。
```cpp
#include