本文已参与「新人创作礼」活动,一起开启掘金创作之路。
@[TOC]
磨磨唧唧写了半个月,想起来就写点喽,ACM必备数论基础知识已全部讲完了,这只是我对数论的总结和理解,发现文章中有错误,可以指正,仅供参考,谢谢。撒花啦。
组合计数
排列数
从n个不同的元素中依次取出m个元素排成一列,产生的不同排列的数量为:Anm=(n−m)!n! ###
组合数
从n个不同的元素中取出m个组成一个集合(不考虑顺序),产生不同集合的数量为:Cnm=(n−m)!m!n!
一些简单性质:
1.Cnm=Cnn−m
2.Cnm=Cn−1m+Cn−1m−1
3.Cn0+Cn1+Cn2+.....+Cnn=2n
多重集的排列数 设S={ n1⋅a1,n2⋅a2,n3⋅a3,.....,nk⋅ak}是由n1个a1,n2个a2..nk个ak组成的多重集。
S 的全排列个数为:n1!n2!n3!....nk!n! 多重集的组合数
这里有一个小小的扩展:在上面的约束条件中再添加选取a1个数不少于n0,并保证n0<r,问可以产生多重集的数量为:
思路: 选取不少于n0 ,意味着什么呢:在插入(k-1)个挡板时有了约束,我们不妨把插完挡板后,
第一个挡板前的元素个数视为从a1选取的个数。
这样就有了 [n0 和 剩下的(r−n0)], 然后挡板要求只能被落在"剩下的(r−n0)"。
这样就满足了选取a1个数不少于n0的约束。
然后就能迎刃而解啦。 (r−n0)+(k−1)=r−n0+k−1
产生多重集的数量是:Cr−n0+k−1k−1
这个结论在求更为一般的r的情况,会用到。
例题:Counting swaps
传送门
题意:给定一个n的排列P,问进行若干次操作,每次选择两个整数x,y交换Px,Py
问用最少的操作次数将给定排列变成单调上升的序列1,2....n有多少种方式。对结果1e9+9取模。
思路:
最少操作次数??怎么实现?对于P排列,可以看成一个图。 花出来的图,对于每个点都是入度和出度是1.可知道这个图一定一个环或者多个环。
而且环是一个完整的环,不存在环包含环。而得到单调上升序列,那么图就成了每个点成一个自环。 现在我们需要考虑 原图变成各个点成自环的过程。
两个位置i,j交换,对图的效果是怎么样的呢。 对没有错,i,j指向的点交换了。 如图:
对位置2,4交换一下,得到的图是这样的。
这里交换了两个位置,使得一个环变成了两个环。数学归纳法,到最后各个点都成了自环。一个环的点有k个。
那么要交换k−1次后,k个点成了闭环。(这里可能有人就要问了,为啥我要选一个环上的两点呢,我选不在一个环两个点
进行不行吗?题意有说明,执行交换操作的最小次数,所以我们的操作时要最优的)。
这里我们知道了最少的操作次数,现在我们就来求结果吧。
令一个环长度是n,将该换拆成长度为x和y的两个环。
那么我们拆成长度x,y的环的方式有n种。
(x−1)!(y−1)!(n−2)!,为啥呢。x环,需要交换x−1次,y环,需要交换y−1次,然后现在我们已经具体一种方式了,
在一种具体方式中,就是多重集排列数了。
F(n)=nn−2我们可以通过打表得到结果。
ACcode
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=1e9+9;
ll read(){
ll s = 0, f = 1; char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
return s * f;
}
using namespace std;
ll qpow(ll a,ll x){
ll ans=1;
while(x){
if(x&1){
ans=(ans*a)%P;
}
x>>=1;
a=(a*a)%P;
}
return ans;
}
ll jie[N],nv[N];
void pre(){
jie[0]=1;
for(int i=1;i<=1e5;i++){
jie[i]=(jie[i-1]*i)%P;
}
}
int a[N];
bool flag[N];
int cnt,c[N];
void solve(){
int n=read();
rep(i,1,n){
a[i]=read();flag[i]=false;
}
cnt=0;
rep(i,1,n){
if(flag[i]==0){
int k=0;
int j=i;
while(flag[j]==0){
flag[j]=1;
k++;
j=a[j];
}
c[++cnt]=k;
}
}
ll sum=jie[n-cnt];
rep(i,1,cnt){
sum=sum*qpow(jie[c[i]-1],P-2)%P;
}
rep(i,1,cnt){
if(c[i]==1) continue;
sum=sum*qpow(c[i],c[i]-2)%P;
}
printf("%lld\n",sum);
return ;
}
int main (){
pre();
int T=read();
while(T--)
solve();
getchar();
getchar();
return 0;
}
Lucas定理 Cnm≡Cn mod pm mod p∗Cn/pm/p(mod p)
若p是质数,则对于任意整数1<=m<=n,有: Cnm≡Cn mod pm mod p∗Cn/pm/p(mod p)
证明:

