翻译网页
点击停止翻译
收起

五一集训笔记 密码保护

参考文献

模运算

这个笔记所有的 % 均表示模运算。

带余除法

考虑 a÷b=c...d

  • a=bc+d
  • 0d<b
  • c=a/bd=a%b
  • (a+b)%c=a%c+b%c,减法、乘法同理。
    • 证明:设 a=k1c+a,b=k2c+b,则左式和右式均可以化为 (a+b)%c

新手题

1

给定 a,b,c,求 a+b,ab,a×b % c 的值。保证 a,b,cint 范围内。

  • 标准错误写法:
#include<iostream>
#define ll long long
using namespace std;
int a,b,c; 
int main(){
	cin>>a>>b>>c;
	cout<<(a+b)%c<<endl;
	cout<<(a-b)%c<<endl;\\(1)
	cout<<(a*b)%c<<endl;\\(2)
	return 0;
}
  • 其中 (1)ab 可能小于 0,于是又一种错误的改法出现了:cout<<abs(a-b)%c<<endl;
    • 这么一改,直接把数给变了,答案能对吗??!
    • 所以正确的改法为:cout<<((a-b)%c+c)%c<<endl;
  • (2) 处可能会炸 int,这里可以开 long long,也可以先乘上 1ll,还可以直接用上面的规律。

2

给定 n,p,求 n!%pn1000

  • 又一个标准错误写法:
#include<iostream>
#define ll long long
using namespace std;
ll n,p,ans=1; 
int main(){
	cin>>n>>p;
	for(int i=1;i<=n;i++){
		ans*=i;
	}
	cout<<ans%p; 
	return 0;
}
  • 此处同上面的 (2),也会炸 int,而且会炸 long long。于是就只能用规律了。正确代码如下:
#include<iostream>
#define ll long long
using namespace std;
ll n,p,ans=1; 
int main(){
	cin>>n>>p;
	for(int i=1;i<=n;i++){
		ans*=i%p;
		ans%=p;
	}
	cout<<ans; 
	return 0;
}

gcdlcm

定义:gcd 就是最大公约数,lcm 是最小公倍数。

注:gcd(a,0)=a

整除符号

b|a 表示 a%b=0

gcd 的性质

gcd(a,b)=gcd(a%b,b)

证明:

gcd(a,b)
=gcd(ab,b)
=gcd(a2b,b)
...
=gcd(a%b,b)

于是就有了:

辗转相除法求 gcd

使用性质,不断把 gcd(a,b) 变成 gcd(a%b,b)ab)即可。

代码:

int gcd(int a,int b){
	if(b==0)return a;
	return gcd(b,a%b);
}

时间复杂度O(loga)

  • 原因很简单:
    • ba<2b 时,a%b=ab,显然小于 a2
    • 2ba 时,因为余数小于除数,而除数又不超过a2,所以 a%b<a2
  • 综上,a%b<a2,所以时间复杂度不超过 log

求多个数的 gcd 复杂度O(nlogmax(ai))

lcm 的性质与求法

性质:如果 gcd(a,b)=g,lcm(a,b)=l,则 ab=gl

所以求法:lcm(a,b)=a×b÷gcd(a,b)

但是 ab 容易炸,所以要用 lcm(a,b)=a÷gcd(a,b)×b

快速幂

提交的题目

给定 a,n,p,求 an%pa,b,p231

首先暴力肯定是不行的,n 的复杂度直接升天。

于是我们考虑拆开。例如:

x37=(x18)2+x=((x9)2)2+x=(((x4)2+x)2)2+x=((((x2)2)2+x)2)2+x=((((x×x)2)2+x)2)2+x

于是一波地龟就完事了。代码:

ll ksm(ll x,ll y,ll p){
//x^y%p
	if(y==0)return 1;
	ll z=ksm(x,y/2,p);
	z=1ll*z*z%p;
	if(y%2)z=z*x%p;
	return z;
}

这里我们也可以用相同的思想求大数加法。代码略。

