组合数学学习笔记(入门版)
排列组合
例1
首先有排列组合公式 ,根据这个递推出 是否可以被k整除,再用二维前缀和预处理出答案即可。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod = 1e9 + 7, N = 2e3 + 10;
int t, k;
int n, m;
int C[N][N];
int sum[N][N]; //前缀和
inline int add(int a, int b) {
if(a + b >= k) return a + b - k;
else return a + b;
}
int main() {
scanf("%d%d", &t, &k);
n = m = 2000;
for(int i = 1; i <= n; i++) C[i][0] = 1;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(i == 1 && j == 1) C[i][j] = 1;
else C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
// cout << i <<' '<<j<<' '<<C[i][j]<< endl; ///
sum[i][j] = 0;
}
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(C[i][j] == 0 && i >= j) sum[i][j] = 1;
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] + sum[i][j] - sum[i - 1][j - 1];
// cout << i <<' '<<j<<' '<<sum[i][j]<< endl; ///
}
}
while(t--) {
scanf("%d%d", &n, &m);
printf("%d\n", sum[n][m]);
}
system("pause");
return 0;
}
组合计数(包含容斥原理)
例1
求 的非负整数解的组数()
方法1:
的方案数 会tle
方法2:
的方案数 OK!!
例2
方程 有多少组解? 其中
——容斥原理!
答案 = 全集 - (恰好满足每个 出现一次的方案数)
一般情况见例4
例3
,满足 方案数
ans = 减去 不合法情况()
不合法情况可以根据容斥原理算出:
时间复杂度
例4
,满足最大的 刚好是 的方案数
ans = {} 的方案数 减去 {} 的方案数
用两次容斥原理即可。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5 + 10, mod = 998244353;
int n, m, k;
ll fac[N], inv[N];
ll quickpow(ll a, ll x) { //底数也开ll 因为可能是inv[]
ll ans = 1;
while(x) {
if(x & 1) {
ans *= a;
ans %= mod;
}
a *= a;
a %= mod;
x /= 2;
}
return ans;
}
void init(int n) {
fac[0] = 1;
for(int i = 1; i <= n; i++) {
fac[i] = fac[i - 1] * i % mod;
}
inv[n] = quickpow(fac[n], mod - 2);
for(int i = n - 1; i >= 1; i--) {
inv[i] = inv[i + 1] * (i + 1) % mod;
}
inv[0] = 1;
}
ll C(int n, int m) {
if(n < m) return 0;
return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
inline ll solve(int k) {
ll ans = C(n, n - m);
for(int i = 1; i * k <= min(n, m); i++) {
if(i % 2) ans = (ans - C(n - m + 1, i) * C(n - k * i, n - m) % mod + mod) % mod;
else ans = (ans + C(n - m + 1, i) * C(n - k * i, n - m) % mod) % mod;
}
return ans;
}
int main() {
init(1e5);
scanf("%d%d%d", &n, &m, &k);
if(k == 0) {
if(m == 0) puts("1");
else puts("0");
system("pause");
return 0;
}
if(n < m || m < k) {
puts("0");
system("pause");
return 0;
}
printf("%lld\n", (solve(k + 1) - solve(k) + mod) % mod);
system("pause");
return 0;
}
例5
题意
有 种物品,给出它们的价值, 组询问,每次给出这 种物品的数量,和想购买东西的价值。问要想买到这些东西,共有多少种方案数?
题解
假设没有数量的限制,那么就是一个完全背包问题
再根据容斥原理(不合法按奇加偶减)减去数量超过的方案数就是最终的答案
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5 + 10;
int n, m, k, q, s;
int c[5], d[5];
ll f[N]; //价值为i的方案数
void init() { // 完全背包
f[0] = 1;
for(int i = 0; i < 4; i++) {
for(int j = c[i]; j <= 100000; j++) {
f[j] += f[j - c[i]];
}
}
}
inline ll solve() {
ll ans = 0;
for(int S = 0; S < (1 << 4); S++) {
int cnt = __builtin_popcount(S);
ll tem = 0;
for(int j = 0; j < 4; j++) {
if(S & (1 << j)) {
tem += 1ll * (d[j] + 1) * c[j];
}
}
if(s < tem) continue;
if(cnt % 2) ans -= f[s - tem];
else ans += f[s - tem];
}
return ans;
}
int main() {
for(int i = 0; i < 4; i++) cin >> c[i];
init();
cin >> q;
while(q--) {
for(int i = 0; i < 4; i++) cin >> d[i];
cin >> s;
printf("%lld\n", solve());
}
system("pause");
return 0;
}
生成函数
普通型生成函数
可以理解成 有无限项的数列 的系数
母函数 模板
#include<cstdio>
using namespace std;
const int N = 1020;
int c1[N],c2[N];
int w[N];
int n;
int main(){
while(scanf("%d",&n)==1&&n){
for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
for(int i=0;i<=n;i++){
c1[i]=0,c2[i]=0;
}
for(int i=0;i<=n;i++){
c1[i]=1;
}
for(int i=2;i<=n;i++){
for(int j=0;j<=n;j++){
for(int k=0;j+k<=n&&k<=w[i];k++){
c2[j+k]+=c1[j];
}
}
for(int k=0;k<=n;k++){
c1[k]=c2[k],c2[k]=0;
}
}
printf("%d\n",c1[n]);
}
return 0;
}
公式
·
所以 中 的系数是 =
(可以用类似 组合数学 求方案数 来表示)
指数型生成函数
在数列的基础上, 还要除以 ,表示排列和顺序无关
指数型母函数 模板
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 15, mod = 1e9 + 7;
int n, m;
int num[N];
int f[N];
double c1[N], c2[N];
int main() {
f[0] = 1;
for(int i = 1; i <= 10; i++) f[i] = f[i - 1] * i;
while(scanf("%d%d", &n, &m) == 2) {
for(int i = 1; i <= n; i++) scanf("%d", &num[i]);
for(int i = 0; i <= n; i++) {
c1[i] = 0;
c2[i] = 0;
}
for(int i=0;i<=num[1];i++){
c1[i] = 1.0 / f[i];
}
for(int i=2;i<=n;i++){
for(int j=0;j<=10;j++){
for(int k=0;j+k<=10&&k<=num[i];k++){
c2[j+k] += 1.0 * c1[j] / f[k];
}
}
for(int k=0;k<=10;k++){
c1[k]=c2[k], c2[k]=0;
}
}
printf("%.0f\n", c1[m] * f[m]);
}
system("pause");
return 0;
}
鸽巢原理
例1
题意
给你 个数,你需要找到这些数中的一些,满足这些数的和是 的倍数,并输出这些数的下标
题解
乍一看感觉好像是 的,但是并不需要,只要预处理出前缀和数组
如果 ,那么直接输出 即可;如果 ,输出 即可
分类:
组合数学
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通