例题:古代猪文
题意:给定整数q,n(1<=q,n<=1e9)计算q∑d∣nCndmod999911659.
思路:
q^{\sum_{d|n} C_n^d}mod999911659=$$q^{\sum_{d|n} C_n^d\ mod\ 999911658}mod999911659
首先质因数分解9999116658=234679*35617.
然后根据中国剩余定理: 求x mod 9999116658,当然这里x很被计算出来。如果模的是质数就能快得出结果。
M=999911658,Mi=P[i]M
x≡a1(mod 2)
x≡a2(mod 3)
x≡a3(mod 4679)
x≡a4(mod 35617)
Miti≡1(mod mi)
这里的ti=Mimi−2
这里不多赘述啦。 直接 x=∑i=1naiMitimodM。
然后这里注意一下细节千万别想当然的认为Miti=Mimi−1 。
因为这里的模数不是同一个,Miti=Mimi−1这样写,我们就默认模数是mi
可是实际的Miti的模数是M.所以ti=Mimi−2还是老老实实写上。
拆解了四个质数,那就分开来进行计算咯。
ACcode
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=999911659;
ll read(){
ll s = 0, f = 1; char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
return s * f;
}
using namespace std;
ll qpow(ll a,ll x,ll mod){
ll ans=1;
while(x){
if(x&1){
ans=ans*a%mod;
}
a=a*a%mod;
x>>=1;
}
return ans;
}
ll jie[N];
ll a[4];
ll mo[4]={2,3,4679,35617};
void init(int m){
jie[0]=1;
for(int i=1;i<=m;i++){
jie[i]=jie[i-1]*i%m;
}
return ;
}
ll C(ll n,ll m,ll mod){
if(n<m) return 0;
return (jie[n]*qpow(jie[m],mod-2,mod))%mod * qpow(jie[n-m],mod-2,mod)%mod;
}
ll lucas(ll n,ll m,ll mod){
if(n<mod and m<mod){
return C(n,m,mod);
}
return C(n%mod,m%mod,mod)*lucas(n/mod,m/mod,mod)%mod;
}
ll M=P-1;
void solve(){
ll q,n;
n=read();
q=read();
if(q%P==0){
printf("0\n");
return ;
}
for(int i=0;i<4;i++){
init(mo[i]);
ll ans=0;
for(int j=1;j*j<=n;j++){
if(n%j==0){
ans=(ans+lucas(n,j,mo[i]))%mo[i];
if(n/j!=j){
ans=(ans+lucas(n,n/j,mo[i]))%mo[i];
}
}
}
a[i]=ans;
}
ll sum=0;
for(int i=0;i<4;i++){
ll Mi=M/mo[i];
ll t=Mi*qpow(Mi,mo[i]-2,mo[i])%M;
sum= (sum+a[i]*t)%M;
}
printf("%lld\n",qpow(q,sum,P));
return ;
}
int main (){
solve();
getchar();
getchar();
return 0;
}
Catalan数列
给定n个0和n个1,排成长度为2n的序列
满足序列任意前缀0的个数都不少于1的个数的序列的数量为:Catn=n+1C2nn
证明:n个0和n个1,排成长度为2n的序列,若S不满足序列任意前缀0的个数都不少于1,则存在一个最小的
位置2P+1 ,使得前2P+1 有P个0和(P+1)个1.而把S[2P+2,2n] 的所有数字去反,得到2n的序列有(n-1)个0
和(n+1)个1.
同理对于(n-1)个0和(n+1)个1任意排成的一个长度的2n序列,必然存在一个位置(2P+1 )使得,P个0和P+1个1,
对后面数字取反,得到的2n序列是n个0和n个1。
在上面中就可以得到了一一对应的关系,
即是"S不满足序列任意前缀0的个数都不少于1"=="(n−1)个0和(n+1)个1任意排成的一个长度的2n序列个数"
而对于(n-1)个0和(n+1)个1的排列个数是C2nn−1
所以给定n个0和n个1,排成长度为2n的序列,
满足序列任意前缀0的个数都不少于1的个数的序列的数量是
C2nn−C2nn−1=n!n!(2n)!−(n−1)!(n+1)!(2n)!=n!(n+1)!(2n)!(n+1−n)=n+1C2nn
该推论的一些性质还有:
解释一下第四个推论: 要求有些不一样,因为行动的时候不能经过y=x直线,意思就是要么0>1或1<0的有多少次数。
那么我们在对角线画上他旁边的对角线,就可以得知封闭的三角形即是可以有Catalan性质的,
然后0和1是(n-1)个即是(2n-2)序列,两条对角线,所以两种情况, 乘2. 得到2Catn−1