矩阵

定义

一个 nm 列的二维数组。

运算

加减法:对应数字加减。

[1234]+[5678]=[1+52+63+74+8]=[681012]

矩阵乘数字:所有的数字全部乘。

[1234]×6=[1×62×63×64×6]=[6121824]

矩阵乘矩阵

要求第一个矩阵的列数等于第二个矩阵的行数。结果的矩阵行数为第一个的行数,列数为第二个的列数。

算第 i 行第 j 列的数时,取第一个矩阵的第 i 行和第二个矩阵的第 j 列,将对应位置的数字相乘后相加即可。

[1234]×[114514]=[1×1+2×51×1+2×11×4+2×43×1+4×53×1+4×13×4+4×4]=[1131223728]

写法

加减法及数乘略。

乘法:这里采用结构体和重载来写。

结构体

struct matrix{
	ll n,m;
	ll a[233][233];
	matrix(){//(1)
		n=m=0;
		memset(a,0,sizeof(a));
	}
};
matrix m1,m2,m3;

代码中 (1) 处是构造函数,无需调用,定义结构体就会自动运行。

重载运算符

matrix operator*(matrix &m1,matrix &m2){
	matrix m3;
	m3.n=m1.n;m3.m=m2.m;
	for(int i=0;i<m3.n;i++){
		for(int j=0;j<m3.m;j++){
			for(int k=0;k<m1.m;k++){
				m3.a[i][j]+=m1.a[i][k]*m2.a[k][j];
			}
		}
	}
	return m3;
}

但此时我们会发现两个 matrix 如果改动的话,就会寄掉,然后被zhx笑一坤年,因此我们需要加个 const,于是就成了:

matrix operator*(const matrix &m1,const matrix &m2){
	matrix m3;
	m3.n=m1.n;m3.m=m2.m;
	for(int i=0;i<m3.n;i++){
		for(int j=0;j<m3.m;j++){
			for(int k=0;k<m1.m;k++){
				m3.a[i][j]+=m1.a[i][k]*m2.a[k][j];
			}
		}
	}
	return m3;
}

加个输入,就大功告成了。

矩阵快速幂

引入——斐波那契

首先都知道斐波那契数列的递推式:fn=fn1+fn2

于是我们准备两个矩阵:

A=[fn1fn2],B=[1110]

于是……

A×B=[fnfn1]

可这除了更慢,有什么用呢?

我们知道开始的 A1=[10],设结果为 C,则 Cn=[fnfn1]=A1×B×B×...×Bn-1 个 B

你想到了……

乘法有快速幂,那矩阵乘法呢?

我们只需要把两个板子塞到一个代码里,变成这样:

struct matrix{
	ll n,m;
	ll a[2][2];
	matrix(){//(1)
		n=m=0;
		memset(a,0,sizeof(a));
	}
};
ll n,p;
matrix m1,m2,tmp,ans;
matrix operator*(const matrix &m1,const matrix &m2){
	matrix m3;
	m3.n=m1.n;m3.m=m2.m;
	for(int i=0;i<m3.n;i++){
		for(int j=0;j<m3.m;j++){
			for(int k=0;k<m1.m;k++){
				m3.a[i][j]+=m1.a[i][k]*m2.a[k][j];
				m3.a[i][j]%=p;
			}
		}
	}
	return m3;
}
matrix ksm(matrix x,ll y){
	if(y==0)return tmp;
	matrix z=ksm(x,y/2);
	z=z*z;
	if(y%2)z=z*x;
	return z;
}

然后你就过了一黄一绿。

例题

口胡题:走 k 步到点 n,求方案数。n100,k109

zi,j 记录 i,j 间有没有边, 10 没有。fi,j 表示走了 i 步到 j 的方案数。先将走 0 步到的点全飙为 01<inf0,i=0f0,1=1,转移时 fi,j=fi1,p1+fi1,p2+... 其中 p 是一步能到的点的编号。这样复杂度是 O(n2k) 的,直接超时。

