组合计数小练
Posted vexoben
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了组合计数小练相关的知识,希望对你有一定的参考价值。
组合数学什么的,最有趣了呢……
[51nod 1251] Fox序列的数量
题意
求满足以下条件的序列数目:
- 序列长度为 $ n $ ,每个元素都属于 $ [1,m] cap Z $ ;
- 这个序列单调不降;
- 这个序列出现次数最多的数是唯一的。
数据范围: $ 1≤n,m≤100000 $ ,答案对 $ 1e9+7 $ 取模
题解
先枚举出现次数最多的数的出现次数 $ k $ ,我们要计算的是 $ x_1+x_2+...+x_{m-1}=n-k, ; x_i≤k-1 $ 的非负整数解数目。
可以枚举有多少个数 $ ≥k $ ,显然需要枚举的个数不超过 $ lfloor frac{n}{k} floor $ ,先给这些数分配 $ k $ 的出现次数,再将剩下的次数随意分配,容斥一下就好了。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define LL long long
#define pii pair<int, int>
using namespace std;
const int N = 200005;
const int mod = 1e9 + 7;
template <typename T> void read(T &x) {
int f = 0;
register char c = getchar();
while (c < '0' || c > '9') f |= (c == '-'), c = getchar();
for (x = 0; c >= '0' && c <= '9'; c = getchar())
x = (x << 3) + (x << 1) + (c ^ '0');
if (f) x = -x;
}
inline int add(register int x, register int y) {
static int z;
return ((z = x + y) >= mod) ? (z - mod) : z;
}
inline int sub(register int x, register int y) {
return (x < y) ? (x - y + mod) : (x - y);
}
inline void upd(int &x, register int y) {
static int z;
x = ((z = x + y) >= mod) ? (z - mod) : z;
}
LL fac[N], fav[N], inv[N];
inline LL C(int x, int y) {
if (x < y || y < 0) return 0;
return fac[x] * fav[y] % mod * fav[x - y] % mod;
}
void init() {
fac[0] = fav[0] = 1;
inv[1] = fac[1] = fav[1] = 1;
for (int i = 2; i < N; ++i) {
inv[i] = -mod / i * inv[mod % i] % mod + mod;
fac[i] = fac[i - 1] * i % mod;
fav[i] = fav[i - 1] * inv[i] % mod;
}
}
void solve() {
static int n, m, f[N];
read(n); read(m);
if (m == 1) return (void) (puts("1"));
if (n == 1) return (void) (printf("%d
", m));
int ans = 0;
for (int i = 1; i <= n; ++i) {
upd(ans, m * C(n - i + m - 2, m - 2) % mod);
for (int j = 1; j <= m - 1 && i * (j + 1) <= n; ++j) {
int del = m * C(m - 1, j) % mod * C(n - i * (j + 1) + m - 2, m - 2) % mod;
if (j & 1) upd(ans, mod - del);
else upd(ans, del);
}
}
cout << ans << endl;
}
int main() {
init();
int T; read(T);
while (T--) solve();
return 0;
}
[51nod 1634] 刚体图
题意
在一个 $ n*m $ 的网格图上,在若干个格子中添加一条对角线(两个方向中的一种),使得不存在一种方法,能够在不改变所有边长的情况下改变图的形状。
数据范围: $ n,m≤10 $
题解
可以发现两个小性质:
1、对角线两个方向上加边对于固定图而言是等价的;
2、以下这张图不是刚体图:
3、不论如何折叠,同一行中经过的竖线和同一列经过的横线一定是互相平行的。
因此,加入一条对角线相当于强制一组横线和一组竖线垂直。
我们的目标是要让所有的横线和竖线垂直。
这就相当于有 $ n $ 个点 $ X $ 部和 $ m $ 个点 $ Y $ 部的联通二分图计数。
这个和无向连通图一样 $ DP $ 就好了。
#include<bits/stdc++.h>
using namespace std;
const int N = 11;
const int mod = 1e9 + 7;
int n, m;
int c[N][N], f[N][N];
inline void upd(int &x, register int y) {
static int z;
x = ((z = x + y) >= mod) ? (z - mod) : z;
}
int Qpow(int x, int p) {
int ans = 1;
while (p) {
if (p & 1) ans = 1LL * ans * x % mod;
x = 1LL * x * x % mod;
p >>= 1;
}
return ans;
}
int dfs(int n, int m) {
if (~f[n][m]) return f[n][m];
if (n == 0) return (m <= 1);
if (m == 0) return (n <= 1);
if (n == 1 && m == 1) return 2;
int ans = Qpow(3, n * m);
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
if (i + j == n + m) continue;
int del = 1LL * dfs(i, j) * Qpow(3, (n - i) * (m - j)) % mod;
del = 1LL * del * c[n - 1][i - 1] % mod;
del = 1LL * del * c[m][j] % mod;
upd(ans, mod - del);
}
}
return f[n][m] = ans;
}
int main() {
c[0][0] = 1;
for (int i = 1; i < N; ++i) {
c[i][0] = 1;
for (int j = 1; j <= i; ++j) {
c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
}
}
memset(f, 0xff, sizeof f);
while (~scanf("%d%d", &n, &m)) printf("%d
", dfs(n, m));
return 0;
}
[51nod 1667] 概率好题
题意
甲乙进行比赛。
他们各有 $ k_1 $ , $ k_2 $ 个集合 $ [L_i,R_i] cap Z $
每次随机从他们拥有的每个集合中都取出一个数
$ S_1=sum $ 甲取出的数, $ S_2 $ 同理
若 $ S_1>S_2 $ 甲胜 若 $ S_1=S_2 $ 平局 否则乙胜
分别求出甲胜、平局、乙胜的概率。
数据范围: $ 1≤k_1, k_2≤8,1≤L≤R≤1e7 $
题解
设甲选出的数是 $ R_i - x_i, ; 0≤x_i≤R_i - L_i $ ,乙选出的数是 $ L_i + y_i, ; 0≤y_i≤R_i - L_i $
那么甲胜出的条件就是 $ sum R_i - x_i > sum L_i + y_i $ ,即:
$ sum x_i + y_i + k = sum R_i - sum L_i - 1 $
$ k $ 是引入的一个变量,使我们可以将不等号化为等号。显然 $ k ≥ 0 $
我们要计算这个方程的解数。注意到未知数不超过 $ 17 $ 个,我们直接枚举那些不符合上界的数容斥即可。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define LL long long
#define pii pair<int, int>
using namespace std;
const int N = 1 << 20;
const int mod = 1e9 + 7;
template <typename T> void read(T &x) {
int f = 0;
register char c = getchar();
while (c < '0' || c > '9') f |= (c == '-'), c = getchar();
for (x = 0; c >= '0' && c <= '9'; c = getchar())
x = (x << 3) + (x << 1) + (c ^ '0');
if (f) x = -x;
}
inline int add(register int x, register int y) {
static int z;
return ((z = x + y) >= mod) ? (z - mod) : z;
}
inline int sub(register int x, register int y) {
return (x < y) ? (x - y + mod) : (x - y);
}
inline void upd(int &x, register int y) {
static int z;
x = ((z = x + y) >= mod) ? (z - mod) : z;
}
int Qpow(int x, int p) {
int ans = 1;
while (p) {
if (p & 1) ans = 1LL * ans * x % mod;
x = 1LL * x * x % mod;
p >>= 1;
}
return ans;
}
int Inv(int x) {
return Qpow(x, mod - 2);
}
LL fac[N], fav[N], inv[N], pw[N];
inline LL C(int x, int y) {
if (x < y || y < 0) return 0;
int ans = fav[y];
for (int i = 0; i < y; ++i) {
ans = 1LL * ans * (x - i) % mod;
}
return ans;
}
void init() {
fac[0] = fav[0] = 1;
inv[1] = fac[1] = fav[1] = 1;
for (int i = 2; i < N; ++i) {
inv[i] = -mod / i * inv[mod % i] % mod + mod;
fac[i] = fac[i - 1] * i % mod;
fav[i] = fav[i - 1] * inv[i] % mod;
}
pw[0] = 1;
for (int i = 1; i < N; ++i) {
pw[i] = pw[i - 1] * 2 % mod;
}
}
int n, m, tot, k1, k2;
int lim[N], a[N];
int l1[N], r1[N], l2[N], r2[N], bitcnt[N];
int calc() {
int ans = 0;
for (int i = 0; i < (1 << m - 1); ++i) {
bitcnt[i] = bitcnt[i >> 1] + (i & 1);
int sum = 0;
for (int j = 1; j < m; ++j) {
if (i >> (j - 1) & 1) {
sum += lim[j] + 1;
}
}
int del = C(n - sum + m - 1, m - 1);
if (bitcnt[i] & 1) upd(ans, mod - del);
else upd(ans, del);
}
return ans;
}
void solve() {
m = n = 0;
tot = 1;
read(k1);
for (int i = 1; i <= k1; ++i) {
read(l1[i]);
read(r1[i]);
lim[++m] = r1[i] - l1[i];
tot = 1LL * tot * (r1[i] - l1[i] + 1) % mod;
n += r1[i];
}
read(k2);
for (int i = 1; i <= k2; ++i) {
read(l2[i]);
read(r2[i]);
lim[++m] = r2[i] - l2[i];
tot = 1LL * tot * (r2[i] - l2[i] + 1) % mod;
n -= l2[i];
}
++m; --n;
LL s1 = calc();
n = 0;
for (int i = 1; i <= k1; ++i) {
n -= l1[i];
}
for (int i = 1; i <= k2; ++i) {
n += r2[i];
}
--n;
LL s2 = calc();
LL s3 = sub(tot, add(s1, s2));
LL inv = Inv(tot);
printf("%lld %lld %lld
", s1 * inv % mod, s3 * inv % mod, s2 * inv % mod);
}
int main() {
init();
int T;
read(T);
while (T--) solve();
return 0;
}
[codeforces 724F] Uniformly Branched Trees
题意
求两两不同构的 $ n $ 个点无标号无根树的个数,满足所有非叶子节点的度为 $ d $
数据范围: $ n≤1000,d≤10 $
题解
一棵树最多有不超过两个重心。如果我们强制以树的重心为根,可以将问题转化为有根树的同构问题。
先处理一个 $ f_{i,j,k} $ 表示一棵大小为 $ i $ 的有根树,树根的度为 $ j $ ,它的子树(不计其本身)最大的大小不超过 $ k $ ,且内部节点满足度数限制的方案数。
转移有两种。其一是不选择大小为 $ k $ 的子树,贡献是 $ f_{i,j,k - 1} $ ,其二是选择 $ l≥1 $ 个大小为 $ k $ 的子树。可以选择的子树有 $ f_{k,d-1,k-1} $ 种,总共要选 $ l $ 棵,贡献就是 $ f_{i-kl,j-l,k-1}*inom{f_{k,d-1,k-1} + l - 1}{f_{k,d-1,k-1}-1} $ ,注意到后面那个组合数等于 $ inom{f_{k,d-1,k-1 + l - 1}}{l} $ ,于是可以在 $ O(d) $ 的时间计算这个组合数。需要特判的是,当 $ k=1 $ 的时候被加入的那个点度数可以是 $ 0 $ (直接接上一个叶子)。
如果 $ n $ 是奇数,那重心只能有唯一一个,答案就是 $ f_{n,d,lceil frac{n}{2} ceil -1} $
如果 $ n $ 是偶数,重心还可能有两个。显然这两个应该是相邻的。删去这两个重心间连着的边,就得到两棵大小为 $ frac{n}{2} $ 的树,贡献就是 $ dbinom{f_{frac{n}{2},d-1,frac{n}{2}-1}+1}{2} $
还有就是特判 $ n=1 $ ,这是唯一允许度数为 $ 0 $ 的点出现的情况。
#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int n, d, mod;
int inv[N], fav[N], fac[N];
int f[N][11][N];
inline void upd(int &x, register int y) {
static int z;
x = ((z = x + y) >= mod) ? (z - mod) : z;
}
int C(int x, int y) {
if (x < y || y < 0) return 0;
int ans = 1;
for (int i = 0; i < y; ++i) {
ans = 1LL * ans * (x - i) % mod;
}
return 1LL * ans * fav[y] % mod;
}
int main() {
cin >> n >> d >> mod;
if (n == 1) return 0 * puts("1");
fac[0] = fav[0] = 1;
inv[1] = fac[1] = fav[1] = 1;
for (int i = 2; i < N; ++i) {
inv[i] = 1LL * -mod / i * inv[mod % i] % mod + mod;
fac[i] = 1LL * fac[i - 1] * i % mod;
fav[i] = 1LL * fav[i - 1] * inv[i] % mod;
}
for (int i = 0; i <= n; ++i) {
f[1][0][i] = 1;
f[1][d - 1][i] = 1;
}
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= d; ++j) {
for (int k = 1; k < i; ++k) {
upd(f[i][j][k], f[i][j][k - 1]);
for (int l = 1; l <= j && k * l + 1 <= i; ++l) {
int del = f[i - k * l][j - l][min(k - 1, i - k * l - 1)];
del = 1LL * del * C(f[k][d - 1][k - 1] + l - 1, l) % mod;
upd(f[i][j][k], del);
}
}
}
}
int ans = f[n][d][(n + 1) / 2 - 1];
if (n % 2 == 0) {
upd(ans, C(f[n / 2][d - 1][n / 2 - 1] + 1, 2));
}
cout << ans << endl;
return 0;
}
[codeforces 960G] Bandit Blues
题意
有 $ n $ 个背包排成一排,他们价值的集合恰好是 $ {1,2,...n} $ 。一个人用这样的方法取背包:一开始手上拿着一个价值为 $ 0 $ 的背包,从左向右或者从右向左走,如果走到位置背包的价值大于他手中背包的价值,他就用它换掉手中的那个背包。已知这个人从左到右走会换 $ A $ 次背包,从右到左走会换 $ B $ 次背包,问可能的排列数。
数据范围: $ a,b,n≤200000 $ ,答案模 $ 998244353 $ 输出。
题解
一个显然的 $ DP $ 是:设 $ f_{i,j,k} $ 表示考虑价值前 $ i $ 大的背包,从左边走要换 $ j $ 次,从右边走要换 $ k $ 次的排列数。
转移就是考虑第 $ i $ 大的那个被加入到哪里。有三种情况。放在最左边:使 $ j $ 加一;放在最右边:使 $ k $ 加一;放在中间的 $ i-2 $ 个位置, $ j $ 和 $ k $ 都不变。
这样做是 $ O(n^3) $ 的,考虑优化这个 $ DP $ 。
第一个优化:注意到放在最左边和放在最右边乘上的系数是相同的,不如直接把它们看做一种转移,最后再将答案乘上一个 $ dbinom{a+b}{a} $ 。即用 $ f_{i,j} $ 表示考虑了前 $ i $ 大的数,转移一和转移二共进行了 $ j $ 次的排列数。这样就得到了一个 $ O(n^2) $ 的 $ DP $ 。
第二个优化:列出现在的转移方程: $ f_{i,j} = (i-2) * f_{i-1,j} + f_{i - 1, j - 1} $ ,如果将 $ f_i $ 看做关于 $ j $ 的多项式,那么这次转移相当于乘上了一个 $ (i-2)+j $ ,于是我们只需要分治 $ FFT $ 求出 $ (0+x)(1+x)...(n-2+x) $ 和初始状态合并一下就得到了答案。这可以做到 $ O(nlogn) $ ,但是我的代码中实现的是 $ O(nlog^2n) $
#include<bits/stdc++.h>
#define LL long long
#define pb push_back
using namespace std;
const int G = 3;
const int N = 1 << 20;
const int mod = 998244353;
int n, a, b;
int rev[N];
int fac[N], fav[N], inv[N];
inline void upd(int &x, register int y) {
static int z;
x = ((z = x + y) >= mod) ? (z - mod) : z;
}
inline int add(register int x, register int y) {
static int z;
return ((z = x + y) >= mod) ? (z - mod) : z;
}
int C(int x, int y) {
return 1LL * fac[x] * fav[y] % mod * fav[x - y] % mod;
}
int Qpow(int x, int p) {
int ans = 1;
while (p) {
if (p & 1) ans = 1LL * x * ans % mod;
x = 1LL * x * x % mod;
p >>= 1;
}
return ans;
}
int Inv(int x) {
return Qpow(x, mod - 2);
}
void calrev(int lim, int l) {
for (int i = 0; i < lim; ++i) {
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << l - 1);
}
}
void NTT(int *a, int lim, int type) {
for (int i = 0; i < lim; ++i) {
if (i < rev[i]) swap(a[i], a[rev[i]]);
}
for (int len = 2; len <= lim; len <<= 1) {
int Wn = Qpow(G, (mod - 1) / len), mid = len >> 1;
for (int i = 0; i < lim; i += len) {
int W = 1;
for (int j = 0; j < mid; ++j) {
int x = a[i + j], y = 1LL * a[i + j + mid] * W % mod;
a[i + j] = add(x, y);
a[i + j + mid] = add(x, mod - y);
W = 1LL * W * Wn % mod;
}
}
}
if (type == -1) {
reverse(a + 1, a + lim);
int tmp = Inv(lim);
for (int i = 0; i < lim; ++i) {
a[i] = 1LL * a[i] * tmp % mod;
}
}
}
vector<int> mul(vector<int> a, vector<int> b) {
int n = a.size() - 1, m = b.size() - 1;
static int A[N], B[N];
for (int i = 0; i <= n; ++i) A[i] = a[i];
for (int i = 0; i <= m; ++i) B[i] = b[i];
int lim = 1, l = 0;
while (lim <= n + m + 2) {
lim <<= 1;
++l;
}
for (int i = n + 1; i < lim; ++i) A[i] = 0;
for (int i = m + 1; i < lim; ++i) B[i] = 0;
calrev(lim, l);
NTT(A, lim, 1);
NTT(B, lim, 1);
for (int i = 0; i < lim; ++i) {
A[i] = 1LL * A[i] * B[i] % mod;
}
NTT(A, lim, -1);
vector<int> ans;
for (int i = 0; i <= n + m; ++i) {
ans.pb(A[i]);
}
return ans;
}
vector<int> solve(int l, int r) {
vector<int> ans;
if (l == r) {
ans.pb(l);
ans.pb(1);
return ans;
}
int mid = (l + r) >> 1;
ans = mul(solve(l, mid), solve(mid + 1, r));
return ans;
}
int main() {
int n, a, b;
cin >> n >> a >> b;
if (n == 1) {
if (a == 1 && b == 1) puts("1");
else puts("0");
return 0;
}
if (a == 0 || b == 0) return 0 * puts("0");
vector<int> res = solve(0, n - 2);
vector<int> tmp;
tmp.pb(0); tmp.pb(0); tmp.pb(1);
res = mul(res, tmp);
fac[0] = fav[0] = 1;
inv[1] = fac[1] = fav[1] = 1;
for (int i = 2; i < N; ++i) {
inv[i] = 1LL * -mod / i * inv[mod % i] % mod + mod;
fac[i] = 1LL * fac[i - 1] * i % mod;
fav[i] = 1LL * fav[i - 1] * inv[i] % mod;
}
if (res.size() < a + b + 1) return 0 * puts("0");
cout << 1LL * C(a + b - 2, a - 1) * res[a + b] % mod << endl;
return 0;
}
[JLOI2016] 成绩比较
题意
G系共有 $ n $ 位同学, $ M $ 门必修课。这 $ N $ 位同学的编号为 $ 0 $ 到 $ N-1 $ 的整数,其中B神的编号为 $ 0 $ 号。这 $ M $ 门必修课编号为 $ 0 $ 到 $ M-1 $ 的整数。一位同学在必修课上可以获得的分数是 $ 1 $ 到 $ U_i $ 中的一个整数。如果在每门课上A获得的成绩均小于等于B获得的成绩,则称A被B碾压。在B神的说法中,G系共有 $ K $ 位同学被他碾压(不包括他自己),而其他 $ N-K-1 $ 位同学则没有被他碾压。D神查到了B神每门必修课的排名。这里的排名是指:如果B神某门课的排名为 $ R $ ,则表示有且仅有 $ R-1 $ 位同学这门课的分数大于B神的分数,有且仅有 $ N-R $ 位同学这门课的分数小于等于B神(不包括他自己)。我们需要求出全系所有同学每门必修课得分的情况数,使其既能满足B神的说法,也能符合D神查到的排名。这里两种情况不同当且仅当有任意一位同学在任意一门课上获得的分数不同。你不需要像D神那么厉害,你只需要计算出情况数模 $ 10^9+7 $ 的余数就可以了。
数据范围: $ n,m≤100,u_i≤10^9,1≤r_i≤n $
题解
设 $ f_{i,j} $ 为考虑前 $ i $ 门课,有 $ j $ 个人被碾压的方案数,于是有:
$ f_{i,j} = sum_{k=j}^{n - 1} f_{i-1,k} dbinom{k}{j} dbinom{n-k - 1}{r_i - 1 - k + j} sum_{l=1}^{u_i} l^{n-r_i}(u_i - l) ^ {r_i - 1} $
不妨设 $ g(x) = sum_{i = 1} ^ {x} i^{n-r} (x - i) ^ {r_i - 1} $ 。手动二项式展开一下,会发现这是关于 $ x $ 的 $ n $ 多项式(注意 $ sum $ 会使次数升高一次),直接上拉格朗日插值就OK
#include<bits/stdc++.h>
#define LL long long
#define pb push_back
using namespace std;
const int G = 3;
const int N = 205;
const int mod = 1e9 + 7;
int n, m, k;
int fac[N], fav[N], inv[N];
int u[N], r[N], f[N][N];
inline void upd(int &x, register int y) {
static int z;
x = ((z = x + y) >= mod) ? (z - mod) : z;
}
inline int add(register int x, register int y) {
static int z;
return ((z = x + y) >= mod) ? (z - mod) : z;
}
int C(int x, int y) {
if (x < y || y < 0) return 0;
return 1LL * fac[x] * fav[y] % mod * fav[x - y] % mod;
}
int Qpow(int x, int p) {
int ans = 1;
while (p) {
if (p & 1) ans = 1LL * x * ans % mod;
x = 1LL * x * x % mod;
p >>= 1;
}
return ans;
}
int Inv(int x) {
return Qpow(x, mod - 2);
}
void prework() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= m; ++i) {
scanf("%d", &u[i]);
}
for (int i = 1; i <= m; ++i) {
scanf("%d", &r[i]);
}
fac[0] = fav[0] = 1;
inv[1] = fac[1] = fav[1] = 1;
for (int i = 2; i < N; ++i) {
inv[i] = 1LL * -mod / i * inv[mod % i] % mod + mod;
fac[i] = 1LL * fac[i - 1] * i % mod;
fav[i] = 1LL * fav[i - 1] * inv[i] % mod;
}
}
int calc(int x, int r) {
static int y[N];
for (int i = 0; i <= n; ++i) {
y[i] = 0;
for (int j = 0; j <= i; ++j) {
upd(y[i], 1LL * Qpow(j, n - r) * Qpow(i - j, r - 1) % mod);
}
}
int ans = 0;
for (int i = 0; i <= n; ++i) {
int tmp = 1;
for (int j = 0; j <= n; ++j) {
if (i == j) continue;
tmp = 1LL * tmp * (x - j) % mod;
tmp = 1LL * tmp * Inv(i - j) % mod;
}
if (tmp < 0) tmp += mod;
upd(ans, 1LL * tmp * y[i] % mod);
}
return ans;
}
void solve() {
f[0][n - 1] = 1;
for (int i = 1; i <= m; ++i) {
int t = calc(u[i], r[i]);
for (int j = 0; j <= n; ++j) {
for (int k = j; k <= n; ++k) {
int del = f[i - 1][k];
del = 1LL * del * C(k, j) % mod;
del = 1LL * del * C(n - k - 1, r[i] - 1 - k + j) % mod;
upd(f[i][j], del);
}
f[i][j] = 1LL * f[i][j] * t % mod;
}
}
printf("%d
", f[m][k]);
}
int main() {
prework();
solve();
return 0;
}
以上是关于组合计数小练的主要内容,如果未能解决你的问题,请参考以下文章