【集训】组合计数
题单
不是自己写的,总结的
交换求和顺序:
- $\sum_{i=1}^{n} \sigma_{0}(i)=\sum_{i=1}^{n} \sum_{d \mid i} 1=\sum_{d=1}^{n} \sum_{i=1}^{n}[d \mid i]=\sum_{d=1}^{n}\left\lfloor\frac{n}{d}\right\rfloor $
- 给定序列 \(a\) ,求其所有区间的区间和:答案是 $\sum_{i=1}^{n} a_{i} \times i \times(n-i+1) $。
二项式定理:
常见组合恒等式:
先从 \(n\) 个中选出 \(m\) 个,再从选出的 \(m\) 个里面选出 \(k\) 个 \(\Longleftrightarrow\) 先从 \(n\) 个中选出最终的 \(k\) 个,再从剩下的 \(n-k\) 个中选出第一步选中但第二步末选中的 \(m-k\) 个。
利用\(\binom{n}{m}= \frac{n!}{m!(n-m)!}\) 展开即可得到证明。
相当于从 \(n\) 个元素中选出 \(k\) 个,但前 \(x\) 个元素中必须选出至少 \(w\) 个;这等价于选出的第 \(w\) 个元素的位置在 \(x\) 之前。
考察 \(n+m+1\) 元集的 \(n+1\) 元子集的最后一个元素。
从 \(n+m\) 个元素中选出前 \(k\) 个,考察前 \(n\) 个元素中选出的数的个数。
常见模型:
有 \(n\) 种物品,第 \(i\) 种物品有 \(a_i\) 个,认为相同物品之间互不区分,则将这 \(\sum a_i\) 个物品排列的方案数为
将 \(n\) 个相同的物品无序地分为 \(m\) 组,每组都非空的方案数:相当于往 \(n-1\) 个空位里面插入 \(m-1\) 个板,因此方案数为 \(\binom{n-1}{m-1}\)。
如果不要求每组非空,考虑直接给每组都加一个,方案数为 \(\binom{n+m-1}{m-1}\)。
求方程 \(\sum_{i=1}^m x_i = n, x_i \in \mathbb{Z}, x_i \geq 0\) 的整数解个数:\(\binom{n+m-1}{m-1}\)。
如果再给出序列 \(y_1...m\),要求 \(x_i \geq y_i\)?
相当于 \(\sum_{i=1}^m (x_i - y_i) = n - \sum_{i=1}^m y_i\),故方案数为 \(\binom{n+m-\sum_{i=1}^m y_i-1}{m-1}\)。
如果要求 \(x_i \leq y_i\) 呢?此时有两种做法:
- DP:\(f(i, j)\) 表示前 \(i\) 个数和为 \(j\) 的方案数,转移用前缀和优化。\(O(nm)\)
- 容斥:钦定 \(S \subseteq \{1, 2, \cdots, m\}\) 以内的限制不被满足,其余的不管,转化为要求 \(x_i > y_i\) 的情形。\(O(2^m \times \text{poly}(m))\)
求有多少长为 \(2n\) 的合法括号序列。答案记作 \(C_n\),这就是卡特兰数。
把左括号看成往右走,有括号看成往上走,那么这个就对应一条 \((0,0)\) 到 \((n,n)\) 的路径,且不能穿过 \(y=x\)(但可以触碰)。
路径一共有 \(\binom{2n}{n}\) 种,考虑怎么算穿过的方案数,也就是触碰了 \(y=x+1\) 这条直线。
我们在最后触碰的位置 \((p, p+1)\) 把 \((0,0) \rightarrow (p, p+1)\) 这一部分翻折,发现就唯一对应一条 \((-1,1) \rightarrow (n,n)\) 的路径,于是方案数就是 \(\binom{2n}{n-1}\)。于是我们有以下这样的关系
这就是卡特兰数。
另一方面,从括号序列的角度,我们还能给出另一种递推式:枚举第一个左括号匹配的右括号的位置为 \(2k\),有
其中 \(C_0 = 1\)。
卡特兰数 \(C_n\) 常用套路
- 对角线不相交的情况下,将一个 \(n\) 条边的凸多边形区域分成三角形区域的方法数,即三角剖分的方案数。
- 一个大小为 \(n\) 的栈,依次入栈 \(1, 2, \cdots, n\),但可以在任意时机出栈,问有多少种出栈序。
- \(n\) 个节点的无标号有根二叉树的个数。
P2671 [NOIP2015 普及组] 求和
现在考虑一组中有 \(m\) 个格子,则这一组对答案的贡献为
若枚举 \(i,j\) 暴力计算,则复杂度为 \(O(n^2)\),会 TLE,所以需要进一步优化
现在我们把这个式子拆开(把括号打开)
第一个式子可以化简为
第二个式子可以化简为
第三个式子 \(\sum\limits_{i=1}^{m} \sum\limits_{j=1}^{i-1} i\times a_j\) 比较麻烦, 我们可以考虑每个 \(a_j\) 对答案的贡献
比如考虑 \(j=1\) 时的 \(a_1\),我们知道它的贡献应该是 \(\sum\limits_{i=2}^{m} i\times a_1\)
也就是说,当 \(i=2\sim m\) 时,会存在 \(i\times a_1\) 这一项
考虑 \(j=2\) 时的 \(a_2\) ,我们知道它的贡献应该是 \(\sum\limits_{i=3}^{m} i\times a_2\)
也就是说,当 \(i=3\sim m\) 时,会存在 \(i\times a_2\) 这一项
以此类推,第三个式子可以化简为
对于第四个式子,我们可以考虑每个 \(j\times a_j\) 对答案的贡献
当 \(i=2\sim m\) 时,\(1\times a_1\) 这一项会出现一次
当 \(i=3\sim m\) 时,\(2\times a_2\) 这一项会出现一次
以此类推,第四个式子可以化简为
整理一下,可得:
简单维护即可。
#include <bits/stdc++.h>
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
#define FOR(i,a,b) for(int i=(a);i<=(b);++i)
#define ROF(i,a,b) for(int i=(a);i>=(b);--i)
#define U unsigned
#define LL long long
using namespace std;
template<class T>
inline void read(T &a){ register U LL x=0,t=1; register char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') t=-1; ch=getchar();} while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } a=x*t;}
inline void print(LL x){if(x<0)putchar('-'),x=-x;if(x>9) print(x/10);putchar(x%10+'0');}
const int mod=10007;
const int maxn=1e5+5;
int n,m;
int a[maxn],b[maxn];
int s1[maxn][2],s2[maxn][2];
int ans;
void sovle(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) {
cin>>b[i];
s1[b[i]][i%2]++;
s2[b[i]][i%2]=(s2[b[i]][i%2]+a[i])%mod;
}
for(int i=1;i<=n;i++){
int y=b[i];
ans+=i*(s2[y][i%2]+a[i]*(s1[y][i%2]-2)%mod)%mod;
ans%=mod;
}
cout<<ans<<endl;
}
signed main(){
sovle();
return 0;
}
把同色,同奇偶性的放在一起考虑。设当前分离出来的这一组编号分别为 \(x_1, \cdots, x_k\),权值分别为 \(y_1, \cdots, y_k\),则贡献为
预处理 \(x, y, xy\) 的后缀和即可。时间复杂度为 \(O(n)\)。
AT_abc266_g [ABC266G] Yet Another RGB Sequence
首先我们考虑把 \(\texttt{rg}\) 算成字符 \(\texttt{\#}\)。
题面即变为:求 \(R - k\) 个 \(\texttt{r}\),\(G-k\) 个 \(\texttt{g}\),\(B\) 个 \(\texttt{b}\),\(k\) 个 \(\texttt{\#}\) 组成不含 \(\texttt{rg}\) 的序列的方案数。
此时,我们先把 \(\texttt{g}\),\(\texttt{b}\) 和 \(\texttt{\#}\) 排好,方案即为 \(C_{G-k +B+k}^{G-k} \times C_{B+k}^k\)。
再考虑把 \(R-k\) 个 \(\texttt{r}\) 加入,此时不能将 \(\texttt{r}\) 放在 \(\texttt{g}\) 前面,所以这一步有 \(C_{R-k+B+k}^{R-k}\) 种方案。
故共有 \(C_{G+B}^{G-k} \times C_{B+k}^k \times C_{R+B}^{R-k}\) 种方案。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int mod=998244353;
ll a,b,c,d,ans;
ll poww(ll a,ll b){
ll res=1;
while(b){
if(b&1) res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
ll C(int n,int m){
if(m==0) return 1;
ll res=1;
for(int i=n;i>=n-m+1;i--)
res=res*i%mod;
for(int j=1;j<=m;j++)
res=res*poww(j,mod-2)%mod;
return res;
}
int main(){
cin>>a>>b>>c>>d;
a-=d,b-=d;
ans=C(c+d+b,b)*C(d+c,d)%mod*C(c+d+a,a)%mod;
cout<<ans<<endl;
return 0;
}
AT_abc240_g [ABC240G] Teleporting Takahashi
当 \(N<|X|+|Y|+|Z|\) 时无解。
否则,考虑先算出将 \(a\) 步分配到一个长为 \(b\) 的维度上,发现方案数是
不妨考虑两维的情况
扩展到三维,答案就是
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=998244353,N=2e7+5;
ll fac[N],ifac[N],n,X,Y,Z;
int ksm(int x,int y,int p=mod){
int ans=1;
for(int i=y;i;i>>=1,x=1ll*x*x%p)if(i&1)ans=1ll*ans*x%p;
return ans%p;
}
ll inv(int x,int p=mod){
return ksm(x,p-2,p)%p;
}
void add(ll &x,ll v){
x+=v;
if(x>=mod) x-=mod;
}
ll C(int x,int y){
if(x<y||y<0) return 0;
return 1ll*fac[x]*ifac[y]%mod*ifac[x-y]%mod;
}
ll F(int m){
if((m+X+Y)%2!=0) return 0;
return 1ll*C(m,(m+X+Y)/2)*C(m,(m+X-Y)/2)%mod;
}
signed main(){
cin>>n>>X>>Y>>Z;
fac[0]=1;
for(int i=1;i<=n*2;i++) fac[i]=1ll*fac[i-1]*i%mod;
ifac[n*2]=inv(fac[n*2]);
for(int i=n*2-1;i>=0;i--) ifac[i]=1ll*ifac[i+1]*(i+1)%mod;
ll ans=0;
for(int z=0;z<=n;z++)
if((z+Z)%2==0)
add(ans,1ll*F(n-z)*C(n,z)%mod*C(z,(z+Z)/2)%mod);
cout<<ans<<endl;
return 0;
}
容斥原理 & 二项式反演
如果有 \(n\) 个约束 \(p_1, p_2, \cdots, p_n\),我们要求计数的对象符合所有约束;但符合约束时不易计算,反而是不符合约束时容易计算,此时便可以采用容斥的手法。
具体地,我们枚举一个约束的集合 \(S\),钦定 \(S\) 中的约束必须不被满足,\(S\) 之外的约束可以满足也可以不满足,再附上 \((-1)^{|S|}\) 的容斥系数,即可算出符合所有约束的方案数。
例:计算有多少个长为 \(n\) 的排列 \(a\) 满足:对每个 \(i = 1, 2, \cdots, n\),都有 \(a_i \neq i\)。
如果各个约束之间本质相同,即钦定违反的集合 \(S\) 之后,方案数只和 \(S\) 的大小 \(|S|\) 有关,我们就得到了二项式反演:
\(g_i\) 的实际含义是,钦定某 \(i\) 个元素被选中,其余不做要求的方案数。也就是说,选中恰好 \(j\) 个元素的方案会被算 \(\binom{j}{i}\) 次,即 \(g_i = \sum_{j=i}^{n} \binom{j}{i} f_j\)。
常见容斥模型:
-
有 \(n\) 个球排成一列,每个球可以染成 \(m\) 种颜色中的一种,相邻球的颜色不能相同,但是每种颜色至少出现一次,问方案数。
-
计算有多少个长为 \(m\) 的序列 \(x\) 满足:\(\sum_{i=1}^{m} x_i = n\),且对每个 \(i = 1, 2, \cdots, m\) 有 \(0 \leq x_i \leq y_i\),其中 \(y_i\) 是给定的序列。
P10596 BZOJ2839 集合计数
给定 \(n, k\)。令 \(S_0 = \{1, 2, 3, \dots, n\},S_1 = \{S \mid S \subseteq S_0\}\),求有多少 \(S_1\) 的子集 \(S\) 满足 \(\left|\bigcap\limits_{S \in S_1} S\right| = k\)。
仍然是钦定。设 \(f(k)\) 表示「至少」\(k\) 个元素是集合的交集,即钦定 \(k\) 个元素作为集合的交集,剩余不做限制的方案数,会有重复。有:
设 \(g(k)\) 表示恰好 \(k\) 个元素是集合的交集的方案数,有:
二项式反演得:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 7,mod = 1000000007;
int ksm(int a, int b, int md) {
int res = 1;
while(b) {
if(b & 1) res = res * a % md;
a = a * a % md;
b >>= 1;
}
return res % md;
}
int fac[N], infac[N];
int getc(int a, int b) {
return fac[a] % mod * infac[b] % mod * infac[a - b] % mod;
}
signed main() {
int n, k;
cin >> n >> k;
fac[0] = infac[0] = 1;
for(int i = 1; i < N; i ++) {
fac[i] = fac[i - 1] * i % mod;
infac[i] = ksm(fac[i], mod - 2, mod) % mod;
}
int ans = 0,p = getc(n, k);
n -= k;
for(int i = n; i >= 0; i --) {
if(i % 2 == 0)
ans = (ans + getc(n, i) % mod * (ksm(2ll, ksm(2ll, n - i, mod - 1), mod) - 1) % mod) % mod;
else
ans = (ans - getc(n, i) % mod * (ksm(2ll, ksm(2ll, n - i, mod - 1), mod) - 1) % mod + mod) % mod;
ans %= mod;
}
cout << (ans * p + mod) % mod << endl;
}
P4859 已经没有什么好害怕的了
给出 \(n\) 个数 \(a_i\) ,以及 \(n\) 个数 \(b_i\) ,要求两两配对使得 \(a>b\) 的对数减去 \(a<b\) 的对数等于\(k\) 。
我们假设 \(a>b\) 对数为 \(x\) ,可以求得 \(x=\frac{n+k}{2}\) 。 我们令 \(f_{i,j}\) 表示前 \(i\) 个 \(a\) 中,选了 \(j\) 组满足 \(a>b\) 的方案数。
容易得到 \(\text{dp}\) 方程 $$f_{i,j}=f_{i-1,j}+(l_i-j+1)\times f_{i-1,j-1}$$ 其中 \(l_i\) 表示 \(b\) 中 \(<a_i\) 的个数。
我们记 \(g_i=f_{n,i}\times (n-i)!\) 即满足 \(a>b\) 的组数 \(\geq i\) 的方案数,再令 \(f_i\) 表示恰好满足 \(a>b\) 的组数 \(= i\) 的方案数。
容易发现对于 \(i>j\) \(f_i\) 恰好在 \(g_j\) 中算了 \({i\choose j}\) 次。
那么存在 \(g(k)=\sum_{i=k}^n{i\choose k}f(i)\) 由二项式反演得 \(f(k)=\sum_{i=k}^n(-1)^{i-k}{i\choose k}g(i)\) 直接求解即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 2000+5, P = 1e9+9;
long long n, k, a[N], b[N], l[N], f[N][N], fac[N], ifac[N], g[N];
long long C(int n, int m) {
return 1ll*fac[n]*ifac[m]%P*ifac[n-m]%P;
}
long long power(long long x,long long y){
long long res = 1;
while(y){
if(y & 1)
res = res * x % P;
x = x * x % P;
y >>= 1;
}
return res;
}
int main(){
cin>>n>>k;
if ((n+k)&1) return puts("0"),0;
k = (n+k)/2;
ifac[0] = ifac[1] = fac[0] = fac[1] = 1;
for(int i = 1;i <= n;i++)
fac[i] = fac[i - 1] * i % P;
ifac[n] = power(fac[n],P - 2);
for(int i = n;i >= 1;i--)
ifac[i - 1] = ifac[i] * i % P;
for (int i = 1; i <= n; i++) cin>>a[i];
for (int i = 1; i <= n; i++) cin>>b[i];
sort(a+1, a+n+1);
sort(b+1, b+n+1);
int ll = 0;
for (int i = 1; i <= n; i++) {
while (ll < n && b[ll+1] < a[i]) ++ll;
l[i] = ll;
}
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
f[i][0] = f[i-1][0];
for (int j = 1; j <= i; j++)
f[i][j] = (1ll*f[i-1][j]+1ll*f[i-1][j-1]*max(0ll, l[i]-j+1)%P)%P;
}
for (int i = 0; i <= n; i++) g[i] = 1ll*f[n][i]*fac[n-i]%P;
int ans = 0;
for (int i = k; i <= n; i++)
if ((i-k)&1) (ans -= 1ll*C(i, k)*g[i]%P) %= P;
else (ans += 1ll*C(i, k)*g[i]%P) %= P;
cout<<(ans+P)%P<<endl;
}