我们考虑加一个维度 d,让其始等于 1。将 fi 看成一个变量则原式等于 fi[d][j]=fi1[i1][p]×z[p][j]

于是 fi=fi1×z=f0×zi 然后题目就变成了矩阵快速幂。

P1962 斐波那契数列

P3390 【模板】矩阵快速幂

这两个题就是上面说的“一黄一绿”。

P4159 [SCOI2009] 迷路

我们考虑把这个题变成刚才的口胡题,也就是把带权的图,变成一个边权均为 1 的图。但这样的话边数将会超级多,于是又双叒叕超时了。

考虑合并边:

然后就变成了刚才的口胡题。

P1349 广义斐波那契数列

数论基础

素数的判定

不说了,很简单。重点在为什么可以循环到 x:因为如果一个因数大于 x,那 x这个因数<x

常见死法:

for(int i=2;i<=sqrt(x);i++)

死因很明显:每跑一次都要算一次 x,于是复杂度无端乘了个 logx。但这玩意一般不致命。一个关于字符串的更加常见也更加致命的死法是:

for(int i=1;i<=strlen(s);i++)

我求 |s||s|,直接变成 |s|2,T 飞了。

逆元

PAY ATTENTION

只有 a,p 互质,才有逆元。——张家瑞

引入——取模

(3+4)%5=2

(34)%5=1+5=4

(3×4)%5=2

(3÷4)%5=???

对于 (a÷b),我们要找到一个数 c,使得原式 =ac,此时 c 就是 b 的逆元。

费马小定理

内容:当 p 是质数,gcd(a,p)=1 时有 ap11(modp)

证明

  • 两边同除以 a 得到 ap21a(modp)
  • 于是原来的 (a÷b) 便成了 abp2%p。一波快速幂就结束了。

欧拉定理

内容:当 gcd(a,p)=1 时有 aϕ(p)1(modp)

证明

  • 将费马小定理进行拓展,现在没有 n 是质数,只有 a,n 互质。

欧拉函数 ϕ

ϕ(p)[1,p] 中与 p 互质的数的个数。定义式为 ϕ(n)=i=1n[gcd(n,i)=1]

计算式:

  • ϕ(n)=npi1pi
    • 如果 n1 个质因子:
      • n=p1ϕ(n)=p11
      • n=p12 时所有 p1 的倍数全都不互质,即 ϕ(n)=p12p11
      • 以此类推,当 n=p1k 时,ϕ(n)=n·p11p1
    • 如果 n2 个质因子:
      • 一波容斥,ϕ(n)=nnp1np2+np1p2=n(11p1)(11p2)
    • 从上面的规律中可以推出计算式。

线性求逆(I)

  1. 算出 [1,n]!
  2. 再求出每一个阶乘的逆元 ifaci=ifaci+1×(i+1)%p
  3. 最后求本数的逆元 invi=faci1×ifaci%p 即可。

咕咕咕

线性求逆(II)

咕咕咕

Exgcd

求解方程 ax+by=c。显然有解的条件为 cgcd(a,b) 的倍数。

两边同除以 gcd(a,b) 得到一个方程 ax+by=c。因为 c 可能是任意整数,所以我们要证 ax+by 也能取遍任意整数。又因为 gcd(a,b)=1cgcd(a,b) 的倍数(也就是 c 可以取任意整数),所以显然成立。

Miller-Rabin 素性测试

靠脸吃饭的素数判定算法。

如果 n 为素数,取 a<n,设 n1=d×2r,则要么 ad1(modn),要么存在 0i<r 满足 s·t·ad×2i

只需要找几十个数字,挨个跑一遍判定就可以了。如果跑 k 个复杂度就是 klogn。代码如下:

