组合数学专题
组合数学专题!
最近 noip 考完了,决定试试冲冲省选,虽然没什么希望。
无望的努力也是一种独特的体验吧。
之后如果可能,会写一个 OI 经历的博客,最近真的有点迷茫,先学再说。
1. 推式子
例 1.1 (二项式反演)
题意
一个有 \(N\) 个元素的集合有 \(2^N\) 个不同子集(包含空集),现在要在这 \(2^N\) 个集合中取出若干集合(至少一个),使得它们的交集的元素个数为 \(K\),求取法的方案数,答案模 \(1000000007\)。
题解
我的离谱思路里程
先选交集的 \(k\) 个元素,\(\displaystyle{\binom{n}{k}}\) 种方案。
剩下的元素有 \(n-k\) 个,包含选出的 \(k\) 个元素的集合数一共是 \(2^{n-k}\)
然后你需要选若干个集合,方案数 \(2^{2^{n-k}}\),因为要至少一个,所以 \(2^{2^{n-k}}-1\)
嗯嗯然后会多考虑交集元素个数大于 \(k\) 的情况
于是答案就是 \(\displaystyle{\binom{n}{k}(2^{2^{n-k}}-1)-\binom{n}{k+1}(2^{2^{n-k-1}}-1)}\)
代入发现是错的
为什么呢?因为 \(\displaystyle{\binom{n}{k}(2^{2^{n-k}}-1)}\) 中元素个数 \(k+1\) 的会被算 \(\displaystyle{\binom{k+1}{k}}\) 次,后面只减了一次,肯定是错的。
那我们这样不就完了吗? \(\displaystyle{\binom{n}{k}(2^{2^{n-k}}-1)-\binom{k+1}{k}\binom{n}{k+1}(2^{2^{n-k-1}}-1)}\)
还是错的 为什么呢?因为元素个数 \(k+2\) 的也会被多减!我们到这就看出来是 容斥 了。
用人类智慧猜一猜 \(\displaystyle{\sum_{k\le i\le n}(-1)^{i-k}\binom{i}{k}\binom{n}{i}(2^{2^{n-i}}-1)}\)
交上去是对的。交完我证了一下容斥,发现是对的(容斥只要证明后面的答案计算次数是 \(0\) )
(事实上,把 \(-1\) 去掉,式子改成 \(\displaystyle{\sum_{k\le i\le n}(-1)^{i-k}\binom{i}{k}\binom{n}{i}2^{2^{n-i}}}\) 也是对的)
正确的思路历程
设 \(F_i\) 表示 \(i\) 个元素的集合,选择若干个子集,他们的交集为空集的方案数,那我们的答案就是 \(\displaystyle{\binom{n}{k}F_{n-k}}\)
然后呢,我们发现 \(\displaystyle{\sum_{i=0}^n \binom{n}{i}F_{n-i}}\) 就是全部的方案数,即 \(2^{2^n}\)
于是 \(\displaystyle{\sum_{i=0}^n \binom{n}{i}F_{i}}=2^{2^n}\)
二项式反演即得上述式子
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int P = 1e9+7, N = 1e6+5;
int n,k;
int ksm(int a, int b, int p)
{
int res=1;
for(; b; b>>=1,a=(ll)a*a%p) if(b&1) res=(ll)a*res%p;
return res;
}
int fc[N],fci[N];
int C(int n, int m)
{
if(m<0 or n<0 or n<m) return 0;
return (ll)fc[n]*fci[m]%P*fci[n-m]%P;
}
int main()
{
scanf("%d%d",&n,&k);
fc[0]=1;
for(int i=1; i<=n; i++) fc[i]=(ll)fc[i-1]*i%P;
fci[n]=ksm(fc[n],P-2,P);
for(int i=n; i; i--) fci[i-1]=(ll)fci[i]*i%P;
int sgn=1; ll ans=0;
for(int i=k; i<=n; i++)
{
int res=(ll)C(n,i)*C(i,k)%P*(ksm(2,ksm(2,n-i,P-1),P)%P-1+P)%P;
ans=(ans+res*sgn+P)%P; sgn=-sgn;
}
printf("%lld\n",ans);
return 0;
}
二项式反演
我一直以为这东西很难,需要高端的组合技巧。
事实上它的难点是背式子!这东西其实非常好证(
首先你得会这两个式子
一些前置的组合数性质
1
左边:从 \(n\) 个数选 \(m\) 个,再从 \(m\) 个数选出 \(k\) 个
右边:从 \(n\) 个数选 \(k\) 个,再从剩下的 \(n-k\) 里选 \(m-k\) 个
这两个显然是等价的!(事实上大部分组合数性质都有组合解释)
2 二项式定理的推论
二项式定理
证明自己搜,这是数学基础知识)
由这个我们有一些推论
二项式反演的证明
你要证这个:
带进去不就好了:
交换求和号:
注意到右边的式子可以化(运用上述的两个性质):
于是我们就要证:
这显然 诶这不就证完了!
例1.2 「KDOI-02」一个仇的复 (组合数卷积)
题意
题意是用 \(k\) 个 \(1\times x\) 的长方形覆盖一个 \(2\times n\) 的条的方案数
题解
首先发现一定是几段区间只有横条,其他是竖条
考虑先求出放 \(i\) 个竖条,把 \(1\sim n\) 分成 \(j\) 段的方案数
这相当于先枚举 \(j+1\) 个空隙中每个插多少个竖条,再枚举空隙的位置
即:
接着呢需要考虑这 \(j\) 段全是横条的方案数
考虑个简化版问题:\(2\times n\) 只有 \(k\) 个横条,没有竖条的方案数
很简单是吧ww
可以考虑组合意义:往 \(2n-2\) 个空隙里插 \(k-2\) 个板
或者推推式子:\(\sum_{i=1}^{k-1}\binom{n-1}{i}\binom{n-1}{k-i}=\binom{2n-2}{k}\)(范德蒙德卷积)
所以结果就是 \(\binom{2n-2}{k-2}\)
那如果有 \(j\) 段区间,分别有 \(a_1,\dots, a_j\) 个长方形
其实就是
这个式子怎么做好难不会ww
首先可以组合意义考虑:这个式子表示从 \(\sum2a_t−2\) 个位置中,选取 \(\sum b_t-2\) 的所有方法,原式的意义就是先枚举每个段落选几个,再把所有方案相加,本质上就是从整个序列中选。那么答案其实就是
或者你可以从生成函数角度考虑:
这是个多元卷积,若令 \(F_t(x)=(1+x)^{2a_t-2}\) ,则答案为 \([x^{k-i-2j}](F_1(x)\ast F_2(x)\ast \dots \ast F_j(x))=\binom{2n-2i-2j}{k-i-2j}\)
故最终答案为
需要注意的是 ,可以全放竖条,这样就没有连续段,这一种情况在式子中没有,所以要加上 \([n=k]\)
效率 \(O(k^2)\)
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 4e7+5, P = 998244353;
int n,k,fc[N],fci[N];
int qpow(int a, int b) { int r=1; for(; b; b>>=1,a=(ll)a*a%P) if(b&1) r=(ll)a*r%P; return r; }
int C(int n, int m) { if(n<0 or m<0 or n<m) return 0; else return (ll)fc[n]*fci[m]%P*fci[n-m]%P; }
int main()
{
scanf("%d%d",&n,&k); fc[0]=1; for(int i=1; i<=(n<<1); i++) fc[i]=(ll)fc[i-1]*i%P;
fci[n<<1]=qpow(fc[n<<1],P-2); for(int i=(n<<1); i; i--) fci[i-1]=(ll)fci[i]*i%P;
int ans=0; for(int i=1; i<=k; i++) for(int j=0; j<=k; j++) (ans+=(ll)C((n-j-i)<<1,k-j-(i<<1))*C(j+1,i)%P*C(n-j-1,i-1)%P)%=P;
printf("%d\n",ans+(n==k));
return 0;
}
例 1.3 六省联考 2017] 组合数问题 (组合数循环卷积 生成函数)
题意
\(\sum_{i=0}^{+\infty} \binom{nk}{ik+r}\bmod p\)
题解
发现是个循环卷积
学会了循环卷积是模 \(x^k-1\)
直接快速幂+暴力卷积 \(O(k^2\log k\log n)\) ,注意 \(k=1\) 特判
NTT 可做到 \(O(klogn)\) 可惜模数不是质数
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 105;
int n,P,k,r;
struct Poly
{
int a[N];
int & operator[] (int x) { return a[x]; }
void clear() { memset(a,0,sizeof(a)); }
friend Poly operator* (Poly &a, Poly &b)
{
Poly c;
for(int i=0; i<(k<<1); i++) { c[i]=0; for(int j=0; j<=i; j++) (c[i>=k?i-k:i]+=(ll)a[j]*b[i-j]%P)%=P; }
return c;
}
} res,f;
Poly qpow(Poly a, ll b)
{
res.clear(); res[0]=1;
for(; b; b>>=1,a=a*a) if(b&1) res=a*res;
return res;
}
int main()
{
scanf("%d%d%d%d",&n,&P,&k,&r); f.clear();
if(k==1) f[0]=2%P; else f[0]=f[1]=1;
f=qpow(f,(ll)n*k); printf("%d\n",f[r]);
return 0;
}
2. 网格图路径计数
做了好多这种题,我们做个总结!反正类似卡塔兰数
多次容斥的套路题
就是两条线之间夹着,反复对称
例2.1.1
把求最长不下降子序列数转成网格图路径计数
例2.1.2
例2.1.3
例 2.2 (未限制方向的方案数)
多重集排列
\(\displaystyle\frac{n!}{m_1!m_2!\dots m_k!}\)
范德蒙德卷积
\(\displaystyle{\sum_i{\binom{n}{i}\binom{m}{k-i}}=\binom{n+m}{k}}\)
从生成函数的角度来理解很简单,有:
组合意义:从 \(n+m\) 选 \(k\) 个,可以枚举在 \(n\) 个中选择 \(i\) 个,则 \(m\) 个中就选择 \(k-i\) 个
常见套路——坐标系转化
\((x,y)\rightarrow (x+y,x-y)\)
\((0,\pm1)\rightarrow(\pm1,\mp1)\)
\((\pm1,0)\rightarrow(\pm1,\pm1)\)
例2.3
\((0,0)\) 到 \((n,m)\) 的方案数,不能经过障碍点
题解
设 \(f_i\) 表示第一次到达的是 \(i\) 号障碍点。
转移显然
代码
#include <bits/stdc++.h>
#define ll long long
#define x first
#define y second
using namespace std;
const ll N = 2e5+5;
const ll M = 3e3+5;
const ll mod = 1e9+7;
typedef pair<ll,ll> P;
ll n,m,k,mn;
ll fc[N],fci[N];
ll f[N];
P h[N];
ll qpw(ll a, ll b)
{
ll ans=1;
for(;b;b>>=1)
{
if(b&1) ans=ans*a%mod;
a=a*a%mod;
}
return ans;
}
void prework()
{
fc[0]=1;
for(ll i=1; i<=mn; i++)
fc[i]=fc[i-1]*i%mod;
fci[mn]=qpw(fc[mn],mod-2);
for(ll i=mn-1; i>=0; i--)
fci[i]=fci[i+1]*(i+1)%mod;
}
inline ll C(ll yy, ll xx) {return fc[yy]*fci[xx]%mod*fci[yy-xx]%mod; }
int main()
{
scanf("%lld%lld%lld",&n,&m,&k); mn=n+m;
for(ll i=1; i<=k; i++) scanf("%lld%lld",&h[i].x,&h[i].y);
sort(h+1,h+1+k);
h[++k]=make_pair(n,m);
prework();
for(ll i=1; i<=k; i++)
{
f[i]=C(h[i].x+h[i].y,h[i].x);
for(ll j=1; j<i; j++)
f[i]=(f[i]-f[j]*C(h[i].x+h[i].y-h[j].x-h[j].y,h[i].x-h[j].x)%mod+mod)%mod;
}
printf("%lld\n",f[k]);
return 0;
}