基本概念
基本计数原理:
分类计算加法原理,分布计算乘法原理。
简单容斥与摩根定理:
- \(\begin{vmatrix}A\cup B\end{vmatrix}=\begin{vmatrix}A\end{vmatrix}+\begin{vmatrix}B\end{vmatrix}-\begin{vmatrix}A\cap B\end{vmatrix}\)
- \(\begin{vmatrix}A\cup B\cup C\end{vmatrix}=\begin{vmatrix}A\end{vmatrix}+\begin{vmatrix}B\end{vmatrix}+\begin{vmatrix}C\end{vmatrix}-\begin{vmatrix}A\cap B\end{vmatrix}-\begin{vmatrix}A\cap C\end{vmatrix}-\begin{vmatrix}C\cap B\end{vmatrix}+\begin{vmatrix}A\cap B\cap C\end{vmatrix}\)
\[\left| \bigcup_{i=1}^n A_i\right|=\sum_{\varnothing\not={\rm J}\subseteq\{1,\cdots,n\}}(-1)^{\begin{vmatrix}{\rm J}\end{vmatrix}+1}\left| \bigcap_{{\rm j}\in{\rm J}} A_j\right|\\ \]代码可以利用 \(dfs\) 或状压实现
摩根定理: 交集的补等于补集的并,并集的补等于补集的交。
即 \(\overline{A\cup B}=\overline{A}\cap\overline{B}\) \(\overline{A\cap B}=\overline{A}\cup\overline{B}\)
\[\left| \bigcap_{i=1}^n \overline{A_i}\right|=\sum_{{\rm J}\subseteq\{1,\cdots,n\}}(-1)^{\begin{vmatrix}{\rm J}\end{vmatrix}}\left| \bigcap_{{\rm j}\in{\rm J}} A_j\right|\\ \left| \bigcap_{i=1}^n A_i\right|=\sum_{{\rm J}\subseteq\{1,\cdots,n\}}(-1)^{\begin{vmatrix}{\rm J}\end{vmatrix}}\left| \bigcap_{{\rm j}\in{\rm J}} \overline{A_j}\right|\\ \]
组合计数:
排列数:
从 \(n\) 个不同元素中依次取出 \(m\) 个元素排成一列,产生的不同排列的数量为:(取 \(m\) 个,将 \(m\) 个排序)
组合数:
从 \(n\) 个不同元素中取出 \(m\) 个组成一个集合(不考虑顺序),产生的不同集合的数量为:
性质:
- \(A_n^k=C_n^k\times k!\)
- \(C_n^m=C_n^{n-m}\)
- \(C_n^m=C_{n-1}^m+C_{n-1}^{m-1}\) (杨辉三角)
- \(\frac{k}{n}\times C_n^k=C_{n-1}^{k-1}\)
- \(\sum_{i=0}^nC_n^i=2^n\)
- \(\sum_{i=0}^n(-1)^iC_n^i=0\)
组合数求解:
单个组合数 \(O(n)\)
代码
int C(int n, int k) {
int p = 1, q = 1;
for (int i = n - k + 1; i <= n; ++i)
p *= i;
for (int i = 1; i <= k; ++i)
q *= i;
return p / q;
}
求 \(C_i^j(i\in[0,n-1],j\in[1,i])\) 递推 \(O(n^2)\)
代码
for (int i = 0; i < n; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j) {
C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
}
}
二项式定理:
高精组合数:
求 \(C_n^m\) 的值,结果可能很大,没有模数。
\({\rm sol}:\)
\(C_n^m=\frac{n!}{m!\times (n-m)!}\)
$\therefore $ 所以可以对 \(n!\) 阶乘进行质因数分解为 \(\prod p_i^{c_i}\) (方法在基础数论博客里)
然后对 \(m!\) 和 \((n-m)!\) 分别进行质因数分解,每次使 \(c_i\) 减一。
最后统计结果。
时间复杂度 \(O(n)\) (高精另算)
代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 5010;
int primes[N], cnt;
int sum[N];
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int get(int n, int p) // 求n!中 质因子 p 需要累乘的次数
{
int res = 0;
while (n)
{
res += n / p;
n /= p;
}
return res;
}
vector<int> mul(vector<int> a, int b)
{
vector<int> c;
int t = 0;
for (int i = 0; i < a.size(); i ++ )
{
t += a[i] * b;
c.push_back(t % 10);
t /= 10;
}
while (t)
{
c.push_back(t % 10);
t /= 10;
}
return c;
}
int main()
{
int a, b;
cin >> a >> b;
get_primes(a);//用欧拉筛筛出 [1~a] 范围内的质数
for (int i = 0; i < cnt; i ++ )
{
int p = primes[i];
sum[i] = get(a, p) - get(a - b, p) - get(b, p); // C(a,b) 中第 i 个质数需要累乘的次数
}
vector<int> res;
res.push_back(1);
for (int i = 0; i < cnt; i ++ ) // 枚举质因子
for (int j = 0; j < sum[i]; j ++ ) // 枚举当前质因子的个数
res = mul(res, primes[i]); // 做高精度乘低精度
for (int i = res.size() - 1; i >= 0; i -- ) printf("%d", res[i]);
puts("");
return 0;
}
多重集排列数:
\(k\) 种元素,有 \(n_1\) 个 \(a_1\) , \(n_2\) 个 \(a_2\) ,\(\cdots\) , \(n_k\) 个 \(a_k\) 。有多少种排列方案。
\[\frac{(n_1+n_2+\cdots+n_k)!}{n_1!\times n_2!\times\cdots\times n_k!} \]
多重集组合数:
1.
\(k\) 种元素,有 \(n_1\) 个 \(a_1\) , \(n_2\) 个 \(a_2\) ,\(\cdots\) , \(n_k\) 个 \(a_k\) 。从中取出 \(r\) ( \(\forall i\in[1,k],r\leq n_i\) )个的方案数。
设 \(r\) 个元素构成的集合是 \(\{x_1·a_1,x_2·a_2,\cdots,x_k·a_k\}\)
\(\therefore x_1+x_2+\cdots+x_k=r\) 且 \(x_i\geq 0\) (隔板法)
答案为 \(C_{r+k-1}^{k-1}\)
2.
\(k\) 种元素,有 \(n_1\) 个 \(a_1\) , \(n_2\) 个 \(a_2\) ,\(\cdots\) , \(n_k\) 个 \(a_k\) 。从中取出 \(r\) ( \(r\leq \sum_{i=1}^kn_i\) )个的方案数为:
若先不考虑 \(n_i\) 的限制,从 \(S=\{\infty · a_1,\infty · a_2,\cdots,\infty · a_k\}\) 中取出 \(r\) 个元素,则方案数为 \(C_{k+r-1}^{k-1}\)
设 \(S_i\) 表示包含至少 \(n_i+1\) 个 \(a_i\) 的多重集,即从 \(S\) 中先取出 \(n_i+1\) 个 \(a_i\), 然后再任选 \(r-n_i-1\) 个元素构成 \(S_i\) ,可得不同的 \(S_i\) 的数量为 \(C_{k+r-n_i-2}^{k-1}\)
那么进一步思考,从 \(S\) 中先取出 \(n_i+1\) 个 \(a_i\) 和 \(n_j+1\) 个 \(a_j\) ,然后再任选 \(r-n_i-n_j-2\) 个元素,构成的集合即为 \(S_i\cap S_j\) ,方案数为 \(C_{k+r-n_i-n_j-3}^{k-1}\)
根据容斥原理可得:\[\left| \bigcup_{i=1}^kS_i \right| =\sum_{i=1}^kC_{k+r-n_i-2}^{k-1}-\sum_{1\leq i<j\leq k}C_{k+r-n_i-n_j-3}^{k-1}+\cdots+(-1)^{k+1}C_{k+r-\sum_{i=1}^kn_i-(k+1)}^{k-1} \]故答案为 \(C_{k+r-1}^{k-1}-\left|\cup_{i=1}^kS_i\right|\)
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll s=0,k=1;
char c=getchar();
while(c>'9'||c<'0'){
if(c=='-')k=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
s=(s<<3)+(s<<1)+(c^48);
c=getchar();
}
return s*k;
}
const int mod=1e9+7;
ll a[25],n,m,ans,inv[25];
ll ksm(ll a,ll b){
ll t=1;
for(;b;b>>=1,a=a*a%mod)
if(b&1) t=t*a%mod;
return t;
}
ll C(ll y,ll x){
if(x<0||y<0) return 0;
if(x>y) return 0;
y%=mod;
ll ans=1;
for(int i=0;i<x;i++) ans=ans*(y-i)%mod;
(ans*=inv[x])%=mod;
if(y==0) return 1;
return ans;
}
int main(){
ll t=1;
for(int i=1;i<=20;i++) t=t*i%mod;
inv[20]=ksm(t,mod-2);
for(int i=19;i>=0;i--) inv[i]=inv[i+1]*(i+1)%mod;
n=read();m=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int j=0;j< 1<<n ;j++){
if(j==0) ans=(ans+C(n+m-1,n-1))%mod;
else{
ll tt=n+m;
ll s=0;
for(int i=0;i<n;i++)
if(j>>i&1){
s++;
tt-=a[i+1];
}
tt-=s+1;
if(s&1) ans=(ans-C(tt,n-1))%mod;
else ans=(ans+C(tt,n-1))%mod;
}
}
printf("%lld\n",(ans+mod)%mod);
return 0;
}
例题:
数字求和:
设 \(f(x)\) 为 \(x\) 在十进制下的各位数字之和,例如 \(f(123) = 1+2+3=6\) ,求
答案对 \(10^9+7\) 取模。
考虑除去 \(10^n\) 这一个数字之外的其他数字中:
\(\forall i \in [1,9]\) ,都会在每一位上出现 \(10^{n-1}\) 次。
所以这些数字对答案的贡献就是 \(\sum_{i=1}^9i\times n\times 10^{n-1}=45\times n\times 10^{n-1}\)
故答案为 \(45\times n\times 10^{n-1}+1\)
圆盘染色:
\(n\) 块组成一个圆盘,用 \(m\) 种颜色染色,使得相邻两块的颜色不同。求方案数。
考虑如果 \(n\) 的颜色与 \(n-2\) 相同,则将 \(n-2,n-1,n\) 看作一个整体。
那么 \(n-1\) 块有 \(m-1\) 种选择,则方案数为 \((m-1)\times f(n-2)\) ,( \(f(i)\) 表示分成 \(i\) 块的方案数)
考虑如果 \(n\) 的颜色与 \(n-2\) 不同,将 \(n-2,n-1,n\) 看作一个整体。
那么 \(n-1\) 块有 \(m-2\) 种选择,则方案数为 \((m-2)\times f(n-1)\) 。
\(\therefore f(n)=(m-1)\times f(n-2)+(m-2)\times f(n-1)\)
( \(f(1)=0,f(2)=m\times(m-1),f(3)=m\times (m-2)\) )
数三角形:
给定一个 \(N\times M\) 的网格,请计算三点都在格点上的三角形共有多少个。注意三角形的三点不能共线。
【三角形数量】等于【任选三个点的方案数】减去【三点共线的方案数】
【三点共线的方案数】等于【横着的】加【竖着的】加【斜着的】
【横着的】:\((m+1)\times C_{n+1}^3\)
【竖着的】:\((n+1)\times C_{m+1}^3\)
斜着的直线按斜率正负分为两种,并且这两种的方案数是相等的。因此,我们只需要计算出斜率为正的方案数,再乘以 \(2\) 即可。将核心计算内容——斜率为正的方案数记为 \(ans\) 。
假设网格中 \(AB\) 平行于底边 \(x\) 轴,长度为 \(i\) , \(BC\) 平行于侧边 \(y\) 轴,长度为 \(j\) ,那么 \(AC\) 上的整点数量为 \(\gcd(i,j)-1\) (不算 \(A,C\) 两点)。\[\therefore ans=\sum_{i=1}^n\sum_{j=1}^m(n-i+1)(m-j+1)(\gcd(i,j)-1) \]此时还能继续化简:
\[\begin{array}{l} =\sum_{i=1}^n\sum_{j=1}^m(n-i+1)(m-j+1)(\sum_{d\mid \gcd(i,j)}\varphi(d)-1)\\ =\sum_{i=1}^n\sum_{j=1}^m(n-i+1)(m-j+1)\sum_{d\mid\gcd(i,j)}^{d\neq 1}\varphi(d)\\ =\sum_{d=2}^{{\rm min}(n,m)}\varphi(d)\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}(n-id+1)\sum_{j=1}^{\lfloor\frac{m}{d}\rfloor}(m-jd+1)\\ =\frac{1}{4}\sum_{d=2}^{{\rm min}(n,m)}\varphi(d)(n-d+n\% d+2)\lfloor\frac{n}{d}\rfloor(m-d+m\% d+2)\lfloor\frac{m}{d}\rfloor\\ (等差数列) \end{array} \]至此可以 \(O(n)\) 求得答案。
Counting swaps:
给定你一个 \(1∼n\) 的排列 \(p\) ,可进行若干次操作,每次选择两个整数 \(x,y\) ,交换 \(px,py\) 。用最少的操作次数将给定排列变成单调上升的序列 \(1,2,\cdots,n\) ,有多少种方式呢?
对于一个排列 \(p_1,p_2,\cdots,p_n\) ,每一个 \(p_i\) 向 \(i\) 连一条无向边,构成一张由若干环组成的无向图,目标状态即为 \(n\) 个自环。
设 \(f[i]\) 表示一个大小为 \(i\) 的环,在保证交换次数最少的情况下,有多少种方法将其变成目标状态。
每一次交换可以把大小为 \(i\) 的环拆成大小为, \(x,y\) 的两个环(\(x+y=n\)) 。设 \(T(x,y)\) 表示有多少种交换方法可以将一个大小为 \(i\) 的环拆成两个大小分别为 \(x,y\) 的环。可以发现:当 \(x=y\) 时, \(T(x,y)=x\) ,否则 \(T(x,y)=x+y\) 。
\(x\) 环需要 \(x−1\) 次交换达到目标状态,同理 \(y\) 环需要 \(y−1\) 次操作,由于 \(x\) 环和 \(y\) 环的操作互不干扰且可以随意排列,因此得到转移方程:
\[f[i]=\sum_{x+y=i}f[x]\times f[y]\times T(x,y)\times \frac{(i-2)!}{(x-1)!\times (y-1)!} \](在打表后可发现 \(f[n]=n^{n-2}\))
假设排列中有 \(k\) 个大小为 \(L_1,L_2,\cdots,L_k\) 的环,则\[Ans=(\prod_{i=1}^kf[L_i])\times (\frac{(n-k)!}{\prod_{i=1}^k(L_i-1)!}) \]