int n;
const int NUM=50;
int ksm(int a,int b,int mod){
    if(b==1)return a;
    int t=ksm(a,b/2);
    if(b%2==1)return ((t*t)%mod*a)%mod;
    else return (t*t)%mod;
}
bool Miller_Rabin(int n,int a){
    int d=n-1,r=0;
    while(d%2==0)d/=2,r++;
    int t=ksm(a,d,n);
    if(t==1)return 1;
    for(int i=0;i<r;i++){
        if(t==n-1)return 1;
        t=(t*t)%n;
    }
    return 0;
}
bool check(int n){
    if(n<2)return 0;
    for(int i=1;i<=NUM;i++){
        int a=rand()%(n-1)+1;
        if(!Miller_Rabin(n,a))return 0;
    }
    return 1;
}

中国剩余定理

引入——韩信点兵

韩信点兵,三三数之余一,五五数之亦余一,七七数之仍余一,问兵几何?

做法

考虑下面的方程组:

{x%a1=m1x%a2=m2...x%an=mn

  • 我们要解的话,有一种很简单的方法:所有的两两分组解一下;解完之后的再分组解一次,直到就剩一个方程了。于是问题转化为解两个同余方程。
    • 枚举:a1,a1+m1,a1+2m1,...,a1+p1p2,直到枚举的值满足第二个方程。最多枚举 p2 次(没解)。
    • 卡常大妙招:当 p1<p2 时交换两个方程,于是这种方法就有个名字:大数翻倍法
    • 时间复杂度:O(min(p1,p2))
      • 但值得注意的一点是:答案一般不会炸 long long(1018)
      • 但是答案是所有 plcm。而为了好做,一般 p 都是互质的
      • 这意味着,Πpi1018,pi109,也就是 n 必须小于 3
      • 所以你是死不掉的。
      • 于是你就过了一个蓝题(注:由于数据水,下文提及的紫题也能过,但其正常情况下显然不对)。
    • 代码:

  • 正统做法
    • 这个做法可以过紫题

筛法

引入——那次模拟赛

一个夜晚的模拟赛中,你看到了一个题目:

给定一个 n,请找出 [1,n] 中的所有质数。

你的脑中浮现了两个思路:

  • 暴力:复杂度是 nn 的。
  • 想到之前学的 Miller-Rabin,复杂度提升到了 nlogn

你往下滚动着页面,直到看到……

数据规模与约定
对于 20% 的数据,1n104
对于 50% 的数据,1n105
对于 80% 的数据,1n2×106
对于 100% 的数据,1n107

“这么厉害的 Miller-Rabin,居然才能得 50 分??!”你不由得喊了出来,“这出题人是喝 DDV 了吗?”

于是,你打开了百度……

埃氏筛

显然对于每一个质数,其倍数全都不是质数。所以我们考虑开一个 bool 数组 aai 表示 i 是不是质数。只要还没被标记的就是质数(因为它不是比它小的所有质数的倍数),比它大的所有倍数全部应标记为非质数。时间复杂度是 O(nloglogn)

“啊?这都只有 80 分?这题想满分到底多难?”

欧拉(线性)筛

埃氏筛的 loglogn 是哪里来的呢?能优化掉吗?

很显然,尽管是埃氏筛,其还是有可能出现一个数被筛多次的情况,例如 210=2×3×5×7。埃氏筛的 loglogn 就来自这里。所以优化的核心就是让每个数字只会被其最小的因数筛一次。

核心代码:

for(int i=2;i<n;i++){
	if(not_prime[i]==false){
		cnt++;
		prime[cnt]=i;
	}
	for(int j=1;j<=cnt;j++){
		int x=prime[j]*i;
		if(x>n)break;
		not_prime[x]=1;
		if(i%prime[j]==0)break;
	}
}

你长舒了一口气。这 100 分真是来之不易啊。你望着排名,得意的笑了。

全剧终。

拓展:积性函数和完全积性函数

定义

积性函数:当 gcd(a,b)=1f(a)=f(b) 的函数。

完全积性函数:对于定义域内任意 a,b 均满足 f(a)=f(b) 的函数。

为什么 ϕ 是积性函数?

证法一:由计算式即可得到。这里不再赘述。

证法二

如何求一组连续的 ϕ

咕咕咕

Baby Step Giant Step(也叫 BSGS、北上广深、拔山盖世、小步大步)算法

引入——小破题

给定 a,b,p,其中 p 为质数,解方程 axb(modp)

一波暴力:从 1 开始枚举 x,然后算完 就可以了。复杂度 O(答案),原地升天。

p 为质数联想到费马小定理,所以极端复杂度是 O(p) 的,还是有亿点点慢。

更加聪明的做法?

考虑把 a0,a1,a2,...,ap2,ap1 分组。假设每组有 s 个数。

{1:a1,a2,...,as2:as+1,as+2,...,a2s...k:a(k1)s+1,a(k1)s+2,...,aks

将第一组每个数字遍历一遍。如果 b 在其中则结束,否则下一组。

但这不与上面的一样吗?

发现第二组每一项等于第一组对应项 ×as,移项得第一组等于第二组 ×as

若第 k 组中存在 b,就可以转换为 b×aks,则本题转换为一个数 b 经过转换是否存在于第一组中。

也就是说,如果第 i 组存在 b,那第一组肯定有 \(\dfrac{b}{a^{s(i-1)}\)

这样我们就只需要把第一组扫一遍,然后枚举出现 p 的组即可。第一个操作是 O(s) 的,第二个是 ps 的,相当于复杂度为 min(s,ps) 的。所以 s=pss=p 时最优。代码:

ll a,b,p;
ll ksm(ll a,ll b,ll p){
    ll res=1;
    a%=p;
    while(b){
        if(b&1)res=res*a%p;
        a=a*a%p,b>>=1;
    }
    return res;
}
ll solve(ll a,ll b,ll p){
    // 求解 a^x % p = b
    ll s=sqrt(p),val=1;
    set<ll>S;
    for(int i=0;i<s;++i){
        S.insert(val);
        val=val*a%p;
    }
    for(int i=0;1ll*i*s<=p;++i){
        // 查找第 i 组(从 0 开始,这样就查找 b/a^si 即可)
        ll inv=ksm(a,i*s,p);
        ll tmp=b*ksm(inv,p-2,p)%p;
        if(S.count(tmp)){
            // inv 就是第 i 行的第一个数
            for(int j=i*s;;++j){
                // 暴力查找第 i 组中满足条件的数
                if(inv==b)return j;
                inv=inv*a%p;
            }
        }
    }
    return -1;
}

组合数学

加法原理与乘法原理

引入——走路

(1) 从甲地到乙地,有两班飞机和三班火车,有几种走法?

解:2+3=5(种)

(2) 从甲地到乙地,有两班飞机;从乙地到丙地,有三班火车;一共有几种走法?

解:2×3=6(种)

为什么 (1) 用加法而 (2) 用乘法捏?

一般地,像 (1) 中同时只能选一个的,使用加法原理;像 (2) 中能同时选两个的,使用乘法原理。

排列与组合

引入——选人排队

(1)从 n 个人中选 m 个人,有多少种选法?

答:Cnm 种。

(2)从 n 个人中选 m 个人排成一队,有多少种排法?

答:Pnm 种。

所以 Cnm 咋求呢?

Cnm=Pnmm!=n!m!(nm)!。这个式子叫做组合数的定义式

=Pnmm! 这一步很好理解,因为 m 种的全排列是 m! 种,而我们不考虑顺序,于是就是 排列的种数m!

一些东西

定义式

在引入部分都有了。

Cnm=n!m!(nm)!Pnm=n!(nm)!

组合数的性质

  • Cnm=Cnnm。同样不难理解,选 m 个就相当于选 nm 个不选。
  • Cn0+Cn1+Cn2+...+cnn西=2n。显然每个东西都有选和不选两种情况,而每个东西之间相互独立。于是就是 2n 了。同理,我们发现:杨辉三角中第 i 行之和等于 2n1
  • n1 时,Cn0Cn1+Cn2+...Cnn=0
    • n 为偶数时,显然可以直接抵消。
    • n 为奇数时,我们考虑杨辉三角:
      • 杨辉三角的每个数字等于上一行左右两个数字之和。
      • n 为奇数时,第 n 行所有奇数位置(红色)之和等于第 n1 行所有数字之和;所有偶数位置(绿色)之和也等于上一行的数字之和。于是一减全没了。

组合数递推式

Cnm=Cn1m1+Cn1m

理解:考虑第一个选或者不选。如果选就是在其余 n1 个里选 m1 个;不选就是在这 n1 个里选 m 个。

二项式定理

(x+y)n=Cn0xny0+Cn1xn1y1+Cn2xn2y2+...+Cnnx0yn=i=0nCnixniyi。很好理解,不再赘述。

例题

数学题

[1,n] 中选 m 个数(不计顺序),每个数可以选任意多次。求方案数?

  • nm
    • 显然不对,这叫不计顺序?
  • Cnmm
    • 此时同一个数字被复制多次,但同一个数字被复制到不同的位置时会当成不同的数字,所以还是不对。
  • 正解
    • 考虑选 m 个数字并排序。
      • 如果不能重复,那选出来的 a1,a2,a3,...,am 就应满足不等式 1a1<a2<...<amn,而我们知道这个不等式的解有 Cnm 组。
      • 而加上能重复的条件之后,不等式就成了 1b1b2...bmn。我们希望将其转化为上面的不等式。
      • 考虑一组数字 c1,c2,c3,...,cm,其中 c1=b1,c2=b2+1,c3=b3+2,...,cm=bm+m1
      • 于是我们也可以列出一个不等式:1c1<c2<c3<...<cmn+m1
      • 然后用与关于 a 的不等式相同的方法得到这个不等式的解有 Cn+m1m 组,而这个不等式每一组解就对应了 b 的不等式的一组解,所以答案就是 Cn+m1m

编程题

  • Cnm%p
    • n,m1018,p=1
      • 弱智,直接输出 0
    • n,m1000
      • 递推式他不香吗?
    • n,m106,p 为质数。
      • 可以使用上面提到的线性求逆(I)。
  • 给定 x,k,将 x 拆成 k 个组合数的和。只要 n1,n2 不同或 m1,m2 不同就叫不同的组合数。
    • 《电信诈骗》
    • x 拆成 k11 和一个 xk+1,于是 x=C10+C20+C30+...+Ck10+Cxk+11
  • 给定 n1,m1,n2,m2,比较 Cn1m1Cn2m2 的大小。
    • log 的性质:logab=loga+logb

抽屉原理

引入——还是引入一下吧

一共 n+1 个物品,放入 n 个抽屉,显然至少有 1 个抽屉有两个物品。

题目

  • 给定 n 个数,要求从中选任意多个数,使得它们的和为 c 的倍数。cn105
    • 史诗级巨坑:《任意多个数》
    • 我们加个条件,让选的数必须连续。此时我们可以直接前缀和,得到 n+1 个数。
    • %c 的值构造抽屉,由于 cn,而我们会构造 c+1 个抽屉,所以肯定有一个抽屉有至少两个前缀和 si,sj,那么 ai+1+ai+2+...+aj 就是 c 的倍数。
  • 平面上有 n 个点,用 3L×L 的各边平行于坐标轴的正方形来覆盖所有的点,求最小的 L
    • 找出这些点的四至点,并画出相应的长方形(红色)。如图:
    • 因为只有三个正方形,所以我们必须让某一个正方形覆盖其中的两个点(绿色)。显然放在角上最优。比如这样:
    • 此时红色正方形就变成了剩余点的四至点。同理,我们再用另一个正方形(蓝色)再次覆盖两个四至点:
    • 最后用一个正方形(橙色)覆盖最后剩的点。
    • 当然上图显然不是最优的。我们把所有可能的情况全部枚举,计算最小值即可。
posted @ 2023-04-29 15:03  蒟蒻OIer  阅读(18)  评论(0编辑  收藏  举报