组合数学相关
1. Lucas & exLucas
1.1 Lucas 定理
先摆结论:\(\dbinom{n}{m}\bmod p=\dbinom{\lfloor n/p\rfloor}{\lfloor m/p\rfloor}\dbinom{n\bmod p}{m\bmod p}\bmod p\)。
证明:考虑组合数的定义,因为 \(\forall i\in[1,p-1],\dbinom{p}{i}\equiv 0\pmod p\),且 \(\dbinom{p}{0}=\dbinom{p}{p}=1\),所以 \((a+b)^p\equiv a^p+b^p\pmod p\)。
因此,\((1+x)^n=(1+x)^{p\lfloor n/p\rfloor}(1+x)^{n\bmod p}\equiv (1+x^p)^{\lfloor n/p\rfloor}(1+x)^{n\bmod p}\pmod p\)。
因为我们只关心 \(x^m\) 前的系数,而 \((1+x^p)^{\lfloor n/p\rfloor}\) 得到的所有指数都为 \(p\) 的倍数,\((1+x)^{n\bmod p}\pmod p\) 得到的所有指数都小于 \(p\),所以此时 \(m\) 只能被写成 \(p\lfloor m/p \rfloor+m\bmod p\),得证。
预处理阶乘及其逆元 \(\mathcal{O}(1)\) 求组合数,则总时间复杂度为 \(\mathcal{O}(p+log_{p}n)\)。
1.2. 例题
I. P3807 【模板】卢卡斯定理
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+5;
ll ksm(ll a,ll b,ll p){
ll s=1;
while(b){
if(b&1)s=s*a%p;
a=a*a%p,b<<=1;
} return s;
}
ll fc[N<<1],ifc[N];
ll c(ll n,ll m,ll p){
if(n<m)return 0;
if(n<p)return fc[n]*ifc[m]%p*ifc[n-m]%p;
return c(n/p,m/p,p)*c(n%p,m%p,p)%p;
}
int main(){
int t,n,m,p;
cin>>t;
while(t--){
cin>>n>>m>>p;
fc[0]=1;
for(int i=1;i<p;i++)fc[i]=fc[i-1]*i%p;
ifc[p-1]=ksm(fc[p-1],p-2,p);
for(int i=p-2;~i;i--)ifc[i]=ifc[i+1]*(i+1)%p;
cout<<c(n+m,n,p)<<endl;
}
return 0;
}
2. Prufer 序列
感觉很多时候生成树计数可以用 Prufer 序列来计算,故学习该算法。
2.1. 树生成 Prufer 序列
Prufer 序列的定义是这样的:给定一棵树 \(T\),每次找到编号最小的叶子结点 \(a\) 并将与其相邻的 \(b\) 加入 Prufer 序列中,删除 \(a\)。重复操作直至树中只剩下 \(2\) 个节点。显然,Prufer 序列的长度为 \(n-2\)。
具体地,用指针维护 \(a\)。删除 \(a\) 之后:
若 \(b<a\) 且 \(b\) 变为叶子结点,那么删除 \(b\),将与其相邻的 \(b’\) 加入序列。操作递归进行,即若 \(b'<b\) 且 \(b'\) 变为叶子结点,那么删除 \(b'\),将与其相邻的 \(b’’\) 加入序列;若 \(b''<b\) 且 \(b''\) 变为叶子结点,那么删除 \(b''\),将与其相邻的 \(b'''\) 加入序列,以此类推。
最后 \(a\) 自增找到下一个编号最小的叶子结点。
时间复杂度为 \(n\)。
2.2. Prufer 序列生成树
设点集 \(a\) 包含 \(1\sim n\) 所有的点,每次取出 Prufer 序列 \(b\) 第一个数 \(u\),在点集 \(a\) 中找到最小的没有在 \(b\) 中出现过的数 \(v\),连边 \((u,v)\) 并将 \(u,v\) 分别在序列 \(b,a\) 中删除。重复操作直至 \(b\) 中没有元素。显然,\(a\) 中还剩下两个元素 \(a_1,a_2\),则连边 \((a_1,a_2)\)。
具体地,用指针维护 \(v\)。连边 \((u,v)\) 之后:
若 \(u<v\) 且 \(u\) 在 \(b\) 中是最后一次出现,那么将 \(u\) 与 \(b\) 中下一个数 \(u’\) 相连。操作递归进行,即若 \(u’<u\) 且 \(u’\) 在 \(b\) 中是最后一次出现,那么将 \(u’\) 与 \(b\) 中下一个数 \(u''\) 连边;若 \(u''<u'\) 且 \(u''\) 在 \(b\) 中是最后一次出现,那么将 \(u’’\) 和 \(b\) 中下一个数 \(u'''\) 相连,以此类推。
最后 \(v\) 自增找到下一个最小的没有在 \(b\) 中出现过的数。
时间复杂度为 \(n\)。
2.3. Prufer 序列得到的一些推论
- \(n\) 个节点的有标号无根树个数为 \(n^{n-2}\)。这也是 \(n\) 个节点的无向完全图的生成树个数。
- \(n\) 个节点的有标号有根数个数为 \(n^{n-1}\)。对于每个无根树,钦定任意一个节点为根都可以形成唯一的有根树,因此将 \(n^{n-2}\) 乘上 \(n\) 即可。
- 度数为 \(d\) 的节点在 Prufer 序列中出现了 \(d-1\) 次,这个由 Prufer 序列的构造方式可知。
- \(n\) 个有度数要求 \(d_i\) 的节点的有标号无根树个数为 \(\dfrac{(n-2)!}{\prod_{i=1}^n(d_i-1)!}\),这个由推论 3 和多重集的排列数可知。
2.4. 例题
*I. CF156D Clues
HOT TEA.
如果将同一个连通块缩成点,设最终剩下 \(k\) 个点,那么它的 Prufer 序列长度为 \(k-2\),且每个位置都可以填 \(1\sim n\) 之间的任意数,因此方案数为 \(n^{k-2}\)。
但是,从节点集合 \(a\)(原来包含 \(1\sim k\),对应连通块的编号)中取出最小的不包含 \(b\) 中出现节点的连通块编号 \(v\) 时,我们并不知道选择的是连通块内的哪一个节点,因此要乘上对应的连通块大小 \(s_v\)。因此答案为 \(n^{k-2}\times \prod_{i=1}^k s_i\)。注意 \(k=2\) 时需要特判输出 \(1\bmod p\)。
时间复杂度 \(n\log n\)。直接 DFS 可以做到 \(n\)。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define gc getchar()
inline int read(){
int x=0,sign=0; char s=gc;
while(!isdigit(s))sign|=s=='-',s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+(s-'0'),s=gc;
return sign?-x:x;
}
const int N=1e5+5;
ll n,m,k,p,f[N],sz[N],ans=1;
int find(int x){return f[x]==x?x:f[x]=find(f[x]);}
void merge(int x,int y){
x=find(x),y=find(y);
if(x==y)return;
if(sz[x]<sz[y])swap(x,y);
sz[x]+=sz[y],f[y]=x;
}
int main(){
cin>>n>>m>>p;
for(int i=1;i<=n;i++)f[i]=i,sz[i]=1;
for(int i=1,x,y;i<=m;i++)merge(x=read(),y=read());
for(int i=1;i<=n;i++)if(f[i]==i)ans=ans*sz[i]%p,k++;
if(k==1)cout<<1%p<<endl;
else{
while((k--)-2)ans=ans*n%p;
cout<<ans<<endl;
}
return 0;
}
原文地址:https://www.cnblogs.com/alex-wei/p/Combinatorial_Mathematics.html
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 移动端适配必须掌握的基本概念和适配方案
- [OHIF-Viewers]医疗数字阅片-医学影像-Redux中的reducer到底是什么,以及它为什么叫reducer?
- 【从零开始用Swift开发一个iOS应用(仿微博)】开篇-1. demo上手体验
- 数据结构与算法-二维数组中的查找
- 卷积神经网络之 - GoogLeNet / Inception-v1
- 卷积神经网络之 - Alexnet
- Linux系列之学会使用Top命令进行系统监控
- 一简单线程同步笔试题分享,欢迎纠错分享更多思路
- 卷积神经网络之-NiN 网络(Network In Network)
- 【注意力机制】空间注意力机制之Spatial Transformer Network
- 【小技巧】用Python给你的视频添加字幕
- 震惊!我三步就搞定了 Tomcat 源码环境搭建!
- 【小技巧】深度学习中的那些效率提升利器(附资源)
- 多个线程为了同个资源打起架来了,操作系统是如何让他们安分的?
- 学习链表,这些题你值得一刷!