Catalan数
基础:
从 \((0,0)\) 走到 \((n,n)\) 不经过对角线的方案数.
答案为:\(\frac{C(2n,n)}{(n+1)}\)
证明:
我们新增一个点 \((n-1,n+1)\) 即终点左上角的点,我们需要证明:所有从经过对角线到终点的路径和从 \((0,0)\) 到 \((n-1,n+1)\)的路径一一对应,我们用总方案-不合法路径,就可以得到答案了
首先我们明确穿过对角线的路径一 定经过线L2,L2为 \((0,1)\) 到 \((n-1,n)\) 这一条直线。显然,从 \((0,0)\) 到 \((n-1,n+1)\) 的路径一定经过L2,我们设与L2的交点为M,将M到终点的路径沿着L2对折,就对应一条到终点的路径,并且一定经过对角线,所以到 \((n-1,n+1)\) 的路径>=到\((n,n)\)的路径.
然后对于到 \((n,n)\) 的所有路径沿着L2翻折之后,也对应一条到 \((n-1,n+1)\)的路径,所以到 \((n-1,n+1)\) 的路径<=到\((n,n)\)的路径
综上:到 \((n-1,n+1)\) 的路径=到\((n,n)\)的路径
所以答案为:\(C(2n,n)-C(2n,n-1)=\frac{C(2n,n)}{(n+1)}\)
应用(摘自NaVi-Awson)
1.括号序列计数
再来看一个问题:有多少种不同的长度为 的括号序列?
首先一个括号序列是指 (), ()(), (())() 这样的由括号组成的序列,并且没有左右括号无法匹配的情况
我们可以将长度为 \(2n\) 的括号序列映射成刚刚所说的路径:首先对于左括号,那么就向右走一个单位长度,对于右括号,那么就向上走一个单位长度,由于括号序列合法,那么每次向右走的次数不会少于向上的次数,也就是这条路径不会在 \(y=x\) 之上。再考虑每一条这样的路径,也能够对应到一种合法的括号序列,因此,长度为 \(2n\) 的括号序列的方案数就是 \(Cn\)
2.出栈顺序
现在来考虑你有 \(n\) 个元素(元素之间是没有区别的)和一个栈,每次可以将一个元素入栈,或者将栈顶元素弹出,问有多少种可能的操作序列,这可以将问题对应成括号序列,入栈为左括号,出栈为右括号,因此方案数也是 \(Cn\)
3.排队问题
现在有 \(2n\) 个人,他们身高互不相同,他们要成两排,每一排有 \(n\) 个人,并且满足每一排必须是从矮到高,且后一排的人要比前一排对应的人要高,问有多少种排法
我们考虑先把这些人从矮到高排成一排,那么现在来分配哪个人在前,哪个人在后,例如有 \(6\) 个人,身高是 1, 2, 3, 4, 5, 6
那么我们用 1 表示这个人应该在后排,0 表示这个人应该在前排,例如说 100110 表示两排分别是 2, 3, 6 和 1, 4, 5 这是不合法的
那么合法方案应该是怎么样的呢?后排要比前排对应的人高,也就是说 0 的出现次数在每一个地方都不应该小于 1,这恰恰又是一个括号序列,因此,方案仍然是 Catalan 数
4.二叉树计数
现在你需要统计有多少种不同的 \(n\) 个结点的二叉树
图上的是 \(3\) 个结点的二叉树,一共有 \(5\) 种方案
朴素的想法是由于二叉树是递归定义的,可以考虑使用递推方法
我们可以用 \(fn\) 表示有 \(n\) 个结点的二叉树的方案数(空树算一种,即 \(f_{0}=0\) ),那么枚举子树大小可以得到方程 \(f_n=\sum_{i=0}^{n-1}f_if_{n-i-1}\)
如果直接计算,你需要 \(O(n^2)\) 的时间
现在我们换一个角度来想,对这棵二叉树进行遍历,并且考虑一个括号序列,当第一次遇到这个结点的时候,在括号序列末尾添加一个左括号,在从左子树回到这个结点的时候,在括号序列中添加一个右括号,这样,将每一种不同的二叉树都对应到了一种不同的括号序列,同样对于每一种不同的括号序列都可以找到对应的一种不同的二叉树,因此,有 \(n\) 个结点的二叉树的数量也是 \(Cn\)
递推式
\(C_{n+1}=\sum_{i=0}^{n}C_i*C_{n-i}\)
第一类斯特林数
含义
将 \(n\) 个不同的人围成 \(m\) 个不同的桌子的方案数 , 记为 \(S(n,m)\)
递推式
\(S(n,0)=0,S(0,0)=1\)
\(S(n,m)=S(n-1,m-1)+S(n-1,m)*(n-1)\)
分别对应把第 \(n\) 个人放到新加入的第 \(m\) 个桌子中和放到任意一个人的左边
性质
第一类斯特林数 \(S(n,m)\) 是 \(x\) 的 \(n\) 次上升幂展开式中 \(x^m\) 项的系数 , 即:
排列数 \(P(n,k)\) 为在 \(n\) 个人中选 \(k\) 个人的排列方案数
\(P(n,k)=n*(n-1)*(n-2)*...*(n-k+1)\)
\(P(n,k)=S(k,k)*n^k-S(k,k-1)*n^{k-1}+...+S(k,k-i)*n^{k−i}−…S(k,0)*n^0\)
\(P(n,k)=\sum_{i=0}^{k}(-1)^{k-i}S(k,i)*n^{i}\)
同样可以写成上升幂的形式 \(x^{n↑}=x*(x+1)*(x+2)...*(x+n-1)=\sum_{k=0}^{n}S(n,k)*x^k\)
可以用递推式来归纳证明 .
这样我们就可以通过 \(O(n*log^2)\) 的分治 \(FFT\) 合并出一个长度为 \(n\) 的上升幂展开的多项式 , 求出第一类斯特林数的某一行的值了 .
第二类斯特林数
含义
将 \(n\) 个有区别的球放入 \(m\) 个无区别的盒子中的方案数 ,记为 \(S(n,m)\) ,其中集合内是无序的 .
递推式
\(S(n,0)=0,S(0,0)=1\)
\(S(n,m)=m*S(n-1,m)+S(n-1,m-1)\)
证明:
1.若单独一个集合,则方案数等价于\(S(n-1,m-1)\)
2.若不是单独一个集合,则他可以在之前任意 \(j\)个集合里,方案为 \(S(n-1,m)*m\)
展开式
\(\frac{1}{m!}\sum_{i=0}^{m}(-1)^i*C_{m}^{i}*(m-i)^n\)
这个式子本质是对 至少有\(i\)个盒子为空进行容斥
拆开组合数,并合并得到:
\(\sum_{i=0}^{m}\frac{(-1)^{i}}{i!}*\frac{(m-i)^{n}}{(m-i)!}\)
这个东西可以 \(NTT\) 优化
错排
定义
满足 \(a[i]!=i\) 的排列的方案数,设为 \(D(n)\)
递推式
\(D(n)=(n-1)*(D(n-1)+D(n-2))\)
首先新加入一个元素,我们先把它放到最后
选择原来的 \((n-1)\) 个元素的错排中的任意一个和新加入的交换,依旧是错排,方案为 \((n-1)*D(n-1)\)
原来的排列不必为错排,若有一个元素满足 \(a[i]=i\) 也可以和新加入的交换,方案为 \((n-1)*D(n-2)\)
推广
\(D(n)=n!*(1-\frac{1}{1!}+\frac{1}{2!}-\frac{1}{3!}+(-1)^n\frac{1}{n!})\)
这个可以用递推式构造出来,这里用容斥来简单的证明:
总方案为 \(n!\) ,我们要减去至少有一个不满足的,方案为 \(\frac{n*(n-1)!}{1!}\)
加上有至少有两个不满足的,方案为 \(\frac{n*(n-1)*(n-2)!}{2!}\)
也就是说至少有 \(m\) 个不满足的方案为 \(C_{n}^{m}*(n-m)!\)
发现每一项分子都是 \(n!\) ,提出即可得到上式
群论
burnside
\(L=\frac{1}{|G|}\sum_{i=1}^{|G|}c1_{i}\)
表示 等价类的个数\(L\)=每一种置换的不动点个数 除以 总置换个数\(|G|\)
证明
其中 \(c1_i\) 的 \(1\) 表示的是长度为 \(1\) 的循环个数
设 \(Z_k\) 表示使元素 \(k\) 不变化的置换集合, \(E_i\) 为元素 \(i\) 在置换群 \(G\) 的所有操作下能到达的所有元素
\(\sum_{i=1}^{|G|}c1_i=\sum_{i=1}^{n}|Z_i|\)
\(\sum_{i=1}^{n}|Z_i|=\sum_{i=1}^{L}|E_i||Z_i|=L|G|\)
所以 \(L=\frac{1}{|G|}*\sum_{i=1}^{|G|}c1_{i}\)
其中 \(\sum_{i=1}^{n}|Z_i|=\sum_{i=1}^{L}|E_i||Z_i|\) , 是因为所有等价类元素的 \(|Z_i|\) 是相等的
\(|E_i||Z_i|=|G|\) 是因为 轨道大小×稳定化子数=变换个数(证明参考<>)
\(burnside\)的主要用法就是暴力枚举所有的置换,并计算其不动点的个数
Polya
\(L=\frac{1}{|G|}\sum_{a_i∈|G|}m^{|k_i|}\)
表示有 \(m\) 种颜色可以选,其中第 \(i\) 中置换有 \(|k_i|\) 个循环
原因可以简单的想一下: 在一个置换中,不同循环之间的颜色选择互不影响,且相同循环必须颜色相同
且在每一种置换中,同一个等价类都被算了一次,所以要除以 \(|G|\)
\(Polya\)的关键在于求出每一种置换的循环个数,从而得出等价类个数
中国剩余定理
//x%p[i]=m[i]
ll crt(){
ll lcm=1,r,tot=0,x,y;
for(int i=1;i<=n;i++)lcm*=p[i];
for(int i=1;i<=n;i++){
r=lcm/p[i];
exgcd(r,p[i],x,y);
tot=(tot+r*x*m[i]%lcm)%lcm;
}
return (tot+lcm)%lcm;
}
一些理解
结论:
1.几个数相加,如果存在一个加数,不能被数 \(x\) 整除,那么它们的和,就不能被整数 \(x\) 整除
2.两数不能整除,我们扩大被除数,那么余数也会扩大相同的倍数,缩小也成立
3.如果 \(a\) \(mod\) \(b=c\),那么 \((a-k*b)\) \(mod\) \(b=c\)
推断:
我们先求出所有 \(p[i]\) 的最小公倍数(\(lcm\)),然后我们用 \(lcm/p[i]\) 得到当前数的基数设为 \(y\),这个基数满足性质:与 \(p[i]\) 互质,能被 \(p[j],j\neq i\) 整除,由于满足结论 \(2\),我们扩大 \(y\) ,直到 \(y \mod p[i]=m[i]\),这里可以用 \(exgcd\) 求出。因为满足定理 \(1\) ,所以相加起来得到一组解,但是此时并不是最小的解,根据定理 \(3\),我们将解 \(mod\) \(lcm\)得到最小解.
Lucas定理
\[C(n,m)\%p=C(n\%p,m\%p)*C(n/p,m/p)\]
观察式子,其优势在于 \(n,m\)取值较大,而模数较小时使用,复杂度是 \(O(log)\).
对于式子的第二项我们递归求解.
inline ll C(int n,int m){
return mul[n]*ni[m]%mod*ni[n-m]%mod;
}
inline ll Lucas(int n,int m){
if(!m)return 1;
return C(n%mod,m%mod)*Lucas(n/mod,m/mod)%mod;
}
扩展卢卡斯
卢卡斯运用的条件是模数P为质数.
扩展卢卡斯:对P进行质因数分解,得到许多形如 \(p^e\) 的质因子,直接用组合数公式算,然后中国剩余定理合并.
难点在于阶乘和模数不互质,不能求逆,考虑阶乘取模.
1.对于 \(p\) 的倍数我们单独提出算,分子分母相减就可以得到 \(p\) 相除后的值
2.剩下的部分与 \(p\) 互质,按 \(\%p\) 的值分块算.
3.提出来的 \(p\) 的系数,发现形式和 \(2\) 部分相同,直接递归处理
举个例子:
\(7!\) \(mod\) \(2\)
\(7!=1*2*3*4*5*6*7=1*3*5*7*2^3*(1*2*3)\)
\(1,3,5,7\) 部分分块算,\(1,2,3\) 部分当做新的阶乘 \(!(n/p)\) 递归处理即可
对于算 \(p\) 的倍数的部分,可以单独算,设\(x\) 表示 \(p\) 在 \(!n\) 中出现的次数.
显然:\(x=\sum_{i=1}n/p^i\)
预处理阶乘之后的话复杂度就是 \(O(log_{p}n)\)
inline void exgcd(ll a,ll b,ll &x,ll &y){
if(!b)x=1,y=0;
else exgcd(b,a%b,y,x),y-=a/b*x;
}
inline ll ni(ll a,ll b){
ll x,y;
exgcd(a,b,x,y);
x%=b;
if(x<0)x+=b;
return x;
}
inline ll Fac(ll n,ll p,ll pr){
if(n==0)return 1;
ll re=0;
for(ll i=2;i<=pr;i++) if(i%p) re=re*i%pr;
re=qm(re,n/pr,pr);
ll r=n%pr;
for(ll i=2;i<=r;i++) if(i%p) re=re*i%pr;
return re*Fac(n/p,p,pr)%pr;
}
inline ll C(ll n,ll m,ll p,ll pr){
if(n<m)return 0;
ll c=0;
for(RG ll i=n;i;i/=p)c+=(i/p);
for(RG ll i=m;i;i/=p)c-=(i/p);
for(RG ll i=n-m;i;i/=p)c-=(i/p);
if(c>=K)return 0;
ll x=Fac(n,p,pr),y=Fac(m,p,pr),z=Fac(n-m,p,pr);
ll re=x*ni(y,pr)%pr*ni(z,pr)%pr*qm(p,c,pr)%pr;
return (mod/pr)*ni(mod/pr,pr)%mod*re%mod;
}
inline ll Lucas(ll n,ll m){
ll x=MOD,re=0;
for(ll i=2;i<=MOD;i++) if(x%i==0){
ll pr=1;
while(x%i==0) x/=i,pr*=i;
re=(re+C(n,m,i,pr))%MOD;
}
return re;
}
高斯消元
性质
一、交换变换:\(Ri<->Rj\),表示将\(Ri\)与\(Rj\)的所有元素对应交换
二、倍法变换:\(Ri=Ri*k\),表示将\(Ri\)行的所有元素都乘上一个常数k
三、消去变换:\(Ri=Ri+Rj*k\),表示将\(Ri\)行的所有元素对应的加上 \(Rj\) 行元素的\(k\)倍
这些变换不会影响最后的解
思路
利用上面的交换求出一个倒三角,再反推回去.
\[ \begin{matrix} x1 & x2 & x3 \\ 0 & x2 & x3 \\ 0 & 0 & x3 \end{matrix} \tag{1} \]
我们一列一列消除,以此类推得到最后一行的一个未知数,然后我们就可以反推回去了.
inline void solve(){
for(int l=1;l<=n;l++){
int maxid=l;
for(int i=l+1;i<=n;i++)
if(fabs(a[i][l])>fabs(a[maxid][l]))maxid=i;
//找到满足这一列最大的一行
if(maxid!=l)swap(a[maxid],a[l]);
if(a[l][l]==0){puts("No Solution");return ;}
//多解情况
for(int i=l+1;i<=n;i++){
double tmp=a[l][l]/a[i][l];
for(int j=l;j<=n+1;j++)
a[i][j]=a[l][j]-tmp*a[i][j];
}
}
//反推求解
for(int i=n;i>=1;i--){
for(int j=i+1;j<=n;j++)
a[i][n+1]-=a[i][j]*a[j][n+1];
a[i][n+1]/=a[i][i];
}
for(int i=1;i<=n;i++)printf("%.2lf\n",a[i][n+1]);
}
特殊情况
1.无解:如果有一行方程的所有变量的系数都为0,而常数项不为0,方程就无解.
2.多解:如果有一行方程的所有变量的系数都为0,常数项也为0,那么这就说明出现了自由元.
自由元:若这一个元素任意取值其他元素都有唯一的解与其对应,则称为自由元,方程解的个数一般就是 \(x^n\) , \(n\) 是自由元的个数 , \(x\) 是能取的值的数量.
线性基
这可能是假的数学,本蒟蒻也不太懂.
例题
给定n个整数(数字可能重复),求在这些数中选取任意个,使得他们的异或和最大
solution
如果给你的每一个数的最高位的1都在不同位置,那么我们就可以从高位到低位按位贪心,线性基就是把这n个数转化为这个等价的形式.
性质
1.通过线性基中元素 \(xor\) 出的数的值域与原来的数 \(xor\) 出数的值域相同
2.线性基的一些子集 \(xor\) 起来的值不为0,因为构造时保证每一个元素都有一个最高位1.
线性基的构造
1.我们求出最高位1为当前位置的一个数,如果当前位没有数,我们就加入这个数
2.如果当前位有数字,我们就把当前加入的数 \(x\) \(xor\) \(a[j]\),\(a[j]\)为当前位置上的数,然后继续寻找1的情况,这样保证了最高位1不再为当前位.
for(int i=1;i<=n;i++){
scanf("%lld",&x);
for(int j=50;j>=0;j--){
if(!(x>>j&1))continue;
if(!a[j]){a[j]=x;break;}
else x^=a[j];
}
long long ans=0;
for(int i=50;i>=0;i--)
if((ans^a[i])>ans)ans^=a[i];
cout<<ans<<endl;
}
快速傅里叶变换
一般用来求卷积即 \(Cn=\sum_{i=1}^{n}a_{i}*b_{n-i}\)
这里称 \(C\) 是 \(ab\) 的卷积
同时这也是多项式乘法的系数,可以用来加速多项式乘法
算法框架
1.转化系数表示法为点值表示法
2.利用点值表示法的性质 \(Ci=ai*bi\) ,可以 \(O(n)\) 的求出相乘后的点值
3.将点值表示法再次转回系数表示法
DFT
DFT即转化系数表示法为点值表示法的过程,复杂度是 \(O(nlogn)\) 的.
点值表示法
对于多项式 \(A(x)=\sum_{i=1}^{n}aix^{i}\)
我们代入一个 \(x0\) 可以得到一个值 \(A(x0)\),所以一个多项式又可以用n+1个点值表示多项式,那么就称 \({(xi,A(xi)):0<=i<=n}\) 为多项式的点值表示.
一个点值表示可以用秦九韶算法 \(O(n)\) 的算出,要求 \(n+1\) 组,就需要 \(O(n^2)\) 的时间,FFT就是巧妙选择了有特殊性质的x代入,使得算法变成 \(O(nlogn)\)
根据玄学的欧拉定理得知:
n次单位根 \(wn=e^{2\pi i/n}\)
再用上一些奇怪的定理:
消去定理 \(W_{ak}^{bk}=(e^{i*2kπ/ak})^{bk}=e^{i*2kπ*bk/ak}=e{i*2kπ*b/a}=W_{a}^{b}\)
折半定理 \((W_{n}^{k})^2=e{2*2kπ/n}=e^{2kπ/(n/2)}=W_{n/2}{k}\)
另外就是对称关系:
\[w_{n}^{n/2+k}=w_{n}^{k} w_{n}^{n/2}=-w_{n}^{k}\]
所以我们就可以把项分类为奇数次项和偶数次项:
\(A0(x)=a0+a2x+a4x^2+...+a_{n-2}x^{(n-2)/2}\)
\(A1(x)=a1+a3x+a5x^2+...+a_{n-1}x^{(n-1)/2}\)
推出公式:
就可以递归处理了
IDFT
结论:直接把相乘后的点值多项式,代入 \(w_{n}^{-1}\) 再做一遍就可以变回系数表示了
递归版(常数巨大):
#include<complex>
typedef complex<double>dob;
const double pi=acos(-1.0);
const int N=4000005;
int n,m;
dob a[N],b[N];
inline void FFT(dob *A,int len,int o){
if(len==1)return ;
dob wn(cos(2.0*pi/len),sin(2.0*pi*o/len)),w(1,0),t;
dob A0[len>>1],A1[len>>1];
for(RG int i=0;i<(len>>1);i++)A0[i]=A[i<<1],A1[i]=A[i<<1|1];
FFT(A0,len>>1,o);FFT(A1,len>>1,o);
for(RG int i=0;i<(len>>1);i++,w*=wn){
t=w*A1[i];
A[i]=A0[i]+t;
A[i+(len>>1)]=A0[i]-t;
}
}
void work()
{
int x;
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++)scanf("%d",&x),a[i]=x;
for(int i=0;i<=m;i++)scanf("%d",&x),b[i]=x;
m+=n;
for(n=1;n<=m;n<<=1);
FFT(a,n,1);FFT(b,n,1);
for(int i=0;i<=n;i++)a[i]*=b[i];
FFT(a,n,-1);
for(int i=0;i<=m;i++)printf("%d ",int(a[i].real()/n+0.5));
}
int main()
{
work();
return 0;
}
非递归版
inline void FFT(dob *A,int o){
for(int i=0;i<n;i++)if(i<R[i])swap(A[i],A[R[i]]);
for(int i=1;i<n;i<<=1){
dob wn(cos(pi/i),sin(o*pi/i)),x,y;
for(int j=0;j<n;j+=(i<<1)){
dob w(1,0);
for(int k=0;k<i;k++,w*=wn){
x=A[j+k];y=w*A[j+i+k];
A[j+k]=x+y;
A[j+k+i]=x-y;
}
}
}
}
void priwork(){
for(n=1;n<=m;n<<=1)L++;
for(int i=0;i<n;i++)R[i]=(R[i>>1]>>1)|((i&1)<<(L-1));
FFT(a,1);FFT(b,1);
for(int i=0;i<=n;i++)a[i]*=b[i];
FFT(a,-1);
}
快速数论变换
有些题目要求答案要对一个质数取模,用 \(FFT\) 做有些缺陷了
那么就可以用原根代替单位复根
代码实现差不多
inline void NTT(int *A,int o){
for(int i=0;i<n;i++)if(i<R[i])swap(A[i],A[R[i]]);
for(int i=1;i<n;i<<=1){
int t0=qm(3,(mod-1)/(i<<1)),x,y;
for(int j=0;j<n;j+=(i<<1)){
int t=1;
for(int k=0;k<i;k++,t=1ll*t0*t%mod){
x=A[j+k];y=1ll*A[i+j+k]*t%mod;
A[j+k]=(x+y)%mod;A[j+k+i]=(x-y+mod)%mod;
}
}
}
if(o==-1)reverse(A+1,A+n);
}
inline void mul(int *A,int *B){
NTT(A,1);NTT(B,1);
for(int i=0;i<=n;i++)A[i]=1ll*A[i]*B[i]%mod;
NTT(A,-1);
int inv=qm(n,mod-2);
for(int i=0;i<=n;i++)A[i]=1ll*A[i]*inv%mod;
}
但是这个东西有着极大的缺陷:模数必须是费马质数(\(P-1\) 有超过序列长度的 \(2\) 的幂因子的质数)
几个常用的: \(998244353,1004535809,469762049\)
如果模数不是费马质数也有一个套路:
选择若干个乘积大于 \(P^2*n\) 的费马质数取模,最后 \(CRT\) 合并一下
快速沃尔什变化
用处
\(a[i]=\sum a[j]*a[k]\,\,j\bigoplus k=i\)
三种卷积的求法
正向:
\(xor\)卷积: \(A[j+k]=x+y,A[j+k+i]=x-y\)
\(and\)卷积: \(A[j+k]=x+y\)
\(or\)卷积: \(A[j+k+i]=x+y\)
逆向:
\(xor\)卷积: \(A[j+k]=\frac{x+y}{2},A[j+k+i]=\frac{x-y}{2}\)
\(and\)卷积: \(A[j+k]=x-y\)
\(or\)卷积: \(A[j+k+i]=y-x\)
\(xor\)卷积的模板:
inline void fwt(int *A,int o){
for(int i=1;i<m;i<<=1)
for(int j=0;j<m;j+=i<<1)
for(int k=0;k<i;k++){
int x=A[j+k],y=A[j+k+i];
if(!o)A[j+k]=(x+y)%mod,A[j+k+i]=(x-y+mod)%mod;
else A[j+k]=(x+y)*inv[2]%mod,A[j+k+i]=(x-y+mod)*inv[2]%mod;
}
}