山东五一集训
前言
不知不觉已经加入TSOI八个月,之前还遥不可及的数学集训这么快就到了,再加上已经很久没出远门。
所以
TSOI2024进军山东!
TSOI2024和wsb及小魏的合照。
2025.04.30
大约在晚上八点多到达酒店。
为什么这么晚呢,因为小魏没抢到时间好的火车票(wcy学长说坐火车没有生活,我表示认同,谴责小魏)
到酒店之后一起在小魏的房间吃了个饭
味道还是不错的。
之后就是颓废时间原神真好玩
正文
2025.05.01上午
差不多5:30就起床了(唐一的生物钟真强大),刚起床就看到jmx在看手机,我也不甘示弱,继续颓废。
组合数学
来看一些简单的东西
加法原理&乘法原理
加法原理:如果做⼀件事有两类⽅式,第⼀类有\(a\)种⽅法,第⼆类有\(b\)种⽅法,那么完成这件事共有\(a+b\)种⽅法。
乘法原理:如果做⼀件事有两个部分,第⼀部分有\(a\)种⽅法,第⼆部分有\(b\)种⽅法,那么完成这件事共有\(a*b\)种⽅法。
很简单对吧。
排序
现有一个包含\(n\)个人的队伍,问你有几种排列方法。
根据乘法原理答案显然是
提高一下难度
现有\(n\)个人,从其中选出\(m\)个人,有多少种选法。
这也不难,答案就是
我们将这个问题的答案记为
继续这个问题,现有\(n\)个人,从其中选出\(m\)个人,有多少种排队方法。
这个问题的答案就是上一个问题答案乘以\(m!\)
此问题的答案记作
继续下一个问题。
有一个多重集,里边含有\(n_{1}\)个\(1\),\(n_2\)个\(2\),……,\(n_m\)个\(m\)。
那么,这个多重集有多少种排列方法呢?
让我们假设\(n=n_1+n_2……+n_m\)
那么答案就是
再来一道题
求方程
\(x_1+x_2+x_3+……+x_m=n\)
的正整数解的个数
看起来好像有点难度,那么我们不妨将题目转换一下
有\(n\)个小球排成一行,选取\(m-1\)个空隙,插上隔板,有几种插法?
显然,这两个题目是等价的,那么答案遍呼之欲出
二项式定理
让我们尝试证明一下
首先,将这个式子展开
\((x+y)*(x+y)*……*(x+y)\)
如果我们在每个式子中选取\(i\)个\(x\)
很显然有\(\binom{n}{i}\)种选择方法,
而剩下的\(n-i\)个式子则组成了\(y^{n-i}\)
所以在打开后表现为\(x^i y^{n-i}\)形式的共有\(\binom{n}{i}\)种
将\(i\)从\(1\)取到\(n\),答案就出来了。
组合数的性质
在了解二次项定理后,我们可以推出一些关于组合数十分有用的性质
性质一:
这个很简单,直接带公式就好了。
观察这个式子。
有没有一点发现?
没错,这就是杨辉三角。
\(1\)
\(1\) \(1\)
\(1\) \(2\) \(1\)
\(1\) \(3\) \(3\) \(1\)
\(1\) \(4\) \(6\) \(4\) \(1\)
\(1\) \(5\) \(10\) \(10\) \(5\) \(1\)
性质二:
同上,推公式可得。
性质三:
好像有些复杂,别慌,让我们拆解一下
根据性质一
有没有看出一些端倪?
我们一直拆下去。
因为当\(n<m\)我们规定
所以在拆解\(n\)次后最后终将变为\(0\),这个性质也就被推出来了。
性质四:
根据二项式定理
所以
性质五:
很明显
那么左边的式子\(x^k\)的系数就是
就是从\((x+y)^n\)里面选择\(i\)个\(x\),然后从\((x+y)^m\)里面选择\(k-i\)个\(x\)。
对于右边的式子\(x^k\)的系数是
由于左右的式子相等,所以
卡特兰数
假设\(C_n\)是具有\(n\)个节点的不同的有根二叉树的个数。
两个二叉树不同,当且仅当以下三者之一成立。
- 一个为空,一个非空。
- 根的左子树不同。
- 根的右子树不同
那么怎么计算\(C_n\)呢,
我们很快就可以想出一个递推式。
直接枚举左右子树即可。
但是上面的式子是\(n^2\)的。
事实上,有一个更简单的公式。
怎么推呢?
我也不会(没学呢,别喷)。
2025.05.01下午
数论
1.一些定义
如果\(a\)被\(b\)整除,或者\(b\)整除\(a\),记作\(a\mid b\)
一些显然的性质:
(1)\(d\mid a\)并且\(d\mid b\)那么\(d\mid (a\pm b)\)
(2)\(b\mid a\)那么\(bc\mid ac\)
(3)\(b\mid a\),\(d\mid a\),\(d\mid b\)那么\(\frac{b}{d}\mid\frac{a}{d}\)
2.gcd&lcm
欧几里得算法:\(gcd(a,b)=gcd(a-b,b)=gcd(a+b,b)=gcd(a \bmod b,b)\)。
又名辗转相除法,原理就是一直模下去,直到最后其中一个数变为\(0\),那就说明除开了,另一个数就是最大公约数。
来一个练习题:
开整
有点不懂,来看解释。
第一步:运用\(gcd(a,b)=gcd(a-b,b)\),左边减去\(a^m\)(这里假设\(n>m\))。
第二步:提出\(a^m\)。
第三步:因为相邻的两个数一定互质,所以去掉\(a^m\).
重复以上三步,让\(n\)一直减\(m\),直到小于\(m\),也就是等于\(n\) \(mod\) \(m\)。
一直进行辗转相除,到最后指数为\(gcd(n,m)\)。
扩展欧几里得算法(exgcd)
给定整数\(a\),\(b\),\(c\),求一组整数使
怎么证明这个方程有解呢?
因为\(x\),\(y\)均为整数,所以不论如何,\(c\)是\(gcd(a,b)\)的倍数时才有解。
那么我们不妨设\(d=gcd(a,b)\),那么我们只需要求出\(ax+by=d\)的解然后乘上 \(\frac{c}{d}\) 即可。
因为
所以我们设\(x^{'}b+y^{'}(a\bmod b)=d\)
那么
所以
到这里可能有的小盆友就要说了,那\(x^{'}\),\(y^{'}\)怎么求呢?
答:继续设\(x^{''}\)和\(y^{''}\)。
就这样不断递归,直到\(b=0\),这个时候\(x=1\),\(y=0\)就是一组解。
搞定!
模版题:P5656 【模板】二元一次不定方程 (exgcd)
\(code:\)
//为什么要攀登?因为山就在那里。
#include<bits/stdc++.h>
#define byn 0x7f7f7f7f7f7f7f7f
#define int long long
using namespace std;
const int mod=1000000007;
int a,b,c,_,x,y;
inline int read(){
char ch=getchar();
int x=0,f=1;
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
return x*f;
}
inline void write(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
return;
}
inline void exgcd(int a,int b){
if(!b){
x=1,y=b;
return;
}
exgcd(b,a%b);
int lx=x;
x=y,y=lx-a/b*y;
}
signed main(){
cin>>_;
while(_--){
cin>>a>>b>>c;
x=0,y=0;
int gcd=__gcd(a,b);
if(c%gcd) printf("-1\n");
else{
exgcd(a,b);
int numy=a/gcd,numx=b/gcd;
x=x*(c/gcd),y=y*(c/gcd);
int k=ceil((1.0-x)/numx);
x+=numx*k;
y-=numy*k;
if(y<=0){
int miny=y+numy*ceil((1.0-y)/numy);
printf("%lld %lld\n",x,miny);
}else{
printf("%lld ",(y-1)/numy+1);
printf("%lld ",x);
printf("%lld ",(y-1)%numy+1);
printf("%lld ",x+(y-1)/numy*numx);
printf("%lld\n",y);
}
}
}
return 0;
}
唯一分解定理
对于任意的正整数\(n\),我们可以将其分解成若干素数的乘积。
其实这算不上一种正经的算法。
但是这可以给我们提供一种新的看待\(lcm\),\(gcd\)的视角。
对于\(lcm(a,b)\)我们只需要将\(a\)和\(b\)的每一个\(c_i\)都取\(max\)。
而\(gcd(a,b)\)则可以直接取\(min\)。
唯一分解定理的力量
1.\(a\mid bc \Rightarrow \frac{a}{gcd(a,b)}\mid c\)
2.\(gcd(a,b)lcm(a,b)=ab\)
3.\(d\mid a\),\(d\mid b\Longleftrightarrow d\mid gcd(a,b)\)
4.\(a\mid d\),\(b\mid d\Longleftrightarrow lcm(a,b)\mid d\)
可以尝试用唯一分解定理理解一下上述结论。
同余
如果\(a\bmod m=b\bmod m\),那么我们称作\(a\)和\(b\)模\(m\)同余,记作\(a\equiv b(\bmod m)\)
同余的基本性质:
1.\(a\equiv b(\bmod m)\Leftrightarrow a\pm c\equiv b\pm c(\bmod m)\)
2.\(a\equiv b(\bmod m)\Leftrightarrow ac\equiv bc(\bmod m)\)
在模\(m\)的意义下我们不区分\(-1\)和\(m-1\),因为它们模\(m\)的结果是一样的。
更进一步,在模\(7\)的意义下,我们也不区分\(\frac{1}{2}\)和\(4\),因为它们乘\(2\)的结果模\(7\)都是一样的。
可以看出,对于取模,我们只关心结果,对于模\(m\),我们的世界里只有\(0,1,2,……,m-1\)这几个元素,其他元素和他们是等价的。
逆元
首先\(b\)的逆元要满足一个性质:\(b^{-1}*b\equiv 1(\bmod m)\)
也就是说,我们假设\(x\)是\(b\)的逆元,那么一定有\(x*b\equiv 1(\bmod m)\)
我们如何保证一个数\(b\)存在在\(\bmod m\)意义下的逆元呢,当且仅当\(b\)和\(m\)互质时,存在逆元。
对于求逆元,我们有以下几种方法:
(以下部分定理笔者就不进行证明了,主要是我也不太会证,感兴趣的可以在网上搜搜看)
- 1.费马小定理
对于任意一个质数\(p\),如果\(a\),\(p\)互质,那么\(a^{p-1}\equiv 1(\bmod p)\)
之后,我们就可以推出\(a^{p-2}\equiv a^{-1}(\bmod p)\)
这里很明显要用到快速幂,粘一个代码。
\(code:\)
int ksm(int a,int b,int mod){
int ans=1;
while(b){
if(b&1) ans*=a,ans=(ans%mod+mod)%mod;
a*=a,a=(a%mod+mod)%mod,b>>=1;
}
return ans;
}
- 2.1欧拉函数
对于一个自然数\(m\),如果\(a\),\(m\)互质,那么\(a^{\varphi(m)}\equiv 1(\bmod m)\)
之后,我们可以看出\(a^{-1}\equiv a^{\varphi(m)-1}(\bmod m)\)
所以,\(a^{\varphi(m)-1}\)就是\(a\)的逆元
*注:对于一个质数\(p\),\(\varphi(p)=p-1\),可以看出,费马小定理是欧拉函数的特殊情况。
但是欧拉函数怎么求呢?
别着急,继续往下看
先来看一下定义:\(\varphi(p)\)表示\(<p\)的正整数中与\(p\)互质的数的个数。
很显然的,如果\(p\)是质数,那么\(\varphi(p)=p-1\)。
然后可以证明\(\varphi(p^k)=p^k-p^{k-1}\)
因为\(p\)是质数,对\(p^k\)其进行质因数分解,一定会分成\(k\)个\(p\)相乘的形式。
只有不包含任意一个\(p\),才和\(p^k\)互质。
而包含\(p\)的数则有\(p^{k-1}\)个。
所以\(\varphi(p^k)=\frac{p-1}{p}*p^k\)。
这里需要引入一个性质,就是\(\varphi(a*b)=\varphi(a)*\varphi(b)\)
证明需要用到中国剩余定理,这里就不展开说了。(以后有时间笔者尽量补)
那么对于一个数\(n\),将其进行质因数分解,根据唯一分解定理,我们一定可以将\(n\)分成如下形式。
我们发现前面的高次项就是\(n\),所以\(\varphi(n)=n*\frac{p_1-1}{p_1}*\frac{p_2-1}{p_2}*……*\frac{p_x-1}{p_x}\)。
之后就可以通过枚举\(n\)的质因数用\(O(\sqrt n)\)写代码了。
\(code:\)
inline int euler(int x){
int ans=x;
for(int i=2;i*i<=x;i++){
if(x%i==0){
ans=ans/i*(i-1);
while(x%i==0) x/=i;
}
}
if(x>1) ans=ans/x*(x-1);
return ans;
}
- 2.2扩展欧拉定理
很简单,给你三个正整数,\(a,m,b\),你需要求:\(a^b \bmod m\)。
如果\(b\)非常大的话,那么快速幂的时间复杂度就显然不达标了。
我们需要缩小指数,而扩展欧拉定理就能解决这个问题。
扩展欧拉定理是什么呢?
若\(b\ge \varphi(m)\)那么\(a^b \equiv a^{b \bmod\varphi(m)+\varphi(m)}(\mod m)\)
怎么证呢?
我不会……,感兴趣的读者可以自己搜索。
模版题P5091 【模板】扩展欧拉定理
\(code:\)
//为什么要攀登?因为山就在那里。
#include<bits/stdc++.h>
#define byn 0x7f7f7f7f7f7f7f7f
#define int long long
using namespace std;
int a,m,b,ans,mod;
inline int euler(int x){
int ans=x;
for(int i=2;i*i<=x;i++){
if(x%i==0){
ans=ans/i*(i-1);
while(x%i==0) x/=i;
}
}
if(x>1) ans=ans/x*(x-1);
return ans;
}
inline int read(){
char ch=getchar();
int num=0,flag=0;
while(ch>'9'||ch<'0') ch=getchar();
while(ch>='0'&&ch<='9'){
num=num*10+ch-'0';
if(num>=mod) num%=mod,flag=1;
ch=getchar();
}
if(flag) return num+mod;
else return num;
}
inline int ksm(int a,int b,int mod){
int ans=1;
while(b){
if(b&1) ans*=a,ans%=mod;
a*=a,a%=mod,b>>=1;
}
return ans;
}
signed main(){
cin>>a>>m;
mod=euler(m);
b=read();
ans=ksm(a,b,m);
cout<<ans;
return 0;
}
- 3.线性预处理序列逆元1(参考自二两碘酊的博客)
来看一道题:给定 \(n\) 个正整数 \(a_i\) ,求它们在模质数 \(p\) 意义下的乘法逆元。
这道题怎么做?
首先第一个想法就是直接用费马小定理,时间复杂度\(O(n \log p)\)。
这很明显不能满足我们的要求。
为了减少时间复杂度,我们可以采用一种很巧妙的方法。
对于要求出逆元的数组\(a_1,a_2,……,a_n\)。
我们设
那么便有
由于\(f\)数组我们可以直接\(O(n)\)求出来,如果得到了\(g_n\),那么也可以\(O(n)\)求出\(g\)数组。
而\(g_n\)就是\(f_n\)的逆元,直接费马小定理就行了。
费马小定理的时间复杂度是\(O(\log p)\),所以这种方法的时间复杂度就是\(O(n+\log
p)\)
模板题P5431乘法逆元 2
\(code\):
//为什么要攀登?因为山就在那里。
#include<bits/stdc++.h>
#define byn 0x7f7f7f7f7f7f7f7f
#define int long long
using namespace std;
int n,p,k,a[5000010],f[5000010],g[5000010],ans;
inline int read(){
int num=0;
char ch=getchar();
while(ch>'9'||ch<'0') ch=getchar();
while(ch<='9'&&ch>='0'){
num=num*10+ch-'0';
ch=getchar();
}
return num;
}
inline int ksm(int a,int b,int mod){
int ans=1;
while(b){
if(b&1) ans*=a,ans=(ans%mod+mod)%mod;
a*=a,a=(a%mod+mod)%mod,b>>=1;
}
return ans;
}
signed main(){
n=read(),p=read(),k=read();
f[0]=1;
for(int i=1;i<=n;i++) a[i]=read(),f[i]=f[i-1]*a[i],f[i]=f[i]%p;
g[n]=ksm(f[n],p-2,p);
for(int i=n-1;i>=1;i--) g[i]=g[i+1]*a[i+1],g[i]=g[i]%p;
int num=1;
for(int i=1;i<=n;i++){
num*=k;
num=num%p;
ans+=num%p*f[i-1]%p*g[i]%p;
}
cout<<ans%p;
return 0;
}
- 4.线性预处理序列逆元2(不常用)(参考自二两碘酊的博客)
设\(p=ki+r\),那么\(k=\lfloor \frac{p}{i}\rfloor\),\(r=p \bmod i\)。
那么
因为
所以
\(code:\)
void work(){
ele[1]=1;
for(int i=2;i<=n;i++){
int k=p/i,r=p%i;
ele[i]=(p-k)%p*ele[r]%p;
}
}
- 5.扩展欧几里得
根据逆元定义:
那么\(x\)就是\(a\)的逆元。
对于任意的整数\(k\)。
根据逆元的定义,\(a\)与\(p\)互质。
所以方程
一定有整数解。
根据上文提到的扩展欧几里得,直接就可以求出\(x\)的值。
这真是太棒了,即便\(p\)不是质数我们依旧可以这么写,虽然可能略慢于费马小定理,但是用它就对了。
代码和上面一样,就不再给出了。
拉格朗日插值
对于 \(n\) 个点 \((x_i,y_i)\),如果满足 \(\forall i\neq j, x_i\neq x_j\),那么经过这 \(n\) 个点可以唯一地确定一个 \(n-1\) 次多项式 \(y = f(x)\)。
现在,给你如上所述的\(n\)个点,让你求出\(f(k)\)的值是多少。
对于每个坐标\((x_i,y_i)\),我们希望构造一个函数\(G_i(k)\),对于这个函数我们希望只有当\(k=x_i\)时\(G_i(k)\)的值为\(y_i\),其他时候此函数的值均为\(0\)。
这样我们就可以通过给所有坐标的\(G_i(x)\)函数的取值求和来求出我们想要的值。
但是写出\(G_i(k)\)函数的表达式也并不简单,我们继续简化这个问题。
对于坐标\((x_i,y_i)\),构造函数\(D_i(k)\),当\(k\neq x_i\)时此函数值均为\(0\)。
这个表达式我们就可以尝试写出来了
显然,当\(k\neq x_i\)时,表达式中一定会出现一个\(0\)导致函数的值为\(0\)。
而对于\(k=x_i\)的情况,虽然\(D_i(k)\neq y_i\),但是我们只要将所有的\(D_i\)都除去\(D_k(k)\)再乘上\(y_i\)就可以了。
所以
当\(k\neq x_i\)的时候上面的式子一定是\(0\),所以下面的分母是多少都无所谓。
我们来看\(k=x_i\)的情况。
此时\(k=x_i\),我们直接把分母的\(k\)替换掉。
同理连乘符号我们也可以直接提出来。
所以
合并一下
模板题P4781 【模板】拉格朗日插值
\(code:\)
//为什么要攀登?因为山就在那里。
#include<bits/stdc++.h>
#define byn 0x7f7f7f7f7f7f7f7f
#define int long long
using namespace std;
const int mod=998244353;
int ans,n,k,x[20010],y[20010];
int ksm(int a,int b,int p){
int ans=1;
while(b){
if(b&1) ans*=a,ans=(ans%p+p)%p;
a*=a,a=(a%p+p)%p,b>>=1;
}
return ans;
}
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>x[i]>>y[i];
for(int i=1;i<=n;i++){
int num1=1,num2=1;
for(int j=1;j<=n;j++){
if(j==i) continue;
num1=(num1*(k-x[j])%mod+mod)%mod;
num2=(num2*(x[i]-x[j])%mod+mod)%mod;
}
ans=ans+num1%mod*ksm(num2,mod-2,mod)%mod*y[i];
ans=(ans%mod+mod)%mod;
}
cout<<ans;
return 0;
}
如果给出的数据比较核善,保证每个\(x_i=i\),那么我们就可以优化一下我们的时间复杂度。
首先将每个\(x\)替换。
设\(p_i=\prod^i_{j=1}k-j\),\(s_i=\prod^i_{j=i+1}k-j\)。
那么
但是分母那一坨怎么办?
先打开看看。
直接预处理一个\(fac\)阶乘数组就可以了。
很明显,对于\(s\),\(p\),\(fac\)这三个数组我们都可以\(O(n)\)预处理解决。
最后再用\(O(n)\)求和就可以了。
总时间复杂度\(O(n)\)。
模版题:CF622F The Sum of the k-th Powers
但是目前洛谷交不了,我也没写,所以没有代码,嘻嘻~。
中国剩余定理(CRT)
试求如下同余方程组的最小非负整数解:
确保\(b_i\)两两互质。
怎么解决这道题呢。
我们简化一下此题目。
分别求解以下方程:
此时,我们将每个方程的解相加,原方程的解就是
对于这一坨方程,我们首先尝试解其中的一个:
要求出如上方程的解,我们可以进一步装换:
那么\(x_1=y_1*a_1\)。
观察同余方程组,可以发现\(y_1\)一定是\(\prod^n_{i=2}b_i\)的倍数。
我们设\(\prod^n_{i=2}b_i=m_1\)。
那么\(y_1=m_1*k_1\)。
由于\(y_1\)还要满足第一个同余方程。
所以\(m_1*k_1\equiv 1(\bmod b_1)\)。
这时候我们发现\(k_1\)就是\(m_1\)在\(\bmod b_1\)的意义下的逆元,直接扩展欧几里得就可以求出。
按照如上方法,我们就求出了上述\(n\)个同余方程的整数解,将它们加起来,我们就得到了原方程的整数解。
而最小非负整数解就是用你得到的解对所有\(b\)的乘积取模。
模板题P1495 【模板】中国剩余定理(CRT)/ 曹冲养猪
\(code:\)
//为什么要攀登?因为山就在那里。
#include<bits/stdc++.h>
#define byn 0x7f7f7f7f7f7f7f7f
#define int long long
using namespace std;
int n,a[20],b[20],num=1,x,y,ans;
inline int ksm(int a,int b,int mod){
int ans=1;
while(b){
if(b&1) ans*=a,ans%=mod;
a*=a,a%=mod,b>>=1;
}
return ans;
}
inline void exgcd(int a,int b,int &x,int &y){
if(!b){
x=1,y=0;
return ;
}
exgcd(b,a%b,x,y);
int lx=x;
x=y,y=lx-a/b*y;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i]>>b[i],num*=a[i];
for(int i=1;i<=n;i++){
int m=num/a[i];
exgcd(m,a[i],x,y);
ans=(__int128)ans+(__int128)x%num*m%num*b[i]%num;
ans%=num;
}
cout<<(ans%num+num)%num;
return 0;
}
扩展中国剩余定理(exCRT)
还是一样的问题:
试求如下同余方程组的最小非负整数解:
但是不保证\(b\)互质了,这是一个悲伤的故事。
乍一看可能有一点难。
我们先考虑只有两个同余方程怎么求解。
显然\(x\)满足如下两个方程。
将两个方程相减。
由于\(a_1\),\(a_2\),\(b_1\),\(b_2\)已知,所以我们可以直接通过扩展欧几里得求出方程的特解。
因为题目要的是最小正整数解。
显然当\(k_2\)是正整数且最小时\(x\)满足条件。
所以我们对求出\(k_2\)模\(b_1\)的结果就能保证\(x\)是最小的正整数解(具体怎么做详见上文扩展欧几里得算法(exgcd))。
之后我们考虑怎么合并这两个同余方程。
既然\(x\)已经求出那么我们要处理的就只剩下\(a\)和\(b\)。
对于合并后的\(a\),我们直接用两个方程的解\(x\)代替即可(应该很显然吧)。
而对于\(b\)我们直接取\(lcm(b_1,b_2)\)就可以了。
合并后的方程:
假设我们已经合并了\(k-1\)个方程。
设\(m_i=LCM_{j=1}^i b_j\)
我这么写应该能看懂吧……
所以最终的方程就是:
\(x_k\)表示合并了\(k\)个同余方程之后的解。
就这样一直合并,直到只剩一个方程就是我们要的结果了。
模板题P4777 【模板】扩展中国剩余定理(EXCRT)
\(code:\)
//为什么要攀登?因为山就在那里。
#include<bits/stdc++.h>
#define byn 0x7f7f7f7f7f7f7f7f
#define int __int128
using namespace std;
int n,a[100010],b[100010],now1,ans1,x,y;
inline int read(){
char ch=getchar();
int num=0;
while(ch>'9'||ch<'0') ch=getchar();
while(ch>='0'&&ch<='9'){
num=num*10+ch-'0';
ch=getchar();
}
return num;
}
inline void write(int num){
if(num==0) return ;
if(num<0){
num*=-1;
putchar('-');
}
char ch=num%10+'0';
write(num/10);
putchar(ch);
}
inline void exgcd(int a,int b,int &x,int &y){
if(!b){
x=1,y=0;
return ;
}
exgcd(b,a%b,x,y);
int lx=x;
x=y,y=lx-a/b*y;
}
signed main(){
n=read();
for(int i=1;i<=n;i++) a[i]=read(),b[i]=read();
now1=a[1],ans1=b[1];
for(int i=2;i<=n;i++){
int now2=a[i],ans2=b[i],c=ans2-ans1;
int g=__gcd(now1,now2);
if(c%g){
cout<<-1;
return 0;
}
exgcd(now1,now2,x,y);
x=x*(c/g);
x=(x%now2+now2)%now2;
ans1+=x*now1;
now1=now1/g*now2;
ans1=(ans1%now1+now1)%now1;
}
write(ans1);
return 0;
}
2025.05.02上午
线性代数的基础知识
一些定义
- 1.矩阵:\(nm\)个数字,写成\(n\)行,\(m\)列,就是一个\(n*m\)的矩阵。
- 2.向量:\(n*1\)的矩阵成为列向量,\(1*n\)的矩阵称为行向量,当然\(n*1\)的矩阵也可看做数组。
- 3.矩阵的表示通常使用大写字母。
例如:矩阵\(A\)的第\(i\)行,第\(j\)列用\(a_{i,j}\)(或\(A_{i,j}\))表示。 - 4.对角线/主对角线:\(a_{1,1},a_{2,2},a_{3,3}……\)这些元素,也就是从左上到右下的一条倾斜\(45°\)的斜线。
- 5.反对角线:从右上往左下的,一条倾斜\(45°\)的斜线。
矩阵的运算
- 1.加减法:(大小相同的矩阵)按位相加
- 2.乘上常数:
- 3.转置:把矩阵沿对角线对称
数字表示:\((A^T)_{i,j}=A_{i,j}\)
例:
- 4.乘法:不再是按位相乘
一个\(a*b\)的矩阵和一个\(b*c\)的矩阵,相乘会得到一个\(a*c\)的矩阵。
定义:
\(code:\)
memset(C,0,sizeof(C));
for(int i=1;i<=a;i++)
for(int j=1;j<=c;j++)
for(int k=1;k<=b;k++)
C[i][j]+=A[i][k]*B[k][j];
- 5.另一种看待矩阵乘法的方法:
向量的点积:两个\(n\)维向量的点积:
把矩阵\(A\)看成\(a\)个\(b\)维向量组成的矩阵。
把矩阵\(B\)看成\(c\)个\(b\)维向量组成的矩阵。
那么,\(C=A*B\)就是\(a*c\)个点积结果排成的矩阵。
具体地,\(C_{i,j}\)就是\(A\)的第\(i\)个⾏向量,和\(B\)的第\(j\)个列向量,点积的结果。
我们由此可以看出矩阵乘法的一个性质:
矩阵运算的性质
- 1.交换律:\(A\circ B=B\circ A\)
- 2.结合律:\((A\circ B)\circ C=A\circ(B\circ C)\)
加法很明显满足结合律和交换律。
乘法满足结合律但是不满足交换律。
也就是说\(A*B\)和\(B*A\)不一定相等。
乘法满⾜对加法的分配律:
线性空间和线性基
"空间"
全体\(n\)维向量组成的集合,称作\(n\)维“空间”。
1维空间:数轴。
2维空间:平⾯直⻆坐标系(建系!多好的建系条件啊!)。
我们可以把熟悉的 维空间中的点,和 维向量做⼀个⼀⼀对应,因此我们也经常混⽤“点”和“向量”。
线性空间
我们可以发挥我们的想象⼒:空间内的点⽆法脱离这个空间。
数学上的定义:⼀个向量的集合\(V\)是线性空间,当且仅当
都有
换句话说,⽆论怎么“组合”两个向量,还是“拉伸”⼀个向量,还是先拉伸再组合,得到的向量都还是在这个空间中。
线性组合
对于\(n\)个向量\(\vec{v_1},\vec{v_2},……,\vec{v_n}\),以下向量是他们的一种线性组合:
其中\(c_1,c_2,……,c_n\)是常数。我们称\(c_1,c_2,……,c_n\)是这个线性组合的系数。
- 任⼀个线性组合,都能通过“拉伸”和“组合”得到。
- 反过来,从\(n\)个向量出发,⽆论怎么做“拉伸”和“组合”,得到的都还是这\(n\)个向量的某种线性组合。
张出(span)和线性基
我们已经能感受到线性组合和线性空间的关系了。事实上,只要把刚刚的内容总结⼀下,我们⽴刻能发现:
- 任取\(n\)个向量\(\vec{v_1},\vec{v_2},……,\vec{v_n}\),它们的全体线性组合,都构成了一个线性空间\(V\)。我们记\(V=span(\vec{v_1},\vec{v_2},……,\vec{v_n})\)为这\(n\)个向量张出的线性空间。
反过来说对不对呢?也就是说:
- 任取⼀个线性空间,它都可以被描述成某\(n\)个向量的全体线性组合。
这个命题也是正确的。这样的\(n\)个向量,称为这个线性空间的⼀组⽣成元。
如果⼀组⽣成元是最⼩的(也就是说,没有⼀组个数上⽐它更少的⽣成元),它就是这个空间的⼀组线性基(或简称基)。
一个方便理解线性代数的视频。此视频共6节,强烈建议各位读者观看。
基坐标
⼀个空间可以很复杂,但是只要知道了它的⼀组基,我们就可以描述这个空间。同时,我们还可以⽤这组基来描述这个空间的每⼀个向量。
定理:对于一个线性空间\(V\),如果\(\vec{v_1},\vec{v_2},……,\vec{v_n}\)是它的一组基,则对于这个空间内的任意一个向量\(\vec{x}\in V\),都存在唯一的系数\(c_1,c_2,……,c_n\),使得:
这组系数\(c_1,c_2,……,c_n\)就称作\(\vec{x}\)在\(\vec{v_1},\vec{v_2},……,\vec{v_n}\)这组基下的坐标。
我们平时⽤的是⼀组特殊的基来描述\(n\)维空间,这组基中的向量称为单位向量,第\(i\)个单位向量是\(e_i=(0,0,…,1,…,0)\)(只有第\(i\)位是1)。
线性变换
线性空间中的变换
变换:是这个空间中的函数,把⼀个向量映射到另⼀个向量。
线性变换:满⾜线性性的变换。
发现:\(\vec{0}\)必须被映射到\(\vec{0}\)
⼆维空间中熟悉的线性变换:绕原点旋转;沿x轴对称……
一个方便理解线性变换的视频。
线性变换和矩阵
事实上,每⼀个线性变换都可以唯⼀地⽤⼀个矩阵表⽰。
每一个线性变换\(f()\)都可以表示成\(f(\vec{x})=A*\vec{x}\),其中\(A\)是一个矩阵。
线性变换的复合:假如\(f(\vec{x})=A*\vec{x}\),\(g(\vec{x})=B*\vec{x}\),那么:
这也是为什么矩阵乘法为什么定义的与我们的常识不太相同,因为只有这样定义,才能⽤矩阵描述线性变换,⽤矩阵乘法描述线性变换的复合。
2025.05.03下午
大步小步算法(Baby Step Giant Step)
此算法解决的问题大约是给定整数\(a\),\(b\),质数\(p\),求满足\(a^x\equiv b(\bmod p)\)中的非负整数\(x\)的最小值。
对于一个模数\(p\),\(a^0,a^1,……,a^p-1\)一定存在一次或多次对于\(a^i\bmod p\)的结果的循环(因为一个数对\(p\)取模最多有\(p\)种结果)。
因此,我们令\(t=\lfloor\sqrt p\rfloor\),\(0<j \le t-1\)。
设\(x=i*t-j\),那么\(a^{i*t-j}\equiv b(\bmod p)\)。
两边同乘\(a^j\),\(a^{i*t}\equiv b*a^j(\bmod p)\)。
之后我们就可以用\(t\)次枚举\(j\),然后把\(b*a^j\)存到\(map\)中。
之后枚举\(i\),因为前面已经说了,对于一个模数\(p\),\(a^0,a^1,……,a^p-1\)一定存在一次或多次对于\(a^i\bmod p\)的结果的循环。
所以我们就应该保证\(i*t-j<p\),那么便得出\(i\)的范围是\([0,t]\),这样我们就计算出了\(a^{i*t}\)。
我们查询这个数的值在\(map\)中是否存在对应的\(j\),如果存在,因为\(t\)已知,那么便得出了答案。
时间复杂度\(O(t)\),也就是\(O(\sqrt p)\)。
模板题p3846可爱的质数
\(code:\)
//为什么要攀登?因为山就在那里。
#include<bits/stdc++.h>
#define byn 0x7f7f7f7f7f7f7f7f
#define int long long
using namespace std;
int p,a,b;
unordered_map<int,int> mp;
int ksm(int a,int b,int mod){
int ans=1;
while(b){
if(b&1) ans*=a,ans=(ans%mod+mod)%mod;
a*=a,a=(a%mod+mod)%mod,b>>=1;
}
return ans;
}
int dbxb(int a,int b,int p){
int x=ceil(sqrt(p)),now=1;
for(int i=0;i<x;i++){
int y;
if(!i) y=(b%p+p)%p;
else{
now*=a;
now=(now%p+p)%p;
y=((b*now)%p+p)%p;
}
mp[y]=i;
}
now=ksm(a,x,p);
int val=1;
for(int i=1;i<=x;i++){
val*=now;
val=(val%p+p)%p;
if(mp.find(val)!=mp.end()){
int j=mp[val];
if(i*x>=j) return ((i*x-j)%p+p)%p;
}
}
return -1;
}
signed main(){
cin>>p>>a>>b;
b%=p;
int ans=dbxb(a,b,p);
if(ans==-1) cout<<"no solution";
else cout<<ans;
return 0;
}