容斥原理
设S1,S2,....Sn为有限集合,∣S∣表示S的大小,则:
∣USi∣=∑i=1n∣Si∣−∑1<=i<j<=n∣Si∩Sj∣+∑1<=i<j<k<=n∣Si∩Sj∩Sk∣+....+(−1)n+1∣S1∩....∩...Sn∣

解释一下∣Si∩Sj∣,还是挡板的思路,不过一点小区别是,[ni+nj+2,剩下的元素].
那么[第一个挡板前的个数−(nj+1)]就是ai的个数,然后第一个和第二个挡板之间的个数+nj+1就是aj的个数。 后面的∣Si∩Sj∩Sk∣等,都是这样得出来的。
莫比乌斯反演 (大部分和容斥原理一起用 )
设正整数N按照算术基本定理分解质因数为N=p1c1p2c2....pmcm,定义函数
0 & \exists i\in[1,m],c_i>1 \\
1 & m\equiv 0(mod\ 2),\forall i \in[1,m],c_i=1 \\
-1 & m\equiv 1(mod\ 2),\forall i\in[1,m],c_i=1\\
\end{cases}$$
称$\mu$为莫比乌斯函数。
通俗地讲,当$N$包含相等的质因子时,$\mu(N)=0$.
当$N$的所有的质因子各不相等时.
若N有偶数个质因子,$\mu(N)=1$,若$N$有奇数个质因子,$\mu(N)=-1$
### 模板
```cpp
int V[N],prime[N],miu[N] //最小质因数 素数 μ(N)
void get_miu(int n){
// 用欧拉筛的方式筛
int m=0;
miu[1]=1;
for(int i=2;i<=n;i++){
if(v[i]==0){
v[i]=i,prime[++m]=i;
miu[i]=-1;
}
for(int j=1;j<=m;j++){
if(prime[j]>v[i] || prime[j]*i>n) break;
v[i*prime[j]]=prime[j];
if(miu[i]==0) miu[i*prime[j]]=0;
if(v[i]==prime[j]) miu[i*prime[j]]=0;
else miu[i*prime[j]]=-miu[i];
}
}
rep(i,1,100){
printf("i:%d miu:%d\n",i,miu[i]);
}
return ;
}
```
### 例题:Zap
[传送门](https://vjudge.net/problem/%E9%BB%91%E6%9A%97%E7%88%86%E7%82%B8-1101)
> **题意**: 
> **思路**:根据题意要求:可以等价为:多少对二元组$(x,y)$,满足$x<=a/k,y<=b/k$,并且x,y互质。 用到容斥原理:(ps:怎么用容斥呢?当发现求解的问题中,转为至少要多少的时候,很得出。就可以往容斥方向思考。)
> 
ACcode
```cpp
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=1e9+7;
ll read(){
ll s = 0, f = 1; char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
return s * f;
}
using namespace std;
int v[N],prime[N],miu[N];
void get_miu(int n){
int m=0;
miu[1]=1;
for(int i=2;i<=n;i++){
if(v[i]==0){
v[i]=i,prime[++m]=i;
miu[i]=-1;
}
for(int j=1;j<=m;j++){
if(prime[j]>v[i] || prime[j]*i>n) break;
v[i*prime[j]]=prime[j];
if(miu[i]==0) miu[i*prime[j]]=0;
if(v[i]==prime[j]) miu[i*prime[j]]=0;
else miu[i*prime[j]]=-miu[i];
}
}
// 求和
rep(i,1,n){
miu[i]+=miu[i-1];
}
return ;
}
void solve(){
ll x,y,d;
x=read();
y=read();
d=read();
ll a,b;
a=x/d; b=y/d;
ll sum=0;
ll mins=min(a,b);
ll r=0;
for(int i=1;i<=mins;i=r+1){
// a,b区间的左端点为i
// 在两个区间中,得到右端点最小的那个。
r=min(a/(a/i),b/(b/i));
sum+=(miu[r]-miu[i-1])*(a/i)*(b/i);
}
printf("%lld\n",sum);
return ;
}
int main (){
// freopen("in.txt","r",stdin);
// freopen("zap.txt","w",stdout);
get_miu(5e4+10);
int T;
T=read();
while(T--)
solve();
getchar();
getchar();
return 0;
}
```
## 概率与数学期望
### 例题:Rainbow的信号
[传送门](https://vjudge.net/problem/%E9%BB%91%E6%9A%97%E7%88%86%E7%82%B8-3054)
> 题意:在$1~N的N个数中,等概率的选取两个数l和r,如果l>r,则交换l,r$,把$[l,r]的数取出来构成一个P数列$
> 
> 思路:数据在$1e9$的范围内,按位来思考,最多32位。对每一位分析结果,和概率期望值。
> 这里需要明确一点,**等概率选取l,r**,事件的总和是$N^2$,选取的$l==r$,概率是$\frac{1}{N^2}$
> 当l!=r时,这里的l,r并没有要求谁左谁右,概率为$\frac{2}{N^2}$(ps:假如我等概率选取三个数。$l=r!=k$确定位置的概率为$\frac{A_3^3}{N^3}$。又或者在前提条件下,$l=r!=k$,确定位置的概率是$\frac{C_3^2}{N^3})$
> (以下都是对位操作来实现,最终结果就是32次位操作之和)操作第k位。$(k\in[0,31])$
> 1.考虑或操作,假如确定了右端点r,想知道那些l可以得到值呢,结果很显然,假设距离r最近的1的位置就是L。 那么$[1,L]$都可以是l. 还要思考一点r本身就是l。那么前$[1,r-1]$的概率期望是$P_{or}+=(r-1)*\frac{2}{N^2}*2^k$
> 然后是l==r.即是本身$P_{or}+=\frac{1}{N^2}*2^k$
> 2.考虑且操作,同理,固定$r$,考虑$l$,$[L,r]$连续的1是,那么$l$可以为$l\in[L,r-1]$ 同理$l=r$本身.
> 3.异或操作,固定r,寻找合适的l,满足条件. 如:001**0001**001**0**1考虑最后一个1是r,黑色加粗的即是符合的l.呈现出一个
> 很显然的规律,这个时候呢,**l会是以1为间隔的出现。即是奇数段或者偶数段**
Accode
```cpp
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=1e9+7;
ll read(){
ll s = 0, f = 1; char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
return s * f;
}
using namespace std;
ll a[N];
ld suma,sumo,sumx;
ll c1,c2;
ll last[2];
ld p1,p2;
ll n;
void pre(int k,ll ans){
last[0]=last[1]=0;
c1=c2=0;
int t=1;
ll cnt=0;
for(int i=1;i<=n;i++){
int flag;
if(a[i]&(1<<k)) flag=1;
else flag=0;
// and
if(flag==1){
suma+=ans*p1+cnt*p2*ans;
cnt++;
}else{
cnt=0;
}
// or
if(flag==1){
sumo+=p1*ans+ans*p2*(i-1);
}else{
sumo+=last[1]*p2*ans;
}
// xor
if(flag==1){
sumx+=ans*p1;
if(t==0)
sumx+=c2*p2*ans,c2++;
else sumx+=c1*p2*ans,c1++;
t^=1;
}else{
if(t==0)
sumx+=c1*p2*ans,c2++;
else sumx+=c2*p2*ans,c1++;
}
//位置
if(flag==1)
last[1]=i;
else last[0]=i;
}
// printf("%.3LF %.3LF %.3LF\n",sumx,suma,sumo);
}
void solve(){
n=read();
rep(i,1,n){
a[i]=read();
}
p1=1.0/(n*n);
p2=p1*2;
ll bits=1;
rep(i,0,31){
if(i==0) bits=1;
else bits*=2;
pre(i,bits);
}
printf("%.3LF %.3LF %.3LF\n",sumx,suma,sumo);
}
int main (){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
solve();
getchar();
getchar();
return 0;
}
```
### 例题: 绿豆蛙的归宿
[传送门](https://vjudge.net/problem/%E9%BB%91%E6%9A%97%E7%88%86%E7%82%B8-3036)
> 题意:
> 
> 思路:
> 
> 总结,随便敲敲图的前向性构图方式,图中取反,执行拓扑排序的思想,入队列的点的要求是已经得出了该点u到终点n的期望。
Accode
```cpp
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=1e9+7;
ll read(){
ll s = 0, f = 1; char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
return s * f;
}
using namespace std;
ll n,m;
ll ver[N],edge[N],nex[N],head[N];
ll cnt;
void addedge(int x,int y,int value){
ver[++cnt]=y;
edge[cnt]=value;
nex[cnt]=head[x];
head[x]=cnt;
return ;
}
ll in[N],deg[N];
double value[N];
void pre(){
queue<int> q;
q.push(n);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=head[u];i;i=nex[i]){
int v=ver[i];
// printf("v:%d in:%d\n",v,in[v]);
value[v]+=(value[u]+edge[i])/deg[v];
in[v]--;
if(in[v]==0){
// printf("%d\n",v);
q.push(v);
}
}
}
printf("%.2f\n",value[1]);
return ;
}
void solve(){
//
n=read();
m=read();
ll u,v;
rep(i,1,m){
u=read();
v=read();
addedge(v,u,read());
deg[u]++;
in[u]++;
}
value[n]=0;
pre();
return ;
}
int main (){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
solve();
getchar();
getchar();
return 0;
}
```
## 博弈论之SG函数
### NIM博弈
> 用在一推事件是相等的,且博弈的双方都是同等机会公平的选择
> 
> 结论&定理:NIM博弈先手必胜,当且仅当$A_1\ xor A_2\ xor.......xor\ A_n\neq 0$
> 证明:首先知道a^b=x, 可以得到 x^b=a;
> 1.当全是0时,0 ^ 0 ^ 0 ^ 0....^ =0
> 2.如果当前a1^ a2^ a3^ ai...^ an=x; 则可通过一次操作从某堆中取出石子将其转变为 a1^ a2^ a3^ ai'...^an=0;
> 3.如果当前a1^ a2^ a3^ ai'...^ an=0;则不能通过一次操作再使a1^ a2^ a3^ ai'...^ an=0。 对②证明:由于异或值为x,那么对于a1~an中一定存在ai(二进制)的最高位为1(因为对于异或只有存在1和0才能得1).
> 那么我们就可以去ai堆,使ai堆只剩下ai^ x(显然:ai>ai^ x 因为ai和x的最高位都为1,异或后为0,比如:a^
> b=x,a^ b^x=0,)
> 现在我们由以上的3个定理,给出石子a1~an,假设此时a1^a2^a3..an=x(非0);根据定理②我们就可以
> 操作一步使a1^a2^a3..an=0;根据定理③对手只能将当前结果变为x(非0);重复执行以上的操作,一定会存在我执行完操作后,每堆石子都为0;对手不能操作,我们胜利。
### SG函数
> 
> 注意:这里的mex(S)是得到**不属于集合S的最小非负整数**
> 
> 
#### 例题 Cutting game
[传送门](https://vjudge.net/problem/POJ-2311)
题意:

思路:典型的SG算法。必败的初始有(2,2) or (2,3) or (3,2).


ACcode
```cpp
#include<iostream>
#include<cstring>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=1e9+7;
ll read(){
ll s = 0, f = 1; char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
return s * f;
}
using namespace std;
ll n,m;
int a[205][205];
int dfs(int x,int y){
if(a[x][y]!=-1) return a[x][y];
if((x==2 and (y==2 or y==3)) or (x==3 and y==2)){
a[x][y]=0;
return 0;
}
bool b[250];
memset(b,0,sizeof(b));
for(int i=2;i<=x-i;i++){
b[dfs(i,y)^dfs(x-i,y)]=true;
}
for(int j=2;j<=y-j;j++){
b[dfs(x,j)^dfs(x,y-j)]=true;
}
for(int i=0;;i++){
if(b[i]==0){
a[x][y]=i;
return i;
}
}
}
void solve(){
memset(a,-1,sizeof(a));
// cout<<a[2][3]<<endl;
while(scanf("%d %d",&n,&m)!=EOF){
dfs(n,m);
if(a[n][m]) printf("WIN\n");
else printf("LOSE\n");
}
}
int main (){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
solve();
getchar();
getchar();
return 0;
}
```