组合数学
作者:互联网
组合数学题目选做
一、[USACO09FEB]Bulls And Cows S
题目链接:[USACO09FEB]Bulls And Cows S
Solution1
由题目可以很快想到DP解决,于是设 \(f_i\) 表示长度为 \(i\) 的排列的合法方案数,那么:
\[f_i=f_{i-1}+f_{i-k-1} \]\(f_{i-1}\) 即位置 \(i\) 不选公牛,那么可以加上前一位所有的方案数;\(f_{i-k-1}\) 表示位置 \(i\) 选公牛,那么只能加上前 \(k-1\) 位的方案数。
预处理,
\[f_i=i+1(0\le i\le k) \]Code
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
const int MOD = 5000011;
LL f[N];
int n, m;
int main () {
cin >> n >> m;
for (int i = 0; i <= m; i ++) f[i] = i + 1;
for (int i = m + 1; i <= n; i ++) {
f[i] += f[i - 1] + f[i - m - 1];
f[i] %= MOD;
}
cout << f[n];
return 0;
}
Solution2
既然是组合题,怎么能少得了组合数硬搞呢?
考虑每次枚举放 \(m\) 只公牛,那么为了使方案合法,首先要在每头公牛之间放 \(k\) 只奶牛,即 \((m-1)\times k\) 只奶牛。那么还剩 \(n-(m-1)\times k\) 只奶牛,我们将其插入每头公牛的两侧,即 \(m+1\) 个位置,于是可以转化为:在 \(m+1\) 个不同盒子中放 \(n-(m-1)\times k\) 个相同球的问题,方案数为:
\[C_{盒子+球-1}^{盒子-1} \]然后就做完了。
Code
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 2e5;
const LL MOD = 5000011;
int n, k;
LL ans = 1, fac[N];
void init() {
fac[0] = 1;
for (int i = 1; i <= 1e5; i ++) fac[i] = fac[i - 1] * i % MOD;
}
LL ksm(LL a, LL b) {
LL sum = 1;
while (b) {
if (b & 1) sum = sum * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return sum;
}
LL inv(LL x) {
return ksm(x, MOD - 2);
}
LL C(LL n, LL m) {
if (n < m) return 0;
LL fm = fac[m] * fac[n - m] % MOD, fz = fac[n];
return fz * inv(fm) % MOD;
}
int main() {
cin >> n >> k;
init();
for (int i = 1; i <= n; i ++) {
LL ball = n - i - (i - 1) * k;
LL box = i + 1;
ans += C(box + ball - 1, box - 1);
ans %= MOD;
}
cout << ans;
return 0;
}
二、方程的解
题目链接:方程的解
Solution
按照题目实现,用插板法算出答案是 \(C_{g-1}^{k-1}\),可以用递推算组合数,高精度加法更好实现。
Code
#include <string>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const LL MOD = 1000;
const int N = 11000;
LL k, x, fac[N];
string f[1001][101];
int a[1000], b[1000], c[1000];
LL ksm(LL a, LL b) {
a %= MOD;
LL sum = 1;
while (b) {
if (b & 1) sum = sum * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return sum;
}
string add(string s1, string s2) {
memset (a, 0, sizeof(a));
memset (b, 0, sizeof(b));
int l1 = s1.size(), l2 = s2.size();
for (int i = 1; i <= l1; i ++) a[i] = int(s1[l1 - i] - '0');
for (int i = 1; i <= l2; i ++) b[i] = int(s2[l2 - i] - '0');
int l3 = 1, x = 0;
while (l3 <= l1 || l3 <= l2) {
c[l3] = a[l3] + b[l3] + x;
x = c[l3] / 10;
c[l3] %= 10;
l3 ++;
}
c[l3] = x;
while (!c[l3]) l3 --;
string s3;
s3.clear();
for (int i = l3; i >= 1; i --) s3 += char(c[i] + '0');
return s3;
}
LL C(LL n, LL m) {
f[0][0] = "1"; f[1][0] = "1";
for (int i = 1; i <= n; i ++)
for (int j = 0; j <= m; j ++) {
if (!j) {
f[i][j] = "1";
continue;
}
if (j > i) break;
if (i - 1 < j) f[i][j] = f[i - 1][j - 1];
else f[i][j] = add(f[i - 1][j], f[i - 1][j - 1]);
}
}
int main() {
cin >> k >> x;
LL g = ksm(x, x);
g --, k --;
C(g, k);
cout << f[g][k] << endl;
return 0;
}
三、车的放置
题目链接:车的放置
Solution
考虑将网格棋盘分成两部分,分成两个矩形。即:
那么,枚举 \(i\) 表示在区域 \(2\) 放的车的个数,方案数是 \(C_{c}^{i}\times A_{d}^{i}\);那么 \(k-i\) 就是区域 \(1\) 车的个数,方案数就是 \(C_{a}^{k-i} \times A_{b+d-i}^{k-i}\),由乘法原理可得:
\[C_{c}^{i}\times A_{d}^{i}\times C_{a}^{k-i} \times A_{b+d-i}^{k-i} \]将所有合法方案累加起来,得到 \(ans\)。
为什么不枚举 \(i\) 表示区域 \(1\) 的车数呢?因为区域 \(1\) 比区域 \(2\) 高,所以不能确定区域 \(1\) 的车有没有占用区域 \(2\) 的位置,但是枚举区域 \(2\) 就不存在这个问题。
Code
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 2100;
const LL MOD = 100003;
int a, b, c, d, k;
LL fac[N];
void init() {
fac[0] = 1;
for (int i = 1; i <= 2000; i ++) fac[i] = fac[i - 1] * i % MOD;
}
LL ksm(LL a, LL b) {
LL sum = 1;
while (b) {
if (b & 1) sum = sum * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return sum;
}
LL inv(LL x) {
return ksm(x, MOD - 2);
}
LL C(LL n, LL m) {
if (n < m) return 0;
LL fz = fac[n], fm = fac[m] * fac[n - m] % MOD;
return fz * inv(fm) % MOD;
}
LL A(LL n, LL m) {
if (n < m) return 0;
LL fz = fac[n], fm = fac[n - m];
return fz * inv(fm) % MOD;
}
int main() {
init();
cin >> a >> b >> c >> d >> k;
LL ans = 0;
for (int i = 0; i <= min(k, min(c, d)); i ++) {
if (k - i > a) continue;
ans += C(c, i) % MOD * A(d, i) % MOD * C(a, k - i) % MOD * A(b + d - i, k - i) % MOD;
ans %= MOD;
}
cout << ans << endl;
return 0;
}
四、[CQOI2014]数三角形
题目链接:[CQOI2014]数三角形
Solution
我们很容易将所有三个点组合的方案算出来,即 \(C_{n\times m}^{3}\),注意这里的 \(n,m\) 表示的是点数,所以要将题目给的加一,所以考虑求出不合法的方案数,从题目中不难发现不合法的方案数就是三点共线的情况,而这条“线”也存在斜率(设为 \(k\))不同的情况,分类讨论:
- \(k=0\) :总共 \(n\) 行,每行 \(m\) 个数,所以是 \(n\times C_{m}^{3}\)
- \(k=+\infty\) :同理,\(m\times C_{n}^{3}\)
- \(k>0\):具体讨论如下
首先想到枚举三角形三个点中左下角的坐标 \((i,j)\) (横,纵坐标),那么右上角的坐标就有 \((m-i)\times (n-j)\) 种取法,但是这样枚举我们不方便求出中间可以取多少个点,所以改变枚举策略。
枚举左下角和右上角的差距 \((i,j)\) ,那么中间点的取值有 \(gcd(i,j)-1\) 种方案(可以用类似向量数乘的思想证明),左下角的取值有 \((m-i)\times (n-j)\) 种方案,总方案就是:
\[(gcd(i,j)-1)\times (m-i)\times (n-j) \]减去即为答案。
- \(k<0\) :可以看做 \(k>0\) 的情况翻折得出,算数一样。
Code
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL n, m;
LL ans;
LL C(LL n) {
return (n - 2) * (n - 1) * n / 6;
}
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
int main() {
cin >> m >> n;
m ++, n ++;
ans = C(m * n);
ans = ans - n * C(m) - m * C(n);
for (LL i = 1; i <= m; i ++)
for (LL j = 1; j <= n; j ++) {
ans -= (gcd(i, j) - 1) * (m - i) * (n - j) * 2;
}
cout << ans;
return 0;
}
五、序列统计
题目链接:序列统计
Solution
题目可以转化为在一个多重集中选组合的问题,具体说来,多重集就是 \({L,L+1,L+2,...,}R\) 其中每种元素有无限多个,从中选出 \(N\) 个,那么单调不降序列就可以看为一种组合,而不是排列,那么长度为 \(N\) 的长度方案数就是:
\[C_{N + R - L + 1-1}^{R - L +1-1} \]即:
\[C_{N + R - L}^{R - L} \]那么题目要求的是:
\[\sum_{i=1}^{N}C_{i + R - L}^{R - L} \]设 \(R-L\) 为 \(m\),原式转化为:
\[\sum_{i=1}^{N}C_{i+m}^{m} \]将式子展开,写成:
\[C_{m+1}^{m}+C_{m+2}^{m}+C_{m+3}^{m}+...+C_{N+m}^{m} \]那么组合的化式子一般用 \(C_{n}^{m}=C_{n-1}^{m-1}+C_{n-1}^{m}\)。
于是在前面添一个项 \(C_{m+1}^{m+1}\),转化为:
\[\begin{aligned} &\;\;\;\;\; C_{m+1}^{m+1}+C_{m+1}^{m}+C_{m+2}^{m}+...+C_{N+m}^{m}-C_{m+1}^{m+1}\\ &=C_{m+2}^{m+1}+C_{m+2}^{m}+...+C_{N+m}^{m}-1\\ &=C_{m+3}^{m+1}+...+C_{N+m}^{m}-1\\ &=C_{N+m+1}^{m+1}-1 \end{aligned} \]所以,\(ans=C_{N+R-L+1}^{R-L+1}-1\)。
Code
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const LL MOD = 1e6 + 3;
const int N = 2e6 + 10;
int t;
LL n, l, r, k;
LL fac[N];
void init () {
fac[0] = 1;
for (int i = 1; i <= 2e6; i ++) fac[i] = fac[i - 1] * i % MOD;
}
LL ksm(LL a, LL b) {
LL sum = 1;
while (b) {
if (b & 1) sum = sum * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return sum;
}
LL inv(LL x) {
return ksm(x, MOD - 2);
}
LL C(LL n, LL m) {
if (n < m) return 0;
LL fz = fac[n], fm = fac[n - m] * fac[m] % MOD;
return fz * inv(fm) % MOD;
}
LL Lucas (LL n, LL m) {
if (!m) return 1;
return C(n % MOD, m % MOD) * Lucas(n / MOD, m / MOD) % MOD;
}
int main() {
init();
cin >> t;
while (t--) {
cin >> n >> l >> r;
k = r - l;
cout << (Lucas(k + n + 1, k + 1) - 1 + MOD) % MOD << endl;
}
return 0;
}
六、[SHOI2015]超能粒子炮·改
题目链接:[SHOI2015]超能粒子炮·改
Solution
题目要求:
\[\sum_{i=0}^{k}C_{n}^{i} \]由于是组合数化式子,想到两条性质:
- \(C_{n}^{m}=C_{n-1}^{m}+C_{n-1}^{m-1}\)
- \(C_{n}^{m}=C_{n\;mod\;p}^{m\;mod\;p}\times C_{n/p}^{m/p}\)
发现每一项下面的 \(n\) 是定值,所以用第一条性质不好搞,那么用第二条(卢卡斯定理)(这个数据范围也能想到用卢卡斯)。
设 \(f(n,k)\) 表示 \(\sum_{i=0}^{k}C_{n}^{i}\)。则原式可化为:
\[\begin{aligned} &\;\;\;\;\; f(n,k)\\ &=\sum_{i=0}^{k}C_{n}^{i}\\ &=\sum_{i=0}^{k}(C_{n\;mod\;p}^{i\;mod\;p}\times C_{n/p}^{i/p})\\ \end{aligned} \]看到形如 \(i/p\) 的式子,我们很自然地联想到整除分块(当然,标准的整除分块更难),于是:
\[\begin{aligned} &\;\;\;\;\;\sum_{i=0}^{k}(C_{n\;mod\;p}^{i\;mod\;p}\times C_{n/p}^{i/p})\\ &=\sum_{i=0}^{p-1}(C_{n\;mod\;p}^{i\;mod\;p}\times C_{n/p}^{0})+\sum_{i=0}^{p-1}(C_{n\;mod\;p}^{i\;mod\;p}\times C_{n/p}^{1})+...+\sum_{i=0}^{k\;mod\;p}(C_{n\;mod\;p}^{i\;mod\;p}\times C_{n/p}^{k/p})\\ &=(\sum_{i=0}^{p-1}C_{n\;mod\;p}^{i}) \times (C_{n/p}^{0}+C_{n/p}^{1}+...+C_{n/p}^{k/p-1})+C_{n/p}^{k/p}\times \sum_{i=0}^{k\;mod\;p}C_{n\;mod\;p}^{i} \\ &=f(n\;mod\;p,p-1)\times f(n/p,k/p-1)+C_{n/p}^{k/p}\times f(n\;mod\;p,k\;mod\;p) \end{aligned} \]\(C_{n/p}^{k/p}\) 可以用 \(Lucas\) 定理求;\(f(n\;mod\;p,p-1)\) 和 \(f(n\;mod\;p,k\;mod\;p)\) 的两个维始终小于 \(p\),所以用 \(f\) 函数的定义式预处理,注意处理边界。
Code
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const LL MOD = 2333;
const int N = 3000;
int t;
LL n, k;
LL c[N][N], f[N][N];
void init() {
c[0][0] = 1;
for (int i = 1; i <= MOD + 10; i ++) {
for (int j = 0; j <= i; j ++) {
if (j == 0) c[i][j] = 1;
else if (i < j - 1) c[i][j] = c[i - 1][j - 1];
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % MOD;
}
}
for (int i = 0; i <= MOD + 10; i ++) {
for (int j = 0; j <= MOD + 10; j ++)
if (j == 0) f[i][j] = c[i][j];
else f[i][j] = f[i][j - 1] + c[i][j], f[i][j] %= MOD;
}
}
LL ksm(LL a, LL b) {
LL sum = 1;
while (b) {
if (b & 1) sum = sum * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return sum;
}
LL Lucas (LL n, LL m) {
if (m == 0) return 1;
if (n == m) return 1;
if (n < m) return 0;
return c[n % MOD][m % MOD] * Lucas(n / MOD, m / MOD) % MOD;
}
LL F(LL n, LL k) {
if (n < 0 || k < 0) return 0;
if (n == 0 || k == 0) return 1;
if (n <= MOD && k <= MOD) return f[n][k];
return (F(n / MOD, k / MOD - 1) % MOD * f[n % MOD][MOD - 1] % MOD + Lucas(n / MOD, k / MOD) * f[n % MOD][k % MOD] % MOD) % MOD;
}
int main() {
ios::sync_with_stdio(0);
init();
cin >> t;
while (t --) {
cin >> n >> k;
cout << F(n, k) << endl;
}
return 0;
}
七、[HNOI2009]有趣的数列
题目链接:[HNOI2009]有趣的数列
Solution
性质1规定了数列元素的范围,没什么好说;性质2规定了奇数项和偶数项是唯一的排列,可以看做组合,虽然意义不同,但数值上是相同的,那么题目就转化为奇数项和偶数项的取值问题。
设一位数若取为奇数项,则记为 \(0\);取为偶数项,记为 \(1\),比如:
1 2 3 4 5 6
记作
0 1 0 1 0 1
1 2 3 5 4 6
记作
0 1 0 0 1 1
初步观察可以发现,任一前缀的0的个数总是大于等于1的个数,这和任一后缀的1的个数总是大于等于0的个数是等价的,我们对后一句话进行证明。
考虑反证,若存在一个后缀的0的个数大于1的个数,即至少存在一个奇数位,没有比其大的偶数位的数字与其匹配,与题目矛盾,故原命题成立。
而不难想到,这就是卡特兰数,所以题目就是让我们求:
\[Cat_n\;mod\;p \]展开:
\[\begin{aligned} C_{n}^{2n}/(n+1)=C_{2n}^{n}-C_{2n}^{n-1}=\frac{(2n)!}{n!n!}-\frac{(2n)!}{(n+1)!(n-1)!} \end{aligned} \]由于这道题没有设模数,所以只能将阶乘转化为算数基本定理的形式,即:
\[x=p_1^{c_1}p_2^{c_2}...p_k^{c_k} \]然后将除法转化为对应质因子的指数相减,阶乘的快速分解质因数可以参考这题。
Code
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 2e6 + 10;
LL n, p;
LL f[N];
LL ans1 = 1, ans2 = 1;
vector<LL> pri;
void init() {
for (LL i = 2; i <= 2e6; i ++) {
if (!f[i]) {
pri.push_back(i);
for (LL j = 2; j * i <= 2e6; j ++) {
f[i * j] = 1;
}
}
}
}
LL count(LL n, LL pr) {
LL ans = 0;
while (n) {
ans += LL(n / pr);
n /= pr;
}
return ans;
}
LL ksm(LL a, LL b) {
LL sum = 1;
while (b) {
if (b & 1) sum = sum * a % p;
a = a * a % p;
b >>= 1;
}
return sum;
}
int main() {
ios::sync_with_stdio(0);
init();
cin >> n >> p;
for (LL i = 0; i < pri.size(); i ++) {
if (pri[i] > 2 * n) break;
LL pr = pri[i];
LL num1 = count(2 * n, pr), num2 = count(n, pr), num3 = count(n - 1, pr), num4 = count(n + 1, pr);
ans1 *= ksm(pri[i], num1 - 2 * num2) % p;
ans1 %= p;
ans2 *= ksm(pri[i], num1 - num3 - num4) % p;
ans2 %= p;
}
cout << ((ans1 - ans2) % p + p) % p << endl;
return 0;
}
标签:return,组合,int,LL,times,数学,include,MOD 来源: https://www.cnblogs.com/zhangyuzhe/p/16536476.html