数论专题
数论专题
编者:czh(大的,弱的那个),lhy
矩阵
认识矩阵
矩阵就是一个数字阵列,一个 n 行 r 列的矩阵可以表示为:
[a1,1a1,2⋯a1,ra2,1a2,2⋯a2,r⋯an,1an,2⋯an,r]
我们称上面的矩阵为 n×r 的矩阵。
特别的,如果一个行数和列数相等的矩阵,我们称其为方阵,例如下面就是一个 3×3 的方阵:
[143267976]
其实矩阵对我们来说也并不是那么陌生,因为它类似于我们程序中的二维数组。
还有一种矩阵在矩阵乘法中起着重要的作用,它叫单位矩阵,它就只有对角线是 1,其他都是 0,它在矩阵乘法中相当于乘法中的 1,例如下面是一个单位矩阵:
[100010001]
矩阵的运算
矩阵加减法
矩阵的加减法非常简单,只要把两个矩阵对应位置的元素加减即可。要求是两个矩阵的行、列数相等。
矩阵乘法
- 一个整数 k 乘矩阵 A,把 k 乘以矩阵中的每个元素即可。
- 两个矩阵 A 和 B 相乘,要求 A 的列数等于 B 的行数,设 A 为 m×n 的矩阵,B 为 n×u 的矩阵,那么 C=A×B 的尺寸为 m×u,计算公式为 Ci,j=n∑k=1ai,k×bk,j,复杂度为 O(m×n×u)。
根据矩阵乘法的定义,可以推出:
- 矩阵乘法有结合律。(AB)C=A(BC)。
- 矩阵乘法有分配率。(A+B)C=AC+BC。
- 矩阵乘法没有交换律,即 AB≠BA。
快速幂
整数的快速幂非常简单,对于 an 一个个乘要 O(n),这实在是太慢了。
快速幂的一个解法就是分治算法,就是先算 a2,再计算 (a2)2,⋯,一直到 an。
具体实现就是首先先将指数 n 二进制拆分,将其拆为 2 的次幂的和,然后再一个个乘起来就可以了。
下面简单的举个例子。
a13=a8+4+1=a8×a4×a1,其中 a1,a4,a8 的幂次都是 2 的次幂,可以逐级递推来实现。
下面给出代码实现(我还特意不压行了,正常这只在我的代码中占一行)
ll ksm(ll a, ll b){
ll res=1;//一定是1
while(b){
if(b&1) res=res*a;
a=a*a;
b>>=1;
}
return res;
}
矩阵快速幂
矩阵快速幂就相当于把矩阵看成一个整数来,原理和正常快速幂一模一样。
最后基本的运算都讲完了,如果想了解其他(如矩阵的逆)可自行查询,下面给出这些运算的代码。(抽象马蜂不喜勿喷)
class matrix{
public:
ll g[85][85];ll k=85;
matrix(){for(int i=0; i<k; i++) for(int j=0; j<k; j++)g[i][j]=0;}
void clear(){for(int i=0; i<k; i++) for(int j=0; j<k; j++)g[i][j]=0;}
void print(){for(int i=0; i<k; i++){for(int j=0; j<k; j++)cout<<g[i][j]<<' ';cout<<endl;}}
matrix& operator = (const matrix& x){for(int i=0; i<k; i++) for(int j=0; j<k; j++) g[i][j]=x.g[i][j];return *this;}
matrix operator + (const matrix& x){matrix c;for(int i=0; i<k; i++) for(int j=0; j<k; j++) c.g[i][j]=g[i][j]+x.g[i][j];return c;}
matrix operator * (const matrix& x){matrix c;for(int i=0; i<k; i++) for(int j=0; j<k; j++) for(int l=0; l<k; l++) c.g[i][j]+=g[i][l]*x.g[l][j];return c;}
matrix operator ^ (const ll& x){matrix res,a=*this;ll b=x;for(int i=0; i<k; i++) res.g[i][i]=1;while(b){if(b&1)res=res*a;a=a*a;b>>=1;}return res;}
};
k 为矩阵的边长,可自行调整,因为这是我从某一题复制过来的,所以是 85,没有特殊意义。
矩阵的应用
矩阵乘法的精妙之处就在于:
- 很容易将有用的状态存储在一个矩阵中。
- 通过状态矩阵与转移矩阵相乘可以快速得到一次 DP 的值(注意,这个 DP 的状态方程必须是一次的递推式)。
- 求矩阵相乘的结果是要做很多次的乘法,这样的效率有时还不如原来一次的 DP 转移,但是由于矩阵乘法满足结合律,可以先用快速幂算后面的转移矩阵迅速地处理好后面的转移矩阵,再用初始矩阵乘上后面的转移矩阵得到结果,算法复杂度大概就是带个(可能比较大的)常数的 O(logn)。
例题
这题也是非常经典。
首先我们知道 f 的递推式为 fi=fi−1+fi−2,于是有:
- fi=1×fi−1+1×fi−2
- fi−1=1×fi−1+0×fi−2
所以可以推出:
[fifi−1]=[1110]×[fi−1fi−2]
进而,我们就可以推出:
[fnfn−1]=[1110]n−2×[f2f1]
用矩阵快速幂即可,注意乘法的先后顺序,矩阵乘法不满足交换律。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int mod=1e9+7;
ll n;
class matrix{
public:
ll g[3][3];ll k=3;
matrix(){for(int i=0; i<k; i++) for(int j=0; j<k; j++)g[i][j]=0;}
void clear(){for(int i=0; i<k; i++) for(int j=0; j<k; j++)g[i][j]=0;}
void print(){for(int i=0; i<k; i++){for(int j=0; j<k; j++)cout<<g[i][j]<<' ';cout<<endl;}}
matrix& operator = (const matrix& x){for(int i=0; i<k; i++) for(int j=0; j<k; j++) g[i][j]=x.g[i][j];return *this;}
matrix operator * (const matrix& x){matrix c;for(int i=0; i<k; i++) for(int j=0; j<k; j++) for(int l=0; l<k; l++) (c.g[i][j]+=g[i][l]*x.g[l][j]%mod)%mod;return c;}
matrix operator ^ (const ll& x){matrix res,a=*this;ll b=x;for(int i=0; i<k; i++) res.g[i][i]=1;while(b){if(b&1)res=res*a;a=a*a;b>>=1;}return res;}
}f,a,ans;
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
int main(){
n=read();
if(n<=2){write(1);putchar('\n');return 0;}//特判
a.g[0][0]=a.g[0][1]=a.g[1][0]=1;
f.g[0][0]=f.g[1][0]=1;
a=a^(n-2);f=a*f;
write(f.g[0][0]%mod);putchar('\n');
return 0;
}
首先看完题目很自然的想到可以设 dpi,j 表示第 i 个骰子以 j 面朝上的方案数。
首先对立关系由于总共才 6 种情况,所以可以直接用一个二维数组 mpi,j 来存,i,j 排斥为 0,否则为 1。
初始化就是 dp1 的所有方案数为 1。
考虑转移,dpi,j=6∑k=1dpi−1,oppk×mpj,oppk。
其中,oppi 表示 i 的对面。
opp[10]={0,4,5,6,1,2,3}
但是 n≤109,这么做显然是不行的,考虑矩阵加速。
对于样例,我们有如下式子。
[dpn,1dpn,2dpn,3dpn,4dpn,5dpn,6]=[111101111011111111111111111111111111]×[dpn−1,1dpn−1,2dpn−1,3dpn−1,4dpn−1,5dpn−1,6]。
[dpn,1dpn,2dpn,3dpn,4dpn,5dpn,6]=[111101111011111111111111111111111111]n−1×[dp1,1dp1,2dp1,3dp1,4dp1,5dp1,6]。
统计答案 ans 非常简单,就直接将最终得到的矩阵的值全部加起来即可。
当然这样子是不行的,因为每一个筛子因为侧边是可以旋转的,都有 4 种摆法,所以最终得到的 ans 还得乘上 4n,快速幂解决即可(记得取模。。。)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int MN=40;
const int mod=1e9+7;
ll n,m,u,v,mp[MN][MN],ans,opp[MN]={0,4,5,6,1,2,3};
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
struct matrix{
ll g[7][7];
void clear(){memset(g,0,sizeof(g));}
}f1,f2;
inline matrix operator * (matrix a, matrix b){
matrix c;c.clear();
for(int i=1; i<=6; i++) for(int j=1; j<=6; j++) for(int k=1; k<=6; k++) c.g[i][j]=(c.g[i][j]+(a.g[i][k]*b.g[k][j])%mod)%mod;
return c;
}
inline matrix operator ^ (matrix a, ll b){
matrix ans;ans.clear();
for(int i=1; i<=6; i++) ans.g[i][i]=1;
while(b){
if(b&1) ans=ans*a;
a=a*a;
b>>=1;
}
return ans;
}
ll ksm(ll a, ll b){
ll res=1;
while(b){
if(b&1) res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
void init(){
for(int i=1; i<=6; i++) f2.g[i][1]=1;
for(int i=1; i<=6; i++) for(int j=1; j<=6; j++) f1.g[i][j]=mp[i][opp[j]]^1;
}
int main(){
// freopen("1.in","r",stdin);
n=read();m=read();
for(int i=1; i<=m; i++){
u=read();v=read();
mp[u][v]=mp[v][u]=1;
}
init();
matrix res=(f1^(n-1))*f2;
for(int i=1; i<=6; i++) ans=(ans+res.g[i][1])%mod;
ans=(ans*ksm(4,n))%mod;
write(ans);
return 0;
}
矩阵也可以加速 Floyed,比如 这题。
首先,我们设 fl,i,j 表示 i→j 长度为 l 的路径数量,由乘法原理,有:
fl,i,j=n∑k=1fl−1,i,k×f1,k,j
其中 f1 就是我们输入的矩阵,正常来看,只要一层层推就好了,但是发现 k 过于大,所以考虑优化。
观察式子发现这和矩阵乘法一模一样,ft=ft−1×f1,于是就有 fk=fk1。
利用矩阵快速幂即可。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int MN=55;
const int mod=1e9+7;
ll n,k,res;
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
class matrix{
public:
ll g[MN][MN];
matrix(){memset(g,0,sizeof(g));}
void clear(){memset(g,0,sizeof(g));}
matrix& operator = (const matrix& x){for(int i=0; i<n; i++) for(int j=0; j<n; j++) g[i][j]=x.g[i][j];return *this;}
matrix operator * (const matrix& a){matrix res;for(int i=0; i<n; i++) for(int j=0; j<n; j++) for(int l=0; l<n; l++) res.g[i][j]=(res.g[i][j]+g[i][l]*a.g[l][j]%mod)%mod;return res;}
matrix operator ^ (const ll& x){matrix res=*this,a=*this;ll b=x-1;while(b){if(b&1)res=res*a;a=a*a;b>>=1;}return res;}
}f,ans;
int main(){
n=read();k=read();
for(int i=0; i<n; i++) for(int j=0; j<n; j++) f.g[i][j]=read();
ans=f^k;
for(int i=0; i<n; i++) for(int j=0; j<n; j++) res=(res+ans.g[i][j])%mod;
write(res);putchar('\n');
return 0;
}
练习题
GCD 和 LCM
整除
整除定义
a 能整除 b,记为 a|b。其中,a 和 b 为整数,且 a≠0,b 是 a 的倍数,a 是 b 的约数。
性质
- 若 a,b,c 为整数,且 a|b,b|c,则 a|c。
- 若 a,b,m,n 为整数,且 c|a,c|b,则 c|(ma+nb)。
- 定理:带余除法。如果 a 和 b 为整数且 b>0,则存在唯一的整数 q,r,使得 a=bq+r,0⩽r<b。
GCD
定义
a 和 b 的最大公约数是指能同时整除 a 和 b 的最大整数,记为 gcd(a,b)。
注意:由于 −a 的因子和 a 的因子相同,因此 gcd(a,b)=gcd(|a|,|b|)。
性质
- gcd(a,b)=gcd(a,a+b)=gcd(a,ka+b)。
- gcd(ka,kb)=k×gcd(a,b)。
- 定义多个整数的最大公约数,gcd(a,b,c)=gcd(gcd(a,b),c)。
- 若 gcd(a,b)=d,则 gcd(ad,bd)=1,即 ad 与 bd 互素。
- gcd(a+cb,b)=gcd(a,b)。
GCD 的计算
由于 gcd(a,b)=gcd(b,a−b)=gcd(a,a−b),所以就有辗转相减法,用较大的数减较小的数,代码如下:
ll gcd(ll a, ll b){
while(a!=b){
if(a>b) a=a-b;
else b=b-a;
}
return a;
}
但是我们发现这样一次次减太慢了,在最劣的情况下会跑到 max(a,b) 次,我们发现其实多次减法就相当于一次取模,所以我们就有辗转相除(欧几里得)法:
ll gcd(ll a, ll b){
if(!b) return a;
return gcd(b,a%b);
}
LCM
定义
a 和 b 的最小公倍数表示为 lcm(a,b)。
算数基本定理
任何一个大于 1 的正整数 n 都可以唯一分解为有限个素数的乘积:n=pc11pc22⋯pcmm,其中 ci 都为正整数 pi 都为素数。
LCM 的计算
设 a=pc11pc22⋯pcmm,b=pf11pf22⋯pfmm,那么 gcd(a,b)=pmin(c1,f1)1pmin(c2,f2)2⋯pmin(cm,fm)m,lcm(a,b)=pmax(c1,f1)1pmax(c2,f2)2⋯pmax(cm,fm)m。
所以 gcd(a,b)×lcm(a,b)=ab,所以就有 lcm(a,b)=abgcd(a,b)。
ll lcm(ll a, ll b){
return a/gcd(a,b)*b;
}
注意:这里最好先除法再乘法,防止溢出。
裴蜀定理
如果 a 和 b 均为整数,则有整数 x,y 使得 ax+by=gcd(a,b)。
证明:分类讨论
若 a,b 中有一个数位 0,则结论显然成立。
若 a,b 均不为 0。
∵gcd(a,b)=gcd(a,−b)
所以不妨设 a⩾b>0,d=gcd(a,b)
对于 ax+by=d,考虑等式两边同时除以 d 得
a1x+b1y=1,gcd(a1,b1)=1
问题就转化成了 a1x+b1y=1。
考虑之前说的辗转相除法:gcd(a,b)→gcd(b,amodb)→⋯,我们设每次取模的数设为 r,于是有:
不妨令辗转相除法在除到互质的时候退出则 rn=1 所以有
即
由倒数第三个式子 rn−1=rn−3−xn−1rn−2 代入上式,得
然后用同样的办法用它上面的等式逐个地消去 rn−2,⋯,r1,
可证得 1=a1x+b1y. 这样等于是一般式中 d=1 的情况。
推论
整数 a 和 b 互质当且仅当存在整数 x 和 y,使得 ax+by=1。
同余式
同余的定义
同余:设 m 是正整数,若 a 和 b 是整数,且 m|(a−b),则称 a 和 b 模 m 同余。
剩余系:一个模 m 完全剩余系是一个整数的集合,使每个整数恰与此集合中的一个元素模 m 同余。
定理和性质
同余的基本概念:若 a 和 b 是整数,m 为正整数,则 a≡b(modm) 当且仅当 amodm=bmodm。
同余式转化成等式:若 a 和 b 是整数,则 a≡b(modm) 当且仅当存在整数 k,使得 a=b+km。
设 m 为正整数,模 m 的同余满足以下基本性质:
- 自反性:若 a 是整数,则 a≡a(modm)。
- 对称性:若 a 和 b 是整数,且 a≡b(modm),则 b≡a(modm)。
- 传递性:若 a,b,c 是整数,且 a≡b(modm),b≡c(modm),则 a≡c(modm)。
关于同余的加减乘除,若 a,b,c 和 m 是整数,m>0,且 a≡b(modm),c≡d(modm),则有以下性质:
- 加:a+c≡b+c(modm),更一般的,a+c≡b+d(modm)。
- 减:a−c≡b−c(modm),更一般的,a+c≡b+d(modm)。
- 乘:ac≡bc(modm),更一般的,ac≡bd(modm)。
- 除:在同余的两边同时除以一个整数,不一定保持同余,但满足 ac≡bc(modm)→a≡b(modpgcd(c,p))
- 同余的幂:若 a,b,k 和 m 是整数,k>0,m>0,且 ak≡bk(modm)。
- 若 amodp=x,amodq=x,其中,p,q 互质,则 amod(pq)=x。
对于最后一个性质,我们稍微给出证明。
∵amodp=x,amodq=x,gcd(p,q)=1
∴ 一定存在整数 s,t,使得 a=sp+x,a=tq+x
∴sp=tq
∴ 一定存在一个整数 r 使得 s=rq。
∴a=rpq+x
∴amod(pq)=x
除了上面所介绍的这些定理之外,我们还有。
费马小定理
若 p 为素数,gcd(a,p)=1,则 ap−1≡1(modp)。
引理
设 m 是一个整数且 m>1,b 是一个正整数且 gcd(m,b)=1。如果 a1,a2,a3,⋯,am−1 是模 m 的一个完全剩余系,则 b⋅a1,b⋅a2,b⋅a3,⋯,b⋅am−1 也构成模m的一个完全剩余系。
证明:反证法。
设在 b⋅a1,b⋅a2,b⋅a3,⋯,b⋅am−1 中存在 b⋅ai≡b⋅aj(modm)(i≠j,i,j∈(0,m),i,j∈Z)。
则 ai≡aj(modm),矛盾。
证明
构造素数 m 的完全剩余系:M={1,2,3,⋯,m−1},再取一个整数 a 使得 gcd(a,m)=1。
由引理得,A={a,2a,3a,⋯,(m−1)a} 也为 m 的完全剩余系。
∴m−1∏i=1 Ai≡m−1∏i=1(Ai×a)(modm)
令 f=(m−1)!,则有 am−1×f≡f(modm)。
∵m 是质数
∴gcd((m−1)!,m)=1
∴am−1≡1(modm)。
欧拉定理
若 gcd(a,m)=1,则 aφ(m)≡1(modm)。
证明
证明过程和费马小定理基本一样。
设 a1,a2,a3,⋯,aφ(m) 为模 m 意义下的一个简化剩余系,gcd(b,m)=1,b 为正整数,则 b⋅a1,b⋅a2,b⋅a3,⋯,b⋅aφ(m) 也为模 m 的一个简化剩余系。
∴φ(m)∏i=1ai≡φ(m)∏i=1b⋅ai(modm)
∴bφ(m)≡1(modm)。
线性丢番图方程
二元线性丢番图方程
方程 ax+by=c 称为二元线性丢番图方程,其中 a,b,c 是已知整数,x,y 是变量。
定理
设 a,b 是整数且 gcd(a,b)=d。
- 如果 d 不能整除 c,那么方程 ax+by=c 没有整数解。
- 如果 d 能整除 c,那么存在无穷多个整数解。
另外,若 (x0,y0) 是方程的一个特解,那么通解可以表示为 x=x0+bd×n,y=y0+ad×n,n∈Z。
扩欧与二元丢番图方程
求解方程 ax+by=c 的关键就是找到一个特解。根据定理,ax+by=gcd(a,b) 有整数解,所以可以用扩欧求一个特解 (x0,y0),代码如下:
ll exgcd(ll a, ll b, ll &x, ll &y){
if(!b){x=1,y=0;return a;}
ll d=exgcd(b,a%b,y,x);
y-=a/b*x;
return d;
}
为了方便表述,令 d=gcd(a,b)。
求解 ax+by=c 的步骤大概如下:
- 判断方程 ax+by=c 是否有整数解,即 d 是否能整除 c。
- 用扩欧求出 ax+by=d 的一个特解 (x0,y0)。
- 在 ax0+by0=d 两边同时乘 cd,得 ax0×cd+by0×cd=c。
- 对照 ax+by=c,得到 x′0=x0×cd,y′0=y0×cd
- 方程的通解为 x=x′0+bdn,y=y′0+adn。
同余方程
一元线性同余方程
设 x 是未知数,给定 a,b,m,求整数 x,满足 ax≡b(modm)。
研究线性同余方程有什么用处?ax≡b(modm) 表示 ax−b 是 m 的倍数,设为 −y 倍,则 ax+my=b,这就是二元线性丢番图方程,所以求解一元线性同余方程就 等价于 求解二元线性丢番图方程。
定理
设 a,b,m 都是整数,m>0,gcd(a,m)=d。
- 若 d 不能整除 b,则 ax≡b(modm) 无解。
- 若 d 能整除 b,则 ax≡b(modm) 有 d 个模 m 不同余的解。
推论
a 和 m 互质时,因为 d=gcd(a,m)=1,所以线性同余方程 ax≡b(modm) 有唯一的模 m 不同余的解,换句话说,a,m 互质的时候,a 在模 m 意义下,有且仅有唯一的逆元。
同余方程组
同余方程 ax≡b(modm) 有解时,即 gcd(a,m) 能整除 b 时,可以解得 x≡a′(modm′),这也是同余方程的一般形式。
接下来我们来讲下同余方程组的求解,即:
{x≡a1(modm1)x≡a2(modm2)⋯x≡ar(modmr)
中国剩余定理
设 m1,m2,⋯,mr 是两两互质的正整数,则同余方程组 x≡a1(modm1),x≡a2(modm2),⋯,x≡ar(modmr) 有整数解,并且模 M=m1m2⋯mr 唯一解为 x≡(a1t1M1+a2t2M2+⋯+artrMr)(modm)。
其中,Mi=Mmi,ti 是 Mi 在模 mi 的逆元。
证明
∵ 对于任一 i∈{1,2,⋯,r},∀j∈{1,2,⋯,r},j≠i,gcd(mi,mj)=1
∴ 存在整数 ti 使得 tiMi≡1(modmi)
∴aitiMi≡ai⋅1≡ai(modmi)
又 ∵∀j∈{1,2,⋯,r},j≠i,aitiMi≡0(modmj)
x=a1t1M1+a2t2M2+⋯+artrMr 满足 ∀i∈{1,2,⋯,r},x=aitiMi+∑j≠iajtjMj≡ai+∑j≠i0≡ai(modmi)
∴x 为方程组的一个解。
设 x1,x2 都是方程组的解,那么 ∀i∈{1,2,⋯,r},x1−x2≡0(modmi)
而 m1,m2,⋯,mr 两两互质,说明 M=r∏i=1mi 整除 x1−x2,所以方程组的任何两个解之间必然相差 M 的整数倍。
∴ 方程组的集合为 {kM+r∑i=1aitiMi,k∈Z}
实现
中国剩余定理的限制条件是 m1,m2,⋯,mr 两两互质,如果不互质的话只能用迭代法了。
迭代法的思路很简单,每次合并两个同余式,逐步合并,直到合并完所有等式,只剩下一个,就得到了答案。
合并时,把同余方程转化成等式会更容易。
这里以 {x≡2(mod3)x≡3(mod5)x≡2(mod7) 为例。
前 3 步合并了第 1 个和第 2 个的同余式,后 3 步合并第 3 个等式。
- 把第 1 个同余式转化为 x=2+3t,带入第 2 个同余式,得 2+3t≡3(mod5)。
- 移项得 3t≡1(mod5),因为 gcd(3,5) 能整除 1,所以方程有解 t≡2(mod5),转化成 t=2+5u。
- 把 t=2+5u 代入 x=2+3t 得 x=8+15u,即 x≡8(mod15)。
- 把 x=8+15u 代入第 3 个同余式,得 8+15u≡2(mod7)
- 移项得 15u≡−6(mod7),因为 gcd(15,7) 能整除 −6,所以方程有解 u≡1(mod7),转化成 u=1+7v。
- 把 u=1+7v 代入 x=8+15u 得 x=23+105v,即 x≡23(mod105)。
形式化的:
步骤 | 例子 |
---|---|
合并两个等式 x=a1+Xm1,x=a2+Ym2 | x=2+3X,x=3+5Y |
两个等式相等:a1+Xm1=a2+Ym2 移项得 Xm1+(−Y)m2=a2−a1 | 2+3X=3+5Y3X+5(−Y)=1 |
用扩欧求出其特解 X0 | X0=2 |
X 的通解是 X=X0×cd+bd×n 最小值 t=(X0×cd)mod(bd) | t=(X0×cd)mod(bd)=(2×1/1)mod(5/1)=2 |
把 X=t 代入 x=a1+Xm1,求出原方程的一个特解 x′ | x′=2+2×3=8 |
合并后的新 x=a+Xmm=m1m2gcd(m1,m2)a=x′ | m=3×5/1=15a=8 合并后的新方程为 x=8+15X 即 x≡8(mod15) |
例题
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll __int128
using namespace std;
const int MN=1e5+5;
ll n,a[MN],m[MN];
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
ll exgcd(ll a, ll b, ll &x, ll &y){
if(!b){x=1;y=0;return a;}
ll d=exgcd(b,a%b,y,x);
y-=a/b*x;
return d;
}
ll excrt(){
ll x,y,m1=m[1],a1=a[1],ans=0;
for(int i=2; i<=n; i++){
ll a2=a[i],m2=m[i];
ll a=m1,b=m2,c=(a2-a1%m2+m2)%m2;
ll d=exgcd(a,b,x,y);
if(c%d!=0) return -1;
x=x*(c/d)%(b/d);
ans=a1+x*m1;m1=m2/d*m1;
ans=(ans%m1+m1)%m1;
a1=ans;
}
return ans;
}
int main(){
n=read();
for(int i=1; i<=n; i++) m[i]=read(),a[i]=read();
write(excrt());putchar('\n');
return 0;
}
练习题
威尔逊定理
若 p 为素数,则 p 可以整除 (p−1)!+1。
即:
- ((p−1)!+1)modp=0
- (p−1)!modp=p−1
- (p−1)!=pq−1,q∈N
- (p−1)!≡−1(modp)
证明
这是来自外国语2023届景润班一位数竞生的证明
首先,当 p=2 的时候显然定理成立。
所以就只要考虑 p 为奇素数的情况。
取 p 的原根 g,则 1,g,g2,g3,⋯gp−1 除以 p 的余数为 1,2,3,⋯p−1。
∴(p−1)!=g0+1+2+⋯+p−2=g(p−2)(p−1)2(modp)
设 p=2k+1,则 (p−1)!=gk(2k−1)(modp)
由费马小定理,∵(gk)2=g2k=gp−1≡1(modp)
∴gk≡±1(modp)
∵k<p
∴gk≡−1(modp)
∴(p−1)!=gk(2k−1)≡(−1)2k−1≡−1(modp)
例题
设 p=3k+7,记 f(k)=[(p−1)!+1p−[(p−1)!p]]
- 若 p 为合数,(p−1) 能被 p 整除,f(k)=0。
- 若 p 为素数,(p−1)!=pq−1,有 f(k)=[pq−1+1p−[pq−1p]]=[q−[q−1p]]=[q−(q−1)]=1
所以 Sn=n∑k=1f(k) 转换为求 1∼n 内的素数个数,线性筛即可。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int MN=4e6+5;
ll ans[MN];
class prime{
public:
ll p[MN],cnt;
bool vis[MN];
prime(){cnt=0;memset(vis,false,sizeof(vis));}
void clear(){cnt=0;memset(vis,false,sizeof(vis));}
void init(){
for(int i=2; i<=MN; i++){
if(!vis[i]) p[++cnt]=i;
for(int j=1; j<=cnt&&i*p[j]<=MN; j++){
vis[i*p[j]]=true;
if(i%p[j]==0) break;
}
}
}
}p;
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
int main(){
p.init();
for(int i=2; i<MN; i++) ans[i]=ans[i-1]+(!p.vis[3*i+7]);
ll T=read();while(T--){ll x=read();write(ans[x]);putchar('\n');}
return 0;
}
笔者因为懒甚至直接把线性筛放到类里复制过来。。。
康托展开
正向康托展开
题意很简单,就是求长度为 n 的数组 a 在全部 n 的排列中按字典序排序的排名。
先那一组数据来手玩:
求 4 2 3 5 1
的排名。
- 第一位是 4,在这这个排列中,还会有以 1,2,3 作为第一位,所以它会增加 3×(5−1)!=72 的排名。
- 第二位是 2,在以
4
为第一位的排列中,还会有 1 作为第二位,所以它会增加 1×(5−2)!=6 的排名。 - 第三位是 3,在以
4 2
为前两位的排列中,还会有 1 作为第三位,所以他会增加 1×(5−3)!=2 的排名。 - 第四位是 5,在以
4 2 3
为前三位的排列中,还会有 1 作为第四位,所以它会增加 1×(5−4)!=1 的排名。 - 第五位无需考虑
所以,该排列的排名为 72+6+2+1+1=82 名,记得最后要加 1,因为我们算的是该排列前面有多少个排列。
形式化的,记 si 为 n∑j=i+1aj<ai,则 a 的排名就为 n∑i=1si×(n−i)!。
如果暴力求这个数组的话是 O(n2),会超时,所以考虑用树状数组优化,就可以达到 O(nlogn),原理和树状数组求逆序对一样。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int mod=998244353;
const int MN=1e6+5;
ll n,a[MN],t[MN],fac[MN]={1},ans;
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
ll lowbit(ll x){return x&-x;}
void change(ll x, ll v){while(x<=n){t[x]=(t[x]+v)%mod;x+=lowbit(x);}}
ll query(ll x){ll res=0;while(x){res=(res+t[x])%mod;x-=lowbit(x);}return res;}
int main(){
n=read();
for(int i=1; i<=n; i++){fac[i]=fac[i-1]*i%mod;change(i,1);}
for(int i=1; i<=n; i++){
a[i]=read();
ans=(ans+(query(a[i]-1)*fac[n-i])%mod)%mod;
change(a[i],-1);
}
write(ans+1);putchar('\n');
return 0;
}
逆向康托展开
把本题的题面对比下正向康托展开发现式子一模一样,所以也就知道这题中的 Si 就是 n∑j=i+1aj<ai,所以问题就转化成了已知每个数后面有几个小于自己的数,求这个序列。
这个很好办,用树状数组加二分,我们可以从前往后一个一个确定 a。
Si 就为比 i 小并且未被确定的数的个数。
在逐步确定 a 过程中,对于每个 i∈{1,2,⋯,n},比 i 小并且未被确定的数的个数满足单调性,于是就可以二分求 ai,然后标上 ai 已被标记(在树状数组中把 ai 位置减一)。
时间复杂度为 O(nlog2n),用线段树上二分可以优化到 O(nlogn),但是笔者太懒(菜)了,只打了树状数组(反正能过就行)。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int MN=5e4+5;
ll n,t[MN],a[MN];
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
ll lowbit(ll x){return x&-x;}
void change(ll x, ll v){while(x<=n){t[x]+=v;x+=lowbit(x);}}
ll query(ll x){ll res=0;while(x){res+=t[x];x-=lowbit(x);}return res;}
ll get(ll x){
ll l=1,r=n+1,res;
while(l<r){
ll mid=l+r>>1,num=query(mid-1);
if(num>x) r=mid;
else l=mid+1;
}
return l-1;
}
void solve(){
for(int i=1; i<=n; i++) t[i]=0;
n=read();
for(int i=1; i<=n; i++) change(i,1);
for(int i=1; i<=n; i++){
ll x=read();
a[i]=get(x);
change(a[i],-1);
}
for(int i=1; i<=n; i++){
write(a[i]);
if(i!=n) putchar(' ');
}putchar('\n');
}
int main(){
ll T=read();while(T--) solve();
return 0;
}
高斯消元
一个线性方程组有 m 个一次方程,n 个变量,把所有的系数携程一个人 m 行 n 列的矩阵,把每个方程等号右侧的常数放在最右列,得到一个 m 行 n+1 的增广矩阵,高斯消元本质上就是通过多次变换把方程组转化成多个一元一次方程,变换有以下三种,称为线性方程组的初等变换。
- 交换某两行的位置。
- 让矩阵中的一行乘上一个非零的常数 k,相当于让某个方程左右两边同时乘以一个非零常数 k。
- 把某行乘以 k,k≠0 后加到另一行上,相当于让一个方程加上另一个方程的 k 倍。
这里给出三个例子,顺便介绍线性方程组的解的 3 种情况:有唯一解,有无穷多解,无解。
- 有唯一解
{3x1+7x2−5x3=47x1+4x2+x3=588x1−3x2+9x3=88→[37−51418−39|475888]
首先先把左边的方程组写成右边的增广矩阵,然后反复使用初等变换,得(这一段摘抄自算法竞赛,我也不知道这消元能这么丝滑)
[37−51418−39|475888]→[37−5141035−1|4758376]→[37−5058035−1|47127376]→[37−50580057|47127513]→[37−5058001|471279]→[37−5050001|47559]→[37−5010001|47119]→[100010001|5119]
最后解得 {x1=5x2=11x3=9,这是唯一解,称最后的矩阵为 简化阶梯矩阵,特征是左半边是个 单位矩阵。
- 有无穷多个解
{3x1+7x2−5x3=47x1+4x2+x3=582x1+3x2−6x3=−11→[37−514123−6|4758−11]→[37−5058000|471270]
最后的矩阵出现了一个全 0 的行,说明这一行无效。3 个未知数只有两个方程,此时有无穷多个解。
- 无解
{3x1+7x2−5x3=47x1+4x2+x3=582x1+3x2−6x3=5→[37−514123−6|47585]→[37−5058000|4712716]
最后的矩阵出现了一个 0=16 的矛盾行,所以无解。
高斯-约当消元法
可以发现上面的式子很难一眼看出来是怎么推出来的(包括我也是摘抄的),所以,这边给出一种简单消元方法。
消元过程如下:
- 从第 1 列开始,选择一个非 0 的系数(代码中的实现是选最大的系数,避免转换其他系数时产生过大的数值)所在的行,把这一行移动到第 1 行,称此时第一行的 x1 为主元(参考下面转换中第一个矩阵到第二个矩阵的变换)
- 把 x1 的系数转换为 1,参考下面转换的第二个矩阵到第三个矩阵。
- 利用主元 x1 的系数,把其他行的这一列的主元消去,参考下面转换的第三个矩阵到第四个矩阵。
- 重复以上步骤,直到可以判断线性方程组的
[37−51418−39|475888]→[8−3914137−5|885847]→[1−0.381.1214137−5|115847]→[1−0.381.1204.380.1208.12−8.38|114714]→⋯→[100010001|5119]
消元过程中的除法可能会产生精度问题,所以我们设置一个很小的 eps,小于 eps 数则令其为 0,代码中有 3 层 for 循环,复杂度为 O(n3)。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
#define ld long double
using namespace std;
const ld eps=1e-6;
const int MN=105;
ll n,p;
ld a[MN][MN];
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
int main(){
n=read();
for(int i=1; i<=n; i++) for(int j=1; j<=n+1; j++) scanf("%Lf",&a[i][j]);
for(int i=1; i<=n; i++){
p=i;
for(int j=i+1; j<=n; j++) if(fabs(a[j][i])>fabs(a[p][i])) p=j;
for(int j=1; j<=n+1; j++) swap(a[i][j],a[p][j]);
if(fabs(a[i][i])<eps){printf("No Solution\n");return 0;}
for(int j=n+1; j; j--) a[i][j]=(ld)a[i][j]/a[i][i];
for(int j=1; j<=n; j++) if(j!=i){
double tmp=(ld)a[j][i]/a[i][i];
for(int k=1; k<=n+1; k++) a[j][k]-=a[i][k]*tmp;
}
}
for(int i=1; i<=n; i++) printf("%.2Lf\n",a[i][n+1]);
return 0;
}
跟原来的差距是我们记录了有几个方程式可以找到主元,要是能找到 n 个(即 tot=n+1)那么说明这个线性方程组有解,否则在找不到主元的方程中要是有一个方程等号右边不等于零,那么这个方程组无解,否则,它有无数个解。
实现细节:记得要把除数为 0(或者说除数无限接近于 0)的判掉。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const double eps=1e-8;
const int MN=105;
ll n,p,tot=1;
double a[MN][MN];
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
int main(){
n=read();
for(int i=1; i<=n; i++) for(int j=1; j<=n+1; j++) scanf("%lf",&a[i][j]);
for(int i=1; i<=n; i++){
p=tot;
for(int j=tot+1; j<=n; j++) if(fabs(a[j][i])>fabs(a[p][i])) p=j;
if(fabs(a[p][i])<eps) continue;
for(int j=1; j<=n+1; j++) swap(a[tot][j],a[p][j]);
for(int j=n+1; j; j--) if(!(fabs(a[tot][i])<eps)) a[i][j]=(double)a[i][j]/a[tot][i];
for(int j=1; j<=n; j++) if(j!=tot&&!(fabs(a[tot][i])<eps)){
double tmp=(double)a[j][i]/a[tot][i];
for(int k=1; k<=n+1; k++) a[j][k]-=a[tot][k]*tmp;
}
tot++;
}
if(tot==n+1) for(int i=1; i<=n; i++) printf("x%d=%.2lf\n",i,a[i][n+1]);
else{
while(tot<=n) if(!(fabs(a[tot++][n+1])<eps)){write(-1);putchar('\n');return 0;}
write(0);putchar('\n');
}
return 0;
}
矩阵求逆
既然都讲了高斯消元,怎么能不讲矩阵求逆呢?(bushi)
矩阵求逆由于讲课人能力有限,所以我就不给出证明了(我也不会)。
这边给出做法:
- 求 A 的逆矩阵,把 A 和单位矩阵 I 放在一个矩阵里。
- 对 A 进行加减消元使 A 化成单位矩阵。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int mod=1e9+7;
const int MN=405;
ll n,a[MN][MN<<1];
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
ll ksm(ll a, ll b){ll res=1;while(b){if(b&1)res=res*a%mod;a=a*a%mod;b>>=1;}return res;}
int main(){
n=read();
for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) a[i][j]=read();
for(int i=1; i<=n; i++) a[i][i+n]=1;
for(int i=1; i<=n; i++){
ll p=i;
for(int j=i+1; j<=n; j++) if(a[p][i]<a[j][i]) p=j;
if(i!=p) swap(a[i],a[p]);
if(!a[i][i]){printf("No Solution\n");return 0;}
ll ny=ksm(a[i][i],mod-2);
for(int j=1; j<=n; j++) if(i!=j){
ll num=a[j][i]*ny%mod;
for(int k=i; k<=(n<<1); k++) a[j][k]=((a[j][k]-num*a[i][k]%mod)%mod+mod)%mod;
}
for(int j=1; j<=(n<<1); j++) a[i][j]=a[i][j]*ny%mod;
}
for(int i=1; i<=n; i++){for(int j=n+1; j<=(n<<1); j++) write(a[i][j]),putchar(' ');putchar('\n');}
return 0;
}
P4035
求一个点 (x1,x2,x3,⋯,xn) 使得 n+1∑j=1(ai,j−xj)2=C
但是细心发现有 n+1 个方程,考虑相邻相减,然后正好消去常数 C。
于是 n∑j=1(a2i,j−a2i+1,j−2xj(ai,j−ai+1,j))=0
移项,n∑j=1(a2i,j−a2i+1,j)=n∑j=12xj(ai,j−ai+1,j)=0
于是可以高斯消元,增广矩阵:
[2(a1,1−a2,1)2(a1,2−a2,2)⋯2(a1,n−a2,n)n∑j=1(a21,j−a22,j)2(a2,1−a3,1)2(a2,2−a3,2)⋯2(a2,n−a3,n)n∑j=1(a22,j−a23,j)⋯2(an,1−an+1,1)2(an,2−an+1,2)⋯2(an,n−an+1,n)n∑j=1(a2n,j−a2n+1,j)]
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int MN=15;
ll n;
double m[MN][MN],a[MN][MN];
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1; i<=n+1; i++) for(int j=1; j<=n; j++) cin>>a[i][j];
for(int i=1; i<=n; i++) for(int j=1; j<=n; j++){
m[i][j]=(a[i][j]-a[i+1][j])*2;
m[i][n+1]+=a[i][j]*a[i][j]-a[i+1][j]*a[i+1][j];
}
for(int i=1; i<=n; i++){
ll p=i;
for(int j=i+1; j<=n; j++) if(fabs(m[j][i])>fabs(m[p][i])) p=j;
for(int j=1; j<=n+1; j++) swap(m[i][j],m[p][j]);
for(int j=n+1; j; j--) m[i][j]=(double)m[i][j]/m[i][i];
for(int j=1; j<=n; j++) if(j!=i){
double tmp=(double)m[j][i]/m[i][i];
for(int k=1; k<=n+1; k++) m[j][k]-=m[i][k]*tmp;
}
}
for(int i=1; i<=n; i++) printf("%.3lf ",m[i][n+1]);
return 0;
}//250212
例题
容斥原理
德摩根(De Morgan)定理
别看这个定理看得高大尚,其实画个图就可以理解
- ¯A∪B=¯A∩¯B
- ¯A∩B=¯A∪¯B
严格的证明如下,只给第一个,第二个同理。
证明:设 x∈¯A∪B,则 x∉A∪B。
x∉A∪B 等价于 x∉A 和 x∉B 同时成立,所以有
反之,x∈¯A∪¯B,即 x∈¯A 同时 x∈¯B,也就是 x∉A 同时 x∉B,即 x∉A∪B,所以有 x∈¯A∩¯B 的必要条件为 x∈¯A∪B。
故 x∈¯A∩¯B 的充要条件为 x∈¯A∪B。
∴¯A∪B=¯A∩¯B
这个定理还可以推广到一般:
- ¯A1∪A2∪⋯∪An=¯A1∩¯A2∩⋯∩¯An
- ¯A1∩A2∩⋯∩An=¯A1∪¯A2∪⋯∪¯An
这两个可以用数学归纳法,这里一第一个证明为例,第二个同理。
证明:n=2 的时候已经证明成立,这里假定 ¯A1∪A2∪⋯∪An=¯A1∩¯A2∩⋯∩¯An 成立,有
故,定理对 n+1 为真。
容斥原理
根据加法法则,若 A∩B=∅,则 |A∪B|=|A|+|B|,若 A∩B≠∅ 是,这是会将 |A∩B| 多算一次,所以由上面的图直观地得到:
再给出一张图:
于是我们有:|A∪B∪C|=|A|+|B|+|C|−|A∩B|−|A∩C|−|B∩C|+|A∩B∩C|。
证明:
|A∪B∪C|=|(A∪B)∪C|=|A∪B|+|C|−|(A∪B)∩C|
又 ∵(A∪B)∩C=(A∩C)∪(B∩C),|A∪B|=|A|+|B|−|A∩B|
∴|(A∩C)∪(B∩C)|=|A∩C|+|B∩C|−|(A∩C)∩(B∩C)|
∴|A∪B∪C|=|A|+|B|−|A∩B|−|(A∩C)∪(B∩C)|+|C|=|A|+|B|−|A∩B|−|A∩C|−|B∩C|+|(A∩C)∩(B∩C)|+|C|=|A|+|B|+|C|−|A∩B|−|A∩C|−|B∩C|+|A∩B∩C|
证毕。
接下来我们可以推广下,得到真正的容斥定理:
假设 A1,A2,⋯,An 是 n 个有限集合,则有:
|A1∪A2∪⋯∪An|=(|A1|+|A2|+⋯+|An|)−(|A1∩A2|+|A1∩A3|+⋯+|A1∩An|+|A2∩A3|+⋯+|An−1∩An|)+(|A1∩A2∩A3|+⋯+|An−2∩An−1∩An|)⋯+(−1)n−1|A1∩A2∩⋯∩An|=n∑i=1|Ai|−n∑i=1∑j>i|Ai∩Aj|+n∑i=1∑j>i∑k>j|Ai∩Aj∩Ak|+⋯+(−1)n−1|A1∩A2∩⋯∩An|
证明:还是用数学归纳法证明。
当 n=2 时,显然成立。
假定 n−1 时正确,即
|A1∪A2∪⋯∪An−1|=n−1∑i=1|Ai|−n−1∑i=1∑j>i|Ai∩Aj|+n−1∑i=1∑j>i∑k>j|Ai∩Aj∩Ak|+⋯+(−1)n−2|A1∩A2∩⋯∩An−1|
∴|A1∪A2∪⋯∪An|=|(A1∪A2∪⋯∪An−1)∪An|=|A1∪A2∪⋯∪An−1|+|An|−|(A1∪A2∪⋯∪An−1)∩An|=n−1∑i=1|Ai|−n−1∑i=1∑j>i|Ai∩Aj|+n−1∑i=1∑j>i∑k>j|Ai∩Aj∩Ak|+⋯+(−1)n−2|A1∩A2∩⋯∩An−1|+|An|−|(A1∪A2∪⋯∪An−1)∩An|
又 ∵(A1∪A2∪⋯∪An−1)∩An=(A1∩An)∪(A2∩An)∪⋯∪(An−1∩An)
∴|(A1∪A2∪⋯∪An−1)∩An|=|(A1∩An)∪(A2∩An)∪⋯∪(An−1∩An)|=|A1∪An|+|A2∪An|+⋯+|An−1∪An|−|A1∩A2∩An|−|A1∩A3∩An|−⋯−|An−2∩An−1∩An|+⋯+(−1)n−2|A1∩A2∩⋯∩An|=n−1∑i−1|Ai∩An|−n−1∑i=1∑j>i|Ai∩Aj∩An|+⋯+(−1)n−2|A1∩A2∩⋯∩An|
∴|A1∪A2∪⋯∪An|=n∑i=1|Ai|−n∑i=1∑j>i|Ai∩Aj|+n∑i=1∑j>i∑k>j|Ai∩Aj∩Ak|+⋯+(−1)n−1|A1∩A2∩⋯∩An|
容斥原理要记住这些:
(1) 集合 S 中不具有性质 P1,P2,⋯,Pn 的对象个数为
|¯A1∩¯A2∩⋯∩¯An|=|S|−n∑i=1|Ai|+n∑i=1∑j>i|Ai∩Aj|−n∑i=1∑j>i∑k>j|Ai∩Aj∩Ak|+⋯+(−1)n−1|A1∩A2∩⋯∩An|
简记(我的方法):奇减偶加
(2) 集合 S 中至少具有性质 P1,P2,⋯,Pn 之一的对象个数为
|A1∪A2∪⋯∪An|=n∑i=1|Ai|−n∑i=1∑j>i|Ai∩Aj|+n∑i=1∑j>i∑k>j|Ai∩Aj∩Ak|+⋯+(−1)n−1|A1∩A2∩⋯∩An|
简记:奇加偶减
数学例题
【解析 1】简单容斥原理
首先,100 以内能被 2,3,5 中整除的数有:
- |A1|=⌊1002⌋=50
- |A2|=⌊1003⌋=33
- |A3|=⌊1005⌋=20
- |A1∩A2|=⌊100lcm(2,3)⌋=16
- |A1∩A3|=⌊100lcm(2,5)⌋=10
- |A2∩A3|=⌊100lcm(3,5)⌋=6
- |A1∩A2∩A3|=⌊100lcm(2,3,5)⌋=3
故答案为 3∑i=1|Ai|−3∑i=1∑j>i|Ai∩Aj|+|A1∩A2∩A3|=50+33+20−16−10−6+3=74。
【解析 2】简单容斥原理+排列组合
- S 的全排列个数 |S| 为 6!。
- S 中包含
ace
的排列个数 |A1| 为 4!。(捆绑法,把ace
看成一个整体) - S 中包含
bf
的排列个数 |A2| 为 5!。 - S 中既包含
ace
又包含bf
的排列个数 |A1∩A2| 为 3!。
所以,这 6 个字符构成的全排列中不允许出现 ace
和 bf
的排列数为:
例题
这题看起来像多重背包,但是即使是二进制优化也无法过去。
这题的正解是:完全背包+容斥原理~~~(鼓掌)
其实本题就是求解这个问题:∑[n∑i=1cixi=s,xi⩽di]
其中 xi 表示第 i 种硬币的数量,求 x 的解有多少个。
但是正着求需要用单调队列优化多重背包,并不是今天的主题,这里不过多提出。
既然不能正这做,那就考虑反着做,没有限制的方案数是好求的,不合法的方案数也是好求的,考虑容斥。
如果没有 di 的限制,那么这题就是一个完全背包,dpj 表示支付钱数为 j 时的支付方案数,则有:
对于不满足要求的方案则是这么一个问题:
因为我们要求的是不合法的方案数,所以我们只要知道第 i 个硬币要不要取(opi←0 或 opi←1),然后取出 di+1 个第 i 个硬币,那么这种取法不管剩余硬币怎么取,它一定就是不合法的,在此基础上完全背包即可。
变形一下得 ∑[n∑i=1cixi=s−n∑i=1(opi≠0)×ci⋅(di+1)]
枚举那几个硬币是要选的(用二进制枚举第 i 个硬币选不选),然后去转移,最后这种状态下不合法方案个数就是 dp[s−n∑i=1(opi≠0)×ci⋅(di+1)],注意,如果这后面一大串已经小于 0,说明你支付的东西已经大于给定的支付面额,不能考虑进去。
这题中,我们求的是合法的方案数,合法,顾名思义,它不满足任何一个不合法的条件,所以,我们用上面所说的奇减偶加。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int MN=100005;
ll dp[MN]={1},c[5],d[5],T,sum,res;
void write(ll n){if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
int main(){
for(int i=1; i<=4; i++) c[i]=read();
for(int i=1; i<=4; i++) for(int j=c[i]; j<MN; j++) dp[j]+=dp[j-c[i]];
T=read();while(T--){
res=0;for(int i=1; i<=4; i++) d[i]=read();sum=read();
for(int i=0; i<=15; i++){
ll num=sum,cnt=0;
for(int j=1; j<=4; j++) if((i>>(j-1))&1) num-=c[j]*(d[j]+1),cnt^=1;
if(num<0) continue;
res+=!cnt?dp[num]:-dp[num];
}
printf("%lld\n",res);
}
return 0;
}
例题
概率和期望
概率
我们通常把对随机现象的研究叫做随机试验。
称随机试验每一个可能的结果为样本点,记作 ω;所有样本点所组成的集合称为样本空间,记作 Ω。
对于样本空间 Ω,称它的任意子集为随机事件,我们可以用大写字母 A,B,C,⋯ 表示随机事件。
当随机事件中包含的样本点出现了,则这个随机事件就发生。
若样本点个数有限,记一个随机事件 A 的样本点个数为 n(A)。
由于事件是样本点组成的,我们可以把它们看作是集合。
概率是对随机事件发生的可能性的度量。
我们用一个 [0,1] 中的实数表示概率,越接近 1 越可能发生,反之越不可能。
对于事件 A ,我们记其发生的概率为 P(A)。
显然有 P(Ω)=1,P(∅)=0。
古典概型是一类概率模型,满足样本空间有限,且每种情况出现概率相
等。通常在 OI 中我们碰到的都是古典概型。
在古典概型中,设研究事件 A 的样本点个数 n(A)=K,同理 n(Ω)=N,则
(形象点,N 个事件中有 K 件发生了 A 事件,称发生 A 事件的概率为 KN)
例如,记事件 A 摇骰子摇到 ⩽2 的数,则 A={1,2},n(A)=2;
Ω={1,2,3,4,5,6},n(Ω)=6,P(A)=13。
由此,类似集合之间关系,我们有事件之间的关系。
事件之间的关系
- 并事件(和事件):事件 A 与 B 中至少发生了一个,这样的事件为
A 与 B 的并事件(和事件),记作 A∪B 或 A+B。 - 交事件(积事件):事件A 与 B 同时发生,这样的事件为 A 与 B
的交事件(积事件),记作 A∩B 或 AB。 - 互斥事件:两个事件 A,B 不能同时发生,即 A∩B=∅(∅ 表示空集)则事件 A,B 互斥。
- 对立事件:A 与 B 有且仅有一个发生,A∩B=∅ 且 A∪B=Ω,则事件 A,B 互为对立,记 A 的对立事件为 A。
- 独立事件:定义满足 P(AB)=P(A)P(B) 的事件 A,B 互相独立,即互不影响。
概率的性质
- 对于事件 A,B,若 A,B 互为对立事件,则有 P(A)+P(B)=1。
- 对于事件 A,B,若事件互相独立,则同时发生的概率:P(AB)=P(A)×P(B)。
- 对于事件 A,B,则至少一个发生的概率:P(A+B)=P(A)+P(B)−P(AB)(见前面的容斥。特别地,当 A,B 互斥时,P(AB)=0,即 P(A+B)=P(A)+P(B)。
例题
【解析 1】
记 A 为“存在两个同学生日相同”
注意到“存在两人生日相同”和“所有人生日不同”是对立事件,而 A 不好求,就正难则反,考虑求 ¯A,即所有人生日不同的概率。
n(¯A):一个一个确定,第一个人有 365 种可能,后面每个人比前面少 1(不能重复),即 A50365。
P(¯A)=A5036536550≈0.03
所以 P(A)≈0.97。
可以发现其实概率非常大。
期望
通俗的说,随机变量指随机试验各种结果所表示的值。
在数学中,一个随机变量 X 的数学期望 E(X) 为试验中每次可能的结果乘以其概率的和。
形式化地讲,
其中 xi 表示事件的一种取值,pi 即与之对应的概率。
举个例子:投六面骰子投出的点数的期望为:
E(X)=1×16+2×16+3×16+4×16+5×16+6×16=3.5
对于任意随机变量 X,Y 我们有:
即期望的可加性(线性性)。证明略。
例题
【解析 2】
考虑设 E(Xi) 表示第 i 个车站停车次数的期望值,停车为 1,否则为 0,则只要计算 E(X)=k∑i=1E(Xi)。
对于每个车站,每个人不在这个车站下车的可能性为 k−1k,则所有人都不在这里下车的概率为 (k−1k)n,同时每站期望停车次数等于停车概率。则有:
根据期望的可加性:
E(X)=k∑i=1E(Xi)=k×[1−(k−1k)n]
期望的其他性质
设随机变量 X 的取值仅可能是 1∼n 的整数,则有下式成立:
证明:
通常,设事件 A 发生概率为 P(A)(P(A)≠0),则不断试验直到 A 发生,期望次数为 E(X)=1P(A)。
例题
很显然直接 dp,设 dpi,j 表示前 i 个硬币有 j 个是朝上的概率。
很基础的转移,首先我们知道两个硬币的正反情况的是互相独立的事件,所以我们有:
- dpi,j←imaxj=0{dpi−1,j−1×p[i]}
- dpi,j←max(dpi,j,dpi−1,j×(1−p[i]))
实现细节:注意判断边界。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int MN=3e3+5;
ll n;
double p[MN],dp[MN][MN],ans;
void write(ll n){if(n<0){putchar('-');write(-n);return;}if(n>9)write(n/10);putchar(n%10+'0');}
ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
int main(){
n=read();dp[0][0]=1;
for(int i=1; i<=n; i++) scanf("%lf",&p[i]);
for(int i=1; i<=n; i++) for(int j=0; j<=i; j++){
if(j!=0) dp[i][j]+=dp[i-1][j-1]*p[i];
if(j!=i) dp[i][j]+=dp[i-1][j]*(1-p[i]);
}
for(int i=n/2+1; i<=n; i++) ans+=dp[n][i];
printf("%.12lf\n",ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现