完成一个工程可以有
完成一个工程需要分
从
排列的计算公式如下:
公式可以这样理解:$n$ 个人选
全排列:$n$ 个人全部来排队,队长为
全排列是排列数的一个特殊情况。
从
组合数计算公式
如何理解上述公式?我们考虑
组合数也常用
组合数也被称为「二项式系数」,下文二项式定理将会阐述其中的联系。
特别地,规定当
在进入排列组合进阶篇之前,我们先介绍一个与组合数密切相关的定理——二项式定理。
二项式定理阐明了一个展开式的系数:
证明可以采用数学归纳法,利用
二项式定理也可以很容易扩展为多项式的形式:
设 n 为正整数,$x_i$ 为实数,
其中的
接下来我们介绍一些排列组合的变种。
请大家一定要区分 多重组合数 与 多重集的组合数!两者是完全不同的概念!
多重集是指包含重复元素的广义集合。设
相当于把相同元素的排列数除掉了。具体地,你可以认为你有
可以看出,$\displaystyle \binom{n}{m}$ 等价于
设
考虑这个问题:设
这样就限制了每种元素的取的个数。同样的,我们可以把这个问题转化为带限制的线性方程求解:
于是很自然地想到了容斥原理。容斥的模型如下:
- 全集:$\displaystyle \sum_{i=1}^kx_i=r$ 的非负整数解。
- 属性:$x_i\le n_i$。
于是设满足属性
根据容斥原理,有:
拿全集
其中 A 是充当枚举子集的作用,满足
我们把错位排列问题具体化,考虑这样一个问题:
假设我们考虑到第
- 前面
$n-1$ 个信封全部装错; - 前面
$n-1$ 个信封有一个没有装错其余全部装错。
对于第一种情况,前面
对于第二种情况,前面
其他情况,我们不可能通过一次操作来把它变成一个长度为
于是可得错位排列的递推式为
错位排列数列的前几项为
由此可知部分圆排列的公式:
由于组合数在 OI 中十分重要,因此在此介绍一些组合数的性质。
相当于将选出的集合对全集取补集,故数值不变。(对称性)
由定义导出的递推式。
组合数的递推式(杨辉三角的公式表达)。我们可以利用这个式子,在
这是二项式定理的特殊情况。取
二项式定理的另一种特殊情况,可取
拆组合数的式子,在处理某些数据结构题时会用到。
这是
带权和的一个式子,通过对
与上式类似,可以通过对多项式函数求导证明。
可以通过组合意义证明,在恒等式证明中较常用。
通过定义可以证明。
其中
通过组合分析——考虑
[!NOTE] AcWing 885. 求组合数 I
题意: TODO
[!TIP] 思路
详细代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2010;
const int mod = 1e9 + 7;
int c[N][N];
void init() {
for (int i = 0; i < N; ++ i )
for (int j = 0; j <= i; ++ j )
if (!j) c[i][j] = 1;
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
int main() {
int n;
init();
cin >> n;
while (n -- ) {
int a, b;
cin >> a >> b;
cout << c[a][b] << endl;
}
return 0;
}
[!NOTE] AcWing 886. 求组合数 II
题意: TODO
[!TIP] 思路
$$ C_{a}^{b} = a! / ((a - b)! b!) \
fact[i] = i! mod 1e9+7 \
infacr[i] = (i!)^-1 mod 1e9+7 $$
==>
$$ C_{a}^{b} = fact[a] * infact[a - b] * infact[b] $$ 核心思想 预处理阶乘以及阶乘模逆元
详细代码
#include<bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 100010, mod = 1e9 + 7;
int fact[N], infact[N];
int qmi(int a, int k, int p) {
int res = 1;
while (k) {
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
void init() {
fact[0] = infact[0] = 1;
for (int i = 1; i < N; ++ i ) {
fact[i] = (LL)fact[i - 1] * i % mod;
infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}
}
int main() {
init();
int n;
cin >> n;
while (n -- ) {
int a, b;
cin >> a >> b;
cout << (LL)fact[a] * infact[b] % mod * infact[a - b] % mod << endl;
}
return 0;
}
[!NOTE] AcWing 887. 求组合数 III
题意: TODO
[!TIP] 思路
时间复杂度 接近 10^18 -> 4*10^7
$PlogNlogP$ 基于:
$$ [C_{a}^{b}] = [C_{a\bmod p}^{b\bmod p}] * [C_{a/p}^{b/p}] (\bmod p)$$
详细代码
#include<bits/stdc++.h>
using namespace std;
using LL = long long;
int qmi(int a, int k, int p) {
int res = 1;
while (k) {
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
// Ca b = a! / ((a- b)! * b!) = a*(a-1)*...*(a-b+1) / b!
int C(int a, int b, int p) {
if (b > a) return 0;
int res = 1;
for (int i = 1, j = a; i <= b; ++ i , -- j ) {
// res = res * j / i = res * j * i^-1
res = (LL)res * j % p;
res = (LL)res * qmi(i, p - 2, p) % p;
}
return res;
}
int lucas(LL a, LL b, int p) {
if (a < p && b < p) return C(a, b, p);
return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}
int main() {
int n;
cin >> n;
while (n -- ) {
LL a, b;
int p;
cin >> a >> b >> p;
cout << lucas(a, b, p) << endl;
}
return 0;
}
[!NOTE] AcWing 888. 求组合数 IV
题意: TODO
[!TIP] 思路
$$ C_{a}^{b} = [a*(a-1)...(a-b+1)] / [b*(b-1)*...*1] $$
大数运算很难
故第一步:将
$C_{a}^{b}$ 分解质因数:$$C_{a}^{b} = p1^{a1} ... pk^{ak}$$此时
$C_{a}^{b}$ 表示为$C_{a}^{b} = a! / [b! * (a-b)!]$ 更好先求分子的p 再减去分母的p即可
【求每个质数的次数】
$$a! = a/p + a/p^2 + ... + a/p^k$$
详细代码
#include<bits/stdc++.h>
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) {
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);
for (int i = 0; i < cnt; ++ i ) {
int p = primes[i];
sum[i] = get(a, p) - get(a - b, p) - get(b, p);
}
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 ) cout << res[i];
cout << endl;
return 0;
}
[!NOTE] AcWing 1308. 方程的解
题意:
求非负整数解
[!TIP] 思路
隔板法 + 高精度加
详细代码
#include <bits/stdc++.h>
using namespace std;
const static int N = 150; // 组合数大数的位数
int k, x;
int f[1000][100][N]; // 大数记录组合数
int qmi(int a, int b, int p) {
a %= p;
int ret = 1;
while (b) {
if (b & 1)
ret = ret * a % p;
a = a * a % p;
b >>= 1;
}
return ret;
}
void add(int c[], int a[], int b[]) {
for (int i = 0, t = 0; i < N; ++ i ) {
t += a[i] + b[i];
c[i] = t % 10;
t /= 10;
}
}
int main() {
cin >> k >> x;
int n = qmi(x, x, 1000);
// C(n - 1, k - 1);
for (int i = 0; i < n; ++ i )
for (int j = 0; j <= i && j < k; ++ j )
if (!j)
f[i][j][0] = 1;
else
add(f[i][j], f[i - 1][j], f[i - 1][j - 1]);
int *g = f[n - 1][k - 1];
int i = N - 1;
while (!g[i])
i -- ;
while (i >= 0)
cout << g[i -- ];
return 0;
}
[!NOTE] AcWing 1312. 序列统计 [TAG]
题意:
给定三个整数
$N,L,R$ ,统计长度在$1$ 到$N$ 之间,元素大小都在$L$ 到$R$ 之间的单调不降序列的数量。输出答案对
$10^6+3$ 取模的结果。
[!TIP] 思路
约定:题目中的
$N,L,R$ 用$n,l,r$ 表示。数据之间只存在相对关系,则将
$[l,r]$ 区间映射到$[0,r-l]$ ,设序列长度为$k$ ,题意即求:
$$ 满足 \ 0 \le a_1 \le a_2 \le \cdots \le a_k \le r-l,\ 其中 \ a_i \in[0,r-l] \ 的序列个数 $$ 令
$x_1 = a_1, x_2 = a_2 - a_1, \cdots, x_k = a_k - a_{k-1}$ ,则有:
$$ 0 \le x_1 + x_2 + \cdots + x_k \le r-l,\ 其中 \ x_i \ge 0 $$ 问题即:用不超过
$r-l$ 个小球放入$k$ 个盒子,盒子允许为空的方案数。等价为:先给每一个盒子放入一个小球(需要令小球的总数
$+k$ ),即用不超过$r-l+k$ 个小球放入$k$ 个盒子,且盒子不空的方案数。显然可以「隔板法」。但本题的条件是不等式,对于等式直接用
$k-1$ 个隔板将所有小球分为$k$ 部分即可;对于不等式要用$k$ 个隔板,将所有小球分为$k+1$ 部分,其中最后一部分被舍弃(即不选用),本题最后一部分的个数可以为零。则最终答案为
$\sum_{k=1}^{n}C_{r-l+k}^{k}$ 数据范围
$10^9$ ,显然不能枚举长度$k$ 来累加答案,由两个组合数公式进行推导:
$$ C_n^m=C_n^{n-m}, \ C_n^m = C_{n-1}^{m} + C_{n-1}^{m-1} $$ 令
$r-l=m$ ,则:
$$ \begin{aligned} 原式= &\sum_{k=1}^{n}C_{m+k}^{k} \\ = &C_{m+1}^{1} + C_{m+2}^{2} + \cdots + C_{m+n}^{n} \\ = &C_{m+1}^{m} + C_{m+2}^{m} + \cdots + C_{m+n}^{m} \\ = &({\color{red}{C_{m+1}^{m+1}}} + C_{m+1}^{m}) + \cdots + C_{m+n}^{m} - {\color{red}{C_{m+1}^{m+1}}} \\ = &(C_{m+2}^{m+1} + C_{m+2}^{m}) + \cdots + C_{m+n}^{m} - 1 \\ = &\cdots \\ = &C_{m+n+1}^{m+1} - 1 \end{aligned} $$ 然后用
$\rm Lucas$ 定理求解组合数即可。
详细代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const static int MOD = 1000003;
int qmi(int a, int k) {
int ret = 1;
while (k) {
if (k & 1)
ret = (LL)ret * a % MOD;
a = (LL)a * a % MOD;
k >>= 1;
}
return ret;
}
int C(int a, int b) {
if (a < b)
return 0;
int down = 1, up = 1;
for (int i = a, j = 1; j <= b; -- i , ++ j ) {
up = (LL)up * i % MOD;
down = (LL)down * j % MOD;
}
return (LL)up * qmi(down, MOD - 2) % MOD;
}
int lucas(int a, int b) {
if (a < MOD && b < MOD)
return C(a, b);
return (LL)lucas(a / MOD, b / MOD) * C(a % MOD, b % MOD) % MOD;
}
int main() {
int T;
cin >> T;
while (T -- ) {
int n, l, r;
cin >> n >> l >> r;
cout << (lucas(r - l + n + 1, r - l + 1) + MOD - 1) % MOD << endl;
}
return 0;
}
[!NOTE] LeetCode 3154. 到达第 K 级台阶的方案数
题意: TODO
[!TIP] 思路
结合题意 [约束],结合数据范围求解
暴搜 需要记忆化
组合数学 trick
实际上,本题相当于在连续的第二种操作中间插入第一种操作。
假设两种操作分别做了
$i$ 次 /$j$ 次,数学分析:
$第二种操作 上升数量\ up1=1+2+...+2^{j-1}=2^j-1$
$第一种操作上升数量\ up2=i\ (0<=i<=j+1)$
$总的上升数量={up1 + up2}$ 则
$k-1\ (-1是因为起点为1)$ 必然在此范围内,由此可推导$j$ 的合法值,随后排列组合计算即可...
详细代码
class Solution {
public:
// 考虑到第一种操作无法无限使用 假设每次跳都执行一次
// 则根据k的数据范围结合第二种操作的限制 最多跳 32*2=64 次
int k, res;
unordered_map<int, unordered_map<int, unordered_map<int, int>>> mem;
int dfs(int u, int p, int j) {
if (u > k + 2)
return 0;
if (mem[u][p].count(j))
return mem[u][p][j];
int ret = 0;
if (u == k)
ret ++ ;
if (p != 0)
ret += dfs(u - 1, 0, j);
ret += dfs(u + (1 << j), 1, j + 1);
return mem[u][p][j] = ret;
}
int waysToReachStair(int k) {
this->k = k, this->res = 0;
return dfs(1, -1, 0);
}
};
class Solution {
public:
const static int N = 32;
int c[N][N];
void init() {
for (int i = 0; i < N; ++ i )
for (int j = 0; j <= i; ++ j )
if (!j)
c[i][j] = 1;
else
c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
}
int comb(int a, int b) {
return c[a][b];
}
int waysToReachStair(int k) {
init();
k -- ; // ATTENTION 起点偏移一下
int res = 0;
for (int j = 0; j < 31; ++ j ) {
int tot = (1 << j) - 1;
if (tot >= k && tot - (j + 1) <= k) {
int i = tot - k;
// 在 j+1 个缝隙里找到 i 个位置插入
res += comb(j + 1, i);
}
}
return res;
}
};
[!NOTE] AcWing 889. 满足条件的01序列
题意: TODO
[!TIP] 思路
卡特兰数
转化为:平面图路径 中线绿 非法线红
在互补的情况下
$C_{12}^{5} = C_{12}^{7}$ 在线下方的个数等于
$C_{2n}^{n} = C_{2n}^{n} - C_{2n}^{n-1} = C_{2n}^{n} / (n+1)$ 成为卡特兰数
详细代码
#include<bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 100010, mod = 1e9 + 7;
int qmi(int a, int k, int p) {
int res = 1;
while (k) {
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int main() {
int n;
cin >> n;
int a = n * 2, b = n;
int res = 1;
// C2n n
for (int i = a; i > a - b; -- i ) res = (LL)res * i % mod;
for (int i = 1; i <= b; ++ i ) res = (LL)res * qmi(i, mod - 2, mod) % mod;
// 除 n + 1
res = (LL)res * qmi(n + 1, mod - 2, mod) % mod;
cout << res << endl;
return 0;
}
[!NOTE] Luogu NOIP2003 普及组 栈
题意: TODO
[!TIP] 思路
经典组合数
详细代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 40;
LL c[N][N];
int main() {
for (int i = 0; i < N; ++ i )
for (int j = 0; j <= i; ++ j )
if (!j)
c[i][j] = 1;
else
c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
int n;
cin >> n;
cout << c[n * 2][n] / (n + 1) << endl;
return 0;
}
[!NOTE] LeetCode 2400. 恰好移动 k 步到达某一位置的方法数目
题意: TODO
[!TIP] 思路
DP 或组合数
一种新的线形求逆元的方式
详细代码
class Solution {
public:
using LL = long long;
const static int N = 1010, DIFF = 501, MOD = 1e9 + 7;
// [-500, 1500] instead of [-1000, 2000]
LL f[N * 2], g[N * 2];
int numberOfWays(int startPos, int endPos, int k) {
memset(f, 0, sizeof f);
f[startPos + DIFF] = 1;
for (int _ = 0; _ < k; ++ _ ) {
memcpy(g, f, sizeof g);
for (int j = -500; j <= 1500; ++ j )
f[j + DIFF] = (g[j - 1 + DIFF] + g[j + DIFF + 1]) % MOD;
}
return f[endPos + DIFF];
}
};
class Solution {
public:
using LL = long long;
const static int N = 1010, MOD = 1e9 + 7;
int f[N][N];
int numberOfWays(int startPos, int endPos, int k) {
int d = abs(startPos - endPos);
if ((d + k) % 2 || d > k)
return 0;
for (int i = 0; i <= k; ++ i )
for (int j = 0; j <= i; ++ j )
if (!j)
f[i][j] = 1;
else
f[i][j] = (f[i - 1][j] + f[i - 1][j - 1]) % MOD;
// 假定向正方向走 a 步 反方向 k-a 步
// 则 a - (k - a) = d => a = (d + k) / 2
// C[k, (d + k) / 2]
return f[k][(d + k) / 2];
}
};
class Solution {
public:
using LL = long long;
const static int N = 1010, MOD = 1e9 + 7;
LL f[N], g[N], v[N];
int C(int n, int m) {
if (m == 0)
return 1;
return f[n] * v[m] % MOD * v[n - m] % MOD;
}
int numberOfWays(int startPos, int endPos, int k) {
int d = abs(startPos - endPos);
if ((d + k) % 2 || d > k)
return 0;
// 假定向正方向走 a 步 反方向 k-a 步
// 则 a - (k - a) = d => a = (d + k) / 2
// C[k, (d + k) / 2]
f[0] = g[0] = v[0] = 1;
f[1] = g[1] = v[1] = 1;
for (int i = 2; i < N; ++ i ) {
f[i] = f[i - 1] * i % MOD;
g[i] = MOD - (LL)MOD / i * g[MOD % i] % MOD;
v[i] = v[i - 1] * g[i] % MOD;
}
return C(k, (d + k) / 2);
}
};
[!NOTE] LeetCode 3251. 单调数组对的数目 II
题意: TODO
[!TIP] 思路
将数组拆解,每个数分成两个数字分散在不同数组,且一个递增一个递减,求所有可能方案
标准的前缀和优化
TODO: revisit 整理组合数学做法
详细代码
class Solution {
public:
using LL = long long;
const static int N = 2010, M = 1010, MOD = 1e9 + 7;
int f[N][M];
// 假设对于第i个位置 分配给第一个数组的数值大小为j 则第二个数组为 s_i - j
// f[i][j] = sum{f[i - 1][k]} j>=k && (s_i-1 - k >= s_i - j)
// => k <= j && k <= s_i-1 - s_i + j
// 伴随着j的枚举 和是单调递增的...
void mod_add(int & a, int b) {
a = ((LL)a + b) % MOD;
}
int countOfPairs(vector<int>& nums) {
int n = nums.size();
memset(f, 0, sizeof f);
for (int j = 0; j <= nums[0]; ++ j )
f[1][j] = 1;
for (int i = 2; i <= n; ++ i ) {
int s = nums[i - 1], s_last = nums[i - 2];
// 枚举当前选择的 j
int sum = 0, gap = max(0, s - s_last);
for (int j = 0; j <= s; ++ j ) {
// k <= j && k <= s_last - s + j
// 那么对于一个具体的 j 来说,可以取的范围是 [0, min(j, j+s_last-s)]
// 差值的 gap 是 s-s_last
if (j - gap >= 0)
mod_add(sum, f[i - 1][j - gap]);
f[i][j] = sum;
}
}
int res = 0;
for (int j = 0; j < M; ++ j )
mod_add(res, f[n][j]);
return res;
}
};
class Solution {
public:
using LL = long long;
const static int N = 3010, MOD = 1e9 + 7;
LL f[N], g[N];
LL qpow(LL x, int y) {
LL ret = 1;
while (y) {
if (y & 1)
ret = ret * x % MOD;
x = x * x % MOD;
y >>= 1;
}
return ret;
}
void init() {
f[0] = g[0] = 1;
for (int i = 1; i < N; ++ i ) {
f[i] = f[i - 1] * i % MOD;
g[i] = g[i - 1] * qpow(i, MOD - 2) % MOD;
}
}
LL C(int n, int m) {
return f[n] * g[m] % MOD * g[n - m] % MOD;
}
int countOfPairs(vector<int>& nums) {
init();
int n = nums.size(), m = nums.back(); // ATTENTION m
for (int i = 1; i < n; ++ i ) {
m -= max(0, nums[i] - nums[i - 1]); // 减掉必须往上走的强制约束
if (m < 0)
return 0;
}
return C(n + m, n); // 总共可以自由走 n+m 步
}
};
[!NOTE] Luogu [NOIP2016 提高组] 组合数问题
题意: TODO
[!TIP] 思路
为了处理多组数据询问 前缀和预处理
详细代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2010;
int c[N][N];
int s[N][N];
int main() {
int T, k;
scanf("%d%d", &T, &k);
for (int i = 0; i < N; i++)
for (int j = 0; j <= i; j++) {
if (!j)
c[i][j] = 1 % k;
else
c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % k;
if (!c[i][j])
s[i][j] = 1;
}
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) {
if (i)
s[i][j] += s[i - 1][j];
if (j)
s[i][j] += s[i][j - 1];
if (i && j)
s[i][j] -= s[i - 1][j - 1];
}
while (T--) {
int n, m;
scanf("%d%d", &n, &m);
printf("%d\n", s[n][m]);
}
return 0;
}
[!NOTE] LeetCode 1569. 将子数组重新排序得到同一个二叉查找树的方案数 [TAG]
题意: TODO
[!TIP] 思路
TODO 重复做
详细代码
class Solution {
public:
using LL = long long;
const static int N = 1e3 + 10, MOD = 1e9 + 7;
int C[N][N];
void init() {
for (int i = 0; i < N; ++ i )
for (int j = 0; j <= i; ++ j )
if (!j)
C[i][j] = 1;
else
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % MOD;
}
int f(vector<int> nums) {
if (nums.empty())
return 1;
int n = nums.size(), k = nums[0];
vector<int> l, r;
// 排列 不会有多个等于 k 的数
for (auto x : nums)
if (x < k)
l.push_back(x);
else if (x > k)
// ATTENTION: 必须进行下标转移的映射
r.push_back(x - k);
return (LL)C[n - 1][k - 1] * f(l) % MOD * f(r) % MOD;
}
int numOfWays(vector<int>& nums) {
init();
// 减去最初的一种方案
return (f(nums) - 1 + MOD) % MOD;
}
};
class Solution {
int mod = 1000000007;
vector<int> op;
long long P[1005], Q[1005];
public:
long long mul(long long x, long long y) {
if (y == 0) return 1;
if (y == 1) return x;
long long ret = mul(x, y / 2);
if (y % 2 == 0)
return ret * ret % mod;
else
return ret * ret % mod * x % mod;
}
long long C(long long x, long long y) {
return P[x] * Q[y] % mod * Q[x - y] % mod;
}
int dfs(int l, int r) {
if (l > r) return 1;
int mid;
for (int i = 0; i < op.size(); i++) {
if (l <= op[i] && op[i] <= r) {
mid = op[i];
break;
}
}
long long cnt1 = dfs(l, mid - 1);
long long cnt2 = dfs(mid + 1, r);
long long ret = C(r - l, (mid - l)) * cnt1 % mod * cnt2 % mod;
return ret;
}
int numOfWays(vector<int>& nums) {
op = nums;
P[0] = Q[0] = 1;
for (int i = 1; i <= 1000; i++) P[i] = P[i - 1] * i % mod;
for (int i = 1; i <= 1000; i++) Q[i] = mul(P[i], mod - 2);
long long ans = dfs(1, nums.size());
ans = (ans - 1 + mod) % mod;
return ans;
}
};
[!NOTE] LeetCode 1573. 分割字符串的方案数
题意: TODO
[!TIP] 思路
分割 组合数
详细代码
class Solution {
public:
const int MOD = 1e9 + 7;
int numWays(string s) {
long long count1 = 0, n = s.size();
unordered_map<long long, long long> mp; //存储1的位置
for (int i = 0; i < n; i++)
if (s[i] == '1') {
count1++; //统计1 的个数
mp[count1] = i;
}
if (count1 == 0) return ((n - 1) * (n - 2) / 2) % MOD;
if (count1 % 3 != 0) return 0;
long long t = count1 / 3; //确立3等分1的个数
return ((mp[t + 1] - mp[t]) * (mp[2 * t + 1] - mp[2 * t])) % MOD;
}
};
[!NOTE] LeetCode 1735. 生成乘积数组的方案数
题意: TODO
[!TIP] 思路
对每个质因数的幂次求组合数即可
详细代码
using LL = long long;
const int N = 10010;
const int mod = 1e9 + 7;
class Solution {
public:
int f[N], g[N];
int qmi(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = (LL)res * a % mod;
a = (LL)a * a % mod;
b >>= 1;
}
return res;
}
// a! / [(a - b)! * (b)!]
int C(int a, int b) {
return (LL)f[a] * g[b] % mod * g[a - b] % mod;
}
vector<int> waysToFillArray(vector<vector<int>>& queries) {
f[0] = g[0] = 1;
for (int i = 1; i < N; ++ i ) {
f[i] = (LL)f[i - 1] * i % mod;
g[i] = qmi(f[i], mod - 2);
}
vector<int> res;
for (auto & q : queries) {
int n = q[0], k = q[1];
int ret = 1;
for (int i = 2; i <= k / i; ++ i )
if (k % i == 0) {
int s = 0;
while (k % i == 0)
k /= i, ++ s ;
ret = (LL)ret * C(n + s - 1, n - 1) % mod;
}
if (k > 1) ret = (LL)ret * C(n, n - 1) % mod;
res.push_back(ret);
}
return res;
}
};
[!NOTE] LeetCode 建信04. 电学实验课 [TAG]
题意: TODO
[!TIP] 思路
非常好的组合计数题
重复做 TODO
由于每一列只允许防止一个插孔,所以将所有目标插孔按照列从小到大排序后,考虑每两个目标插孔之间的方案数,然后求乘积就是答案。
设 v 是一个长度为 row 的数组,v(j) 表示到达第 j 行的方案数。设二维转移矩阵 T,其中 T(i,j)=1 表示可以从前一列的第 i 行到达当前列的第 j 行。 所以每移动一列,v 就可以更新为 T⋅v。移动 k 列时,vk=T^k⋅v0。
可以提前预处理出来所有二进制位的矩阵乘积结果,即 T1,T2,T4 直到 T29,然后根据两个目标插孔之间的距离进行组合,计算出方案数。
详细代码
class Solution {
public:
using LL = long long;
const static int N = 33, MOD = 1e9 + 7;
int n, A[N][N][N];
int t[N], g[N];
void mul(int c[N][N], int a[N][N], int b[N][N]){
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++ ){
c[i][j] = 0;
for (int k = 0; k < n; k ++ )
c[i][j] = (c[i][j] + 1ll * a[i][k] * b[k][j]) % MOD;
}
}
void mul2(int c[N], int a[N], int b[N][N]){
for (int i = 0; i < n; i ++ ) c[i] = 0;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j++)
c[j] = (c[j] + 1ll * a[i] * b[i][j]) % MOD;
}
int electricityExperiment(int row, int col, vector<vector<int>>& p) {
// Step 1: sorting
this->n = row;
sort(p.begin(), p.end(), [](vector<int> & a, vector<int> & b){
return a[1] < b[1];
});
// Step 2: return 0 if impossible
int len = p.size();
for (int i = 1; i < len; i ++ )
if (p[i][1] == p[i-1][1])
return 0;
// Step 3.1: init A[0]
for (int i = 0; i < n; i ++ )
for (int j = -1; j <= 1; j ++ )
if (i + j >= 0 && i + j < n)
A[0][i + j][i] = 1;
// Step 3.2: calc the A
for (int k = 1; k <= 30; k ++ )
mul(A[k], A[k - 1], A[k - 1]);
// Step 4: calc the result
int res = 1;
for (int i = 1; i < len; i ++ ){
memset(t, 0, sizeof t);
t[p[i - 1][0]] = 1;
int d = p[i][1] - p[i - 1][1];
for (int k = 0; (1 << k) <= d; k ++ )
if ((d >> k) & 1) {
mul2(g, t, A[k]);
memcpy(t, g, sizeof(g));
}
res = 1ll * res * t[p[i][0]] % MOD;
}
return res;
}
};
[!NOTE] Codeforces C. Beautiful Numbers
题意: TODO
[!TIP] 思路
非常好的组合数题目 重复做
详细代码
// Problem: C. Beautiful Numbers
// Contest: Codeforces - Codeforces Round #181 (Div. 2)
// URL: https://codeforces.com/problemset/problem/300/C
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
// 非常好的组合数题目
// 暴力会遍历跑lucas会超时 考虑记忆化
// https://codeforces.com/contest/300/submission/110875084
// 记忆化仍然超时
// https://codeforces.com/contest/300/submission/110875327
// 参考luogu的做法,这里会频繁用到组合数所以直接递推计算 而不用lucas
// https://www.luogu.com.cn/problem/solution/CF300C
// 预处理阶乘和逆
using LL = long long;
const int N = 1000010, MOD = 1e9 + 7;
int a, b, n;
int fact[N], infact[N];
bool check(int x) {
while (x) {
int t = x % 10;
if (t != a && t != b)
return false;
x /= 10;
}
return true;
}
int qmi(int a, int k, int p) {
int res = 1;
while (k) {
if (k & 1)
res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
void init() {
fact[0] = infact[0] = 1;
for (int i = 1; i < N; ++i) {
fact[i] = (LL)fact[i - 1] * i % MOD;
infact[i] = (LL)infact[i - 1] * qmi(i, MOD - 2, MOD) % MOD;
}
}
int main() {
init();
cin >> a >> b >> n;
LL res = 0;
// 枚举 a 的个数
for (int i = 0; i <= n; ++i) {
int s = a * i + b * (n - i);
if (check(s))
res = (res + (LL)fact[n] * infact[i] % MOD * infact[n - i]) % MOD;
}
cout << res << endl;
return 0;
}
[!NOTE] LeetCode 2338. 统计理想数组的数目 [TAG]
题意: TODO
[!TIP] 思路
先考虑数列无重复数字的情况,显然可以 DP 递推来求每个长度下不同结尾数字的方案数
进一步考虑,在无重复数字情况下,每一种方案按隔板法求有重复数字下的方案数
求和即可
详细代码
class Solution {
public:
using LL = long long;
const static int N = 1e4 + 10, M = 15, MOD = 1e9 + 7; // 对于 1e4 在一个数组内最多存在 log1e4 + 1 = 14 个不同的数
int fact[N], infact[N];
int qmi(int a, int k, int p) {
int ret = 1;
while (k) {
if (k & 1)
ret = ((LL)ret * a) % MOD;
a = (LL)a * a % MOD;
k >>= 1;
}
return ret;
}
void init() {
fact[0] = infact[0] = 1;
for (int i = 1; i < N; ++ i ) {
fact[i] = (LL)fact[i - 1] * i % MOD;
infact[i] = (LL)infact[i - 1] * qmi(i, MOD - 2, MOD) % MOD;
}
}
int C(int x, int y) {
return (LL)fact[x] * infact[x - y] % MOD * infact[y] % MOD;
}
int f[M][N]; // 长度为 i 末尾数值为 j 的所有不重复数方案数
int idealArrays(int n, int maxValue) {
init();
memset(f, 0, sizeof f);
for (int i = 1; i <= maxValue; ++ i )
f[1][i] = 1;
for (int i = 1; i < M - 1; ++ i )
for (int j = 1; j <= maxValue; ++ j )
for (int k = 2; k * j <= maxValue; ++ k ) // log
f[i + 1][k * j] = (f[i + 1][k * j] + f[i][j]) % MOD;
int res = 0;
for (int i = 1; i <= n && i < M; ++ i )
for (int j = 1; j <= maxValue; ++ j )
// C_{n - 1}^{i - 1} 隔板法
res = (res + (LL)f[i][j] * C(n - 1, i - 1)) % MOD;
return res;
}
};
[!NOTE] LeetCode 2842. 统计一个字符串的 k 子序列美丽值最大的数目
题意: TODO
[!TIP] 思路
经典组合数 加快速度
详细代码
using LL = long long;
using PII = pair<int, int>;
const static int N = 2e5 + 10, M = 30, MOD = 1e9 + 7;
LL qpow(int x, int y) {
LL ret = 1;
while (y) {
if (y & 1)
ret = ret * x % MOD;
x = (LL)x * x % MOD;
y >>= 1;
}
return ret;
}
bool flag = false;
int fact[N], infact[N];
void init() {
if (flag)
return;
flag = true;
fact[0] = infact[0] = 1;
for (int i = 1; i < N; ++ i ) {
fact[i] = (LL)fact[i - 1] * i % MOD;
infact[i] = (LL)infact[i - 1] * qpow(i, MOD - 2) % MOD;
}
}
LL C(int x, int y) {
return (LL)fact[x] * infact[y] % MOD * infact[x - y] % MOD;
}
class Solution {
public:
int c[M];
int n;
int countKSubsequencesWithMaxBeauty(string s, int k) {
init();
memset(c, 0, sizeof c);
this->n = s.size();
for (auto ch : s)
c[ch - 'a'] ++ ;
{
int cnt = 0;
for (int i = 0; i < M; ++ i )
if (c[i])
cnt ++ ;
// cout << " s = " << s << " cnt = " << cnt << endl;
if (cnt < k)
return 0;
}
unordered_map<int, int> h;
for (int i = 0; i < 26; ++ i )
if (c[i])
h[c[i]] ++ ; // 出现了 x 次的有 y 个
vector<PII> xs;
for (auto [x, y] : h)
xs.push_back({x, y}); // 按 x 次数降序排序
sort(xs.begin(), xs.end());
reverse(xs.begin(), xs.end());
LL res = 1;
for (auto [x, y] : xs) {
if (k >= y) {
// 这么多个(y)必然全选, 每一个都是 x 种可能
res = res * qpow(x, y) % MOD;
k -= y;
} else {
// 这么多个(y)只挑 k 个选, 每一个都是 x 种可能
res = res * qpow(x, k) % MOD * C(y, k) % MOD;
k = 0;
}
if (k == 0)
break;
}
return res;
}
};
[!NOTE] LeetCode 2954. 统计感冒序列的数目
题意: TODO
[!TIP] 思路
组合计数应用题 注意去重的推导和实现思路
详细代码
using LL = long long;
const static int N = 1e5 + 10, MOD = 1e9 + 7;
LL qpow(LL a, LL b) {
LL ret = 1;
while (b) {
if (b & 1)
ret = ret * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return ret;
}
LL f[N], g[N]; // 阶乘 & 逆元
bool flag = false;
void init() {
if (flag)
return;
flag = true;
f[0] = g[0] = 1;
for (int i = 1; i < N; ++ i ) {
f[i] = f[i - 1] * i % MOD;
g[i] = g[i - 1] * qpow(i, MOD - 2) % MOD;
}
}
class Solution {
public:
// 直观看没有太好的思路 考虑尝试数学解法 [组合数学]
//
// ATTENTION 假设连续 0 的数量为 k
// - 如果只有一边都可以发生传染,那么只有一种传染序列
// - 如果两边都可以,那么每一次都可以选择从左或从右传染,除了最后一次,因此有 2^{k−1} 种序列
//
// 则 分区间统计累乘即可 ==> 实际上会有重复情况,因为在不同区间里也只能一次选其中的一个
// ==> 重复元素排列问题 【计算细节】
LL comb(int a, int b) {
return f[a] * g[a - b] % MOD * g[b] % MOD;
}
int numberOfSequence(int n, vector<int>& sick) {
init();
sort(sick.begin(), sick.end());
sick.push_back(n); // 边界哨兵
LL res = 1;
int tot = 0, last = -1;
for (auto x : sick) {
int k = x - last - 1;
if (k > 0) {
LL t = 0;
if (last == -1 || x == n) {
// 特殊情况 只能从某一个方向转移过来
t = 1;
} else {
// 可以从 左/右 转移过来 => 2^(k-1)
t = qpow(2, k - 1);
}
// 【ATTENTION: 计算逻辑】
// 在总长为 tot+len 的序列中,选择 len 个位置填充 (内部的填充方法有 t 种)
res = res * comb(tot + k, k) % MOD * t % MOD;
}
last = x, tot += k;
}
return res;
}
};
[!NOTE] LeetCode 2963. 统计好分割方案的数目
题意: TODO
[!TIP] 思路
区间合并 + 组合数 (后者也可转化成
$2^{m-1}$ 思考)
详细代码
using LL = long long;
const static int N = 1e5 + 10, MOD = 1e9 + 7;
int qmi(int a, int k, int p) {
int ret = 1;
while (k) {
if (k & 1)
ret = (LL)ret * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return ret;
}
int fact[N], infact[N];
bool inited = false;
void init() {
if (inited)
return;
inited = true;
fact[0] = infact[0] = 1;
for (int i = 1; i < N; ++ i ) {
fact[i] = (LL)fact[i - 1] * i % MOD;
infact[i] = (LL)infact[i - 1] * qmi(i, MOD - 2, MOD) % MOD;
}
}
int comb(int a, int b) {
return (LL)fact[a] * infact[b] % MOD * infact[a - b] % MOD;
}
class Solution {
public:
using PII = pair<int, int>;
void merge(vector<PII> & segs) {
vector<PII> res;
sort(segs.begin(), segs.end());
int st = -2e9, ed = -2e9;
for (auto seg : segs)
if (ed < seg.first) {
if (st != -2e9)
res.push_back({st, ed});
st = seg.first, ed = seg.second;
} else
ed = max(ed, seg.second);
if (st != -2e9)
res.push_back({st, ed});
segs = res;
}
int numberOfGoodPartitions(vector<int>& nums) {
init();
int n = nums.size();
unordered_set<int> S;
unordered_map<int, int> l, r;
{
// 先找到 最左侧/最右侧 出现的位置
for (int i = 0; i < n; ++ i ) {
int x = nums[i];
if (S.count(x)) {
l[x] = min(l[x], i), r[x] = max(r[x], i);
} else {
l[x] = r[x] = i;
S.insert(x);
}
}
}
vector<PII> xs;
{
// 根据出现过的元素划分基本区间
for (auto [k, v] : l)
xs.push_back({v, r[k]});
// 区间合并
merge(xs);
}
// 留下的都是可以任意合并的区间
// 最坏情况下 m=1e5
int m = xs.size(), res = 0;
for (int i = 0; i < m; ++ i )
// C{m-1}{i}
res = (res + comb(m - 1, i)) % MOD;
return res;
}
};
[!NOTE] AcWing 1309. 车的放置 [TAG]
题意:
L 型方格,求放
$k$ 个车
的方案数
[!TIP] 思路
先考虑简单情况,对于
$n * m$ 的矩阵,放置$k$ 个车
的方案数为:
$C_n^k * A_m^k$ 先选$k$ 行,再有次序的选择$k$ 列 (组合数)对于本题,按照上下两部分进行切分,切分后各自结合符合 乘法原理
假定上半部分摆放
$i$ 个 ==>$C_b^i * A_a^i$ 下半部分相应为
$k-i$ 个 ==>$C_d^{k-i} * A_{a+c-i}^{k-i}$ 枚举
$i$ 相乘累加即可
详细代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const static int N = 2010, MOD = 100003;
int fact[N], infact[N];
int qmi(int a, int k) {
int ret = 1;
while (k) {
if (k & 1)
ret = (LL)ret * a % MOD;
a = (LL)a * a % MOD;
k >>= 1;
}
return ret;
}
int C(int a, int b) {
if (a < b)
return 0;
return (LL)fact[a] * infact[a - b] % MOD * infact[b] % MOD;
}
int A(int a, int b) {
if (a < b)
return 0;
return (LL)fact[a] * infact[a - b] % MOD;
}
int main() {
fact[0] = infact[0] = 1;
for (int i = 1; i < N; ++ i ) {
fact[i] = (LL)fact[i - 1] * i % MOD;
infact[i] = (LL)infact[i - 1] * qmi(i, MOD - 2) % MOD;
}
int a, b, c, d, k;
cin >> a >> b >> c >> d >> k;
int res = 0;
for (int i = 0; i <= k; ++ i )
res = (res + (LL)C(b, i) * A(a, i) % MOD * C(d, k - i) % MOD * A(a + c - i, k - i) % MOD) % MOD;
cout << res << endl;
return 0;
}
[!NOTE] LeetCode 2514. 统计同位异构字符串数目
题意: TODO
[!TIP] 思路
标准 可重集排列问题
详细代码
class Solution {
public:
using LL = long long;
const static int N = 1e5 + 10, MOD = 1e9 + 7;
LL f[N], g[N];
LL qmi(int a, int k) {
LL ret = 1;
while (k) {
if (k & 1)
ret = (LL)ret * a % MOD;
a = (LL)a * a % MOD;
k >>= 1;
}
return ret;
}
void init() {
f[0] = g[0] = 1;
for (int i = 1; i < N; ++ i ) {
f[i] = f[i - 1] * (LL)i % MOD;
g[i] = g[i - 1] * qmi(i, MOD - 2) % MOD;
}
}
LL get(string & s) {
unordered_map<char, int> hash;
for (auto c : s)
hash[c] ++ ;
LL ret = f[s.size()] % MOD;
for (auto [k, v] : hash)
ret = ret * g[v] % MOD;
return ret;
}
int countAnagrams(string s) {
init();
stringstream ss(s);
string t;
LL res = 1;
while (ss >> t)
res = (res * get(t)) % MOD;
return res;
}
};