Acwing - 算法基础课 - 笔记(数学知识 · 三)(补)
标签: Acwing - 算法基础课 - 笔记(数学知识 · 三)(补) HarmonyOS博客 51CTO博客
2023-07-12 18:24:22 179浏览
数学知识(三)
这一小节讲的是高斯消元,组合数。
高斯消元
高斯消元是用来解方程的,通常来说可以在 的时间复杂度内,求出包含
n 个未知数的,n个方程的多元线性方程组的解。如下的方程组就称为多元线性方程组
…
多元线性方程组的解,通常有三种情况:无解;有唯一解;有无穷组解。
上面的方程组,可以将其系数提取出来,形成系数矩阵,并在最右侧添加常数列,形成增广矩阵,即
…
而矩阵具有三种初等行列变换
- 把某一行乘以一个非零的数
- 交换某2行
- 把某行的若干倍加到另一行上
这三种变换,不会影响方程组的解
高斯消元,可以用某种统一的步骤,通过执行上面的初等行列变换,使得方程组的系数矩阵,变成一个倒三角形的矩阵。即变成
…
从最后一个方程能直接求出 ,依次可以往上求解出
,
,…,
当执行完高斯消元后,如果得到的方程组,是
- 完美的阶梯型(第一行有n个未知数,第二行有n-1个未知数,最后一行有1个未知数)
即一共有n个有效方程,此时有唯一解 - 得到的方程组中有形如
0=非零的
此时无解 - 得到的方程组中有形如
0=0的(这种方程是可以被其他方程表示的,是多余的无效方程)
即有效方程的个数小于n,此时有无穷组解
高斯消元的算法步骤如下
枚举每一列(第c列)
- 找到当前这一列系数绝对值最大的一行(从剩余的行中找)
- 将该行交换到最上面(不是交换到第一行,而是交换到剩余行中最上面的一行)
- 将该行的该列的系数变成1
- 使用该行,对所有剩余行进行消元,将该列消成0
注意,每次遍历,找到当前列的绝对值最大的那一行,处理完毕后,后续的处理只针对剩余的行。
#include <iostream>
#include <cmath>
using namespace std;
const int N = 110;
double a[N][N];
int n;
double eps = 1e-8;
int solve() {
int c = 0, r = 0;
for (; c < n; c++) {
// 每轮循环消掉一列
int t = r;
// 先找到当前需要处理的列的最大系数的那一行
for (int i = r + 1; i < n; i++) {
if (fabs(a[i][c]) > fabs(a[t][c])) t = i;
}
// 当前列的最大系数为0, 则不用处理
if (fabs(a[t][c]) < eps) continue;
// 将最大系数的行交换到最上面来
for (int i = c; i <= n; i++) swap(a[t][i], a[r][i]);
// 将该列系数化为1
for (int i = n; i >= c; i--) a[r][i] /= a[r][c];
// 用该行消掉下面所有行的该列
for (int i = r + 1; i < n; i++) {
for (int j = n; j >= c; j--) {
a[i][j] -= a[r][j] * a[i][c];
}
}
// 有效方程数+1
r++;
}
if (r < n) {
// 有效方程数 < n, 可能是无解或无穷多解
for (int i = r; i < n; i++) {
if (fabs(a[i][n]) > eps) return 0; // 无解
}
return 2; // 无穷多解
} else {
// 唯一解
for (int i = n - 1; i >= 0; i--) {
for (int j = n - 1; j > i; j--) {
a[i][n] -= a[i][j] * a[j][n];
}
}
return 1;
}
}
int main() {
std::ios::sync_with_stdio(false);
cin >> n;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= n; j++) {
cin >> a[i][j];
}
}
int res = solve();
if (res == 0) printf("No solution\n");
else if (res == 2) printf("Infinite group solutions\n");
else {
for (int i = 0; i < n; i++) {
if (fabs(a[i][n]) < eps) a[i][n] = 0;
printf("%.2lf\n", a[i][n]);
}
}
return 0;
}
对上面的代码略加解释:
最外圈的循环是针对列而言的,从左到右枚举0到n-1列(这些列都是方程组中的未知数的系数,第n列是某一个方程组右侧的常数)。
最外圈的循环每进行一次,就能够消掉一个未知数(消掉一个元)。
循环的内部,先是从剩余的行(r行及其下面的行)中找到当前列系数绝对值最大的那一行(若当前列系数最大的都是0,则说明当前列无需进行消元了,直接continue,继续处理下一列);随后,将这一行交换到最上方(交换到r行);接着,将这一行当前列的系数化为1(注意代码中是从右往左处理的,因为处理时需要用到最左侧的那一列,若从左往右处理,则最左侧的列一开始就会被更新,从而影响右侧列的更新);最后,遍历剩余行,将剩余行减掉当前行的某一个倍数,使得能够消掉当前列。最后,将r加一,表示该行处理完毕,接下来的处理都针对该行下面的行。
循环结束后,r的值就是最后得到的有效方程的个数。若每次循环都能恰好消去一个元,则r最后就等于n,此时得到的方程组就是一个完美的阶梯型(从上往下,第一个方程有n个未知数,最后一个方程有1个未知数)。此时方程组有唯一解。
若r小于n,则说明方程组中有些方程是无效的。并且,r还没走到最后一行时,就已经处理到了最后一列(已经消元结束)。此时需要对r以下的行,进行判断。r以下的行,由于消元结束,都不再包含未知数了,所以这些行的0到n-1列(未知数的系数)应当都为0,此时需要判断第n列的常数。若这个常数为0,则方程就是0=0,无效方程;若这个常数不为0,则这个方程本身就矛盾了,所以整个方程组无解。
当方程有唯一解时。我们从最后一个方程往上递推,可以依次求解出每个未知数的值。
需要注意,由于我们在消元的过程中,对每个未知数的系数,都化简成了1。则消元结束后,最后一行应当是
即消元结束后最后一行的常数列,直接就是未知数
这一部分也是整个高斯消元的代码中最难以理解的部分,单独拎出来说。
for(int i = n - 1; i >= 0; i--) {
for(int j = i + 1; j < n; j++) {
a[i][n] -= a[i][j] * a[j][n];
}
}
最后,每个未知数的解,我们都是保存在最右侧的常数列上面的。
针对第i行,我们需要解出
比如我们化简后得到的方程组如下
一共3个方程组,我们枚举从第2行到第0行。
即,n=3,我们枚举i,从2到0
i=2时,j从3开始,发现j < n,则直接结束;
i=1时,j从2开始,我们计算 a[1][3] -= a[1][2] * a[2][3],即相当于把 移向,变成
i=0时,j从1开始,先把 减掉,
的值直接取下一行的常数列
a[1][3];随后 j=2,再尝试把 减掉,
的值直接取第
2行的常数列 a[2][3]。
我们可以发现,从最后一行往上计算,每一行的常数列其实都是某个未知数的解。
比如最后一行的常数列,一定是最后一个未知数的解;倒数第二行的常数列,是倒数第二个未知数的解。
更一般的,在我们循环到第 i 行时,其实需要求解的就是 。而这一行中,还有剩下的
到
个未知数需要消掉,这些未知数的解,在先前求解
i 下面的行时,已经求解了出来(是i下面那些行的常数列)。所以我们遍历 ,依次消掉这些未知数,即用这一行的常数列
a[i][n] 来依次 减掉 到
对于 ,
就是先前已经求解出来的第
行的常数列,即
a[j][n],它的系数就是a[i][j]。所以只需要减掉
a[i][j] * a[j][n] 即可。
所以,我们处理某一行 i,求解 时,只需要用这一行的常数列
a[i][n],依次减掉 (
)这些项。而某个未知数
的值为
a[j][n],系数为 a[i][j]。所以更新的公式就是 a[i][n] -= a[i][j] * a[j][n]
最后,所有行的常数列,就是方程组的解。即 =
a[0][n], =
a[1][n],…, =
a[n-1][n]
扩展练习题:高斯消元解异或线性方程组
#include <iostream>
using namespace std;
const int N = 110;
int a[N][N];
int n;
int solve() {
int c = 0, r = 0;
for (; c < n; c++) {
// 找到第一个系数非0的列
int t = -1;
for (int i = r; i < n; i++) {
if (a[i][c] == 1) {
t = i;
break;
}
}
if (t == -1) continue;
// 交换到最上方
for (int i = c; i <= n; i++) swap(a[t][i], a[r][i]);
// 消掉下面方程的该列
for (int i = r + 1; i < n; i++) {
// 只有系数同样为1时才能消掉
if (a[i][c] == 0) continue;
for (int j = c; j <= n; j++) {
a[i][j] = a[r][j] ^ a[i][j];
}
}
r++;
}
if (r < n) {
for (int i = r; i < n; i++) {
if (a[i][n]) return 0;
}
return 2;
} else {
for (int i = n - 1; i >= 0; i--) {
for (int j = n - 1; j > i; j--) {
// 只有系数为1时, 需要异或上这个值
if (a[i][j]) a[i][n] ^= a[j][n];
}
}
return 1;
}
}
int main() {
std::ios::sync_with_stdio(false);
cin >> n;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= n; j++) {
cin >> a[i][j];
}
}
int res = solve();
if (res == 0) printf("No solution\n");
else if (res == 2) printf("Multiple sets of solutions\n");
else {
for (int i = 0; i < n; i++) {
printf("%d\n", a[i][n]);
}
}
return 0;
}
组合数
求组合数有很多种方式,需要根据数据范围来选择使用哪种方式。
递推
对于组合数 可以用一个递推式来表达,即
所以每个组合数
这样我们就可以根据上面的递推式,预处理出数据范围内全部的组合数,查询时,直接查表即可。
针对递推式
考虑一个口袋里一共有 个苹果?,我们从中选
个苹果,则总共的选法就是
,我们考虑将其中一个苹果打上标记(比如称其为金苹果)。那么总共的选法可以分为2大类:包含这个金苹果;不包含这个金苹果。
包含这个金苹果的选法,则是从余下的苹果中,再选 个,即
不包含这个金苹果的选法,则是从余下的苹果中,再选 个,即
所以
数据范围:1 <= n <= 10000, 1 <= b <= a <= 2000
#include <iostream>
using namespace std;
const int MOD = 1e9 + 7;
const int N = 2010;
int c[N][N];
void init() {
for(int i = 0; i < N; i++) {
for(int j = 0; j <= i; j++) {
if(j == 0) c[i][j] = 1;
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % MOD;
}
}
}
int main() {
int n;
cin >> n;
init();
while(n--) {
int a, b;
cin >> a >> b;
cout << c[a][b] << endl;
}
}
需要注意,上面预处理时,i需要从0开始,需要算出 ,而针对
,当
递推法的复杂度是 ,对于这道题的数据范围,计算量就是
2000^2 = 4 × 10^6
预处理阶乘
组合数的公式为
需要的都是某个数的阶乘,那么我们把所有数的阶乘预处理一下,阶乘数组用fact来表示,即fact[i] = i! mod 10^9 + 7。
同时我们注意到,阶乘项也出现在分母中,而由于我们需要对结果取一个模,所以我们可以对i的阶乘,求一下乘法逆元。把除法转化为乘法。某个数的阶乘在模10^9 + 7下的乘法逆元,我们用infact[i]来表示。
于是用这两个预处理出的数组,就能在
由于模的10^9 - 7是个质数,所以我们求乘法逆元可以直接用欧拉定理的特例,费马小定理进行求解。
注:若a和m互质,求a mod m下的乘法逆元时,若m是质数,则用快速幂进行求解,乘法逆元为ap-2;若m不是质数,则用扩展欧几里得算法进行求解 ,就不要傻乎乎的先对m求个欧拉函数,再用欧拉定理做幂运算来求解,太慢了。
设a-1 为逆元,则有 a × a-1 mod m = 1,那么有a × a-1 = km + 1,移项一下就是 a× a-1 - km = 1。我们将a-1用x替换,m用b替换,-k用y替换,则就是 ax + by = 1,其中a和b是固定的,未知数是x和y。我们需要求解的逆元就是x,而a和b是互质的,所以gcd(a,b) = 1,根据裴蜀定理,这个二元线性方程一定有解,并且能用扩展欧几里得算法进行求解。
数据范围:1 <= n <= 10000,1 <= b <= a <= 10^5
#include <iostream>
typedef long long LL;
using namespace std;
const int N = 100010, MOD = 1e9 + 7;
LL fac[N], infac[N];
int qmi(int a, int b, int p) {
int res = 1;
while (b) {
if (b & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
b >>= 1;
}
return res;
}
void init() {
fac[0] = infac[0] = 1;
for (int i = 1; i < N; i++) {
fac[i] = fac[i - 1] * i % MOD;
infac[i] = infac[i - 1] * qmi(i, MOD - 2, MOD) % MOD;
}
}
int main() {
std::ios::sync_with_stdio(false);
init();
int n, a, b;
cin >> n;
while (n--) {
cin >> a >> b;
LL res = fac[a] * infac[a - b] % MOD * infac[b] % MOD;
printf("%lld\n", res);
}
return 0;
}
对于预处理时,求解fact和 infact数组时,
fact[i] = fact[i - 1] * i 这个很好理解,但是逆元数组 infact的递推怎么理解呢?
可以这样想,对于阶乘 ,其逆元为
,有
对于阶乘,我们将其拆分为
,则有
则 的逆元为
,根据模运算的特性,我们将上式进行一下组合变换,有
那么很明显的,
也就是说,我们只需要求解一下 的逆元
于是就能得到 infact数组的递推式:
预处理出2个阶乘数组,时间复杂度为 ,实际性能瓶颈在于计算
infact[i]时,需要使用快速幂计算 i 的 MOD - 2次方,需要的复杂度是 ,而本题的
MOD = 10^9 + 7,log MOD大概等于30,所以实际上复杂度是 30n
注意,只有在模数 为质数,并且
和
都小于
时,才能用预处理阶乘的方式求解。若
,则可能
[1, a]中有的数和 p 不互质,不互质的话,就不存在乘法逆元。
卢卡斯定理
参考:
定义:
对于非负整数 ,
和质数
,有
,其中
,
一般,使用的是下面的式子
,其中
证明:
- 引理1:对于组合数
,其中
为质数,始终满足
这个引理很好证明,根据组合数公式,有,而
是质数,其只存在
和
两个因子,所以分子中的
一定不会被消去,故计算出的结果一定是
的某个倍数,故其在
下为
- 引理2:对于整数
和质数
,始终满足
对进行二项式展开,得到
,根据引理1,中间项在
下都为0,可以消去,最后得到
- Lucas定理
我们先将和
转换为对应的
接着,有
KaTeX parse error: {align*} can be used only in display mode.
即
根据二项式展开,容易知道 就是
展开式中的
的系数,而在上述等式右侧中,就是要知道
对于等式右侧的方程
即为
中
即为
中
- …
即为
中
将 ,
,
,…,
乘起来,得到
所以右式中 的系数为
,所以有
为了将 转化为这样的形式:
我们先把 拆分成
和
两部分,首先对于
,它就等于
,因为我们将
和
写成
进制表示时,最后的余数就是
和
。接着,我们只需要证明一下
由于我们是先将 和
,分别写成了
随后推出的
现在,我们将 进制下的
和
分别右移一位(相当于除以
),得到的是
我们只要将 和
带入前面的公式,就能得到如下式子(下面用
替代
,
于是,整个卢卡斯定理得证,对于非负整数 ,
以及质数
练习题:求组合数III
数据范围:1 <= n <= 20, 1 <= b <= a <= 10^18,1 <= p <= 10^5,其中 p 是质数
由于每组数据中 p 都不一样,所以这道题用预处理阶乘的方式不是特别合适,因为一次预处理的结果,只能适用于一组数据,对于下一组数据,由于 p 不同,还得重新计算一次阶乘。
// 1283ms
#include <iostream>
typedef long long LL;
using namespace std;
// 快速幂
LL qmi(LL a, LL b, LL p) {
LL res = 1;
while (b) {
if (b & 1) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
// 小于p的组合数直接求
LL C(LL a, LL b, LL p) {
if (b > a) return 0;
LL res = 1;
for (int i = a, j = 1; j <= b; j++, i--) {
res = res * i % p;
res = res * qmi(j, p - 2, p) % p;
}
return res;
}
// 卢卡斯定理
LL lucas(LL a, LL b, LL p) {
if (a < p && b < p) return C(a, b, p);
return C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}
int main() {
std::ios::sync_with_stdio(false);
LL n, a, b, p;
cin >> n;
while (n--) {
cin >> a >> b >> p;
printf("%lld\n", lucas(a, b, p));
}
return 0;
}
将上面的C函数修改一下,只在最后,算一次乘法逆元,可以极大的优化时间复杂度
LL C(LL a, LL b, LL p) {
if (b > a) return 0;
if (b > a - b) b = a - b;
LL x = 1, y = 1;
for (int i = a, j = 1; j <= b; j++, i--) {
x = x * i % p;
y = y * j % p;
}
return x * qmi(y, p - 2, p) % p;
}
完整代码如下
// 66ms
#include <iostream>
typedef long long LL;
using namespace std;
LL qmi(LL a, LL b, LL p) {
LL res = 1;
while (b) {
if (b & 1) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
LL C(LL a, LL b, LL p) {
if (b > a) return 0;
if (b > a - b) b = a - b; //小优化, 因为 C(a, b) = C(a, a - b)
LL x = 1, y = 1;
for (int i = a, j = 1; j <= b; j++, i--) {
x = x * i % p;
y = y * j % p;
}
// 只在最后求一次乘法逆元
return x * qmi(y, p - 2, p) % p;
}
LL lucas(LL a, LL b, LL p) {
if (a < p && b < p) return C(a, b, p);
return C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}
int main() {
std::ios::sync_with_stdio(false);
LL n, a, b, p;
cin >> n;
while (n--) {
cin >> a >> b >> p;
printf("%lld\n", lucas(a, b, p));
}
return 0;
}
至于时间复杂度,因为每次递归都是除以了个 p ,递归的层数就是 ,每层中的复杂度就是计算
C(a % p, b % p) 的复杂度,如果是优化之前,计算 C(a, b)最多需要算 b 次乘法,每次还需要求解个逆元,复杂度应该是 ,优化后,只需要在最后求解一次逆元,复杂度是
,我们将
看成最大能取到
,而
实际最大只能取到
,那么,优化前的复杂度是
,优化后的复杂度是
,实际上由于在
b > a - b时,做了 b = a - b的优化,所以b实际最多取到 a / 2,优化后实际的复杂度会更低,实际只需要 ,其中的
表示
的取值,这道题目的数据范围,
最大能取到
卢卡斯定理,适用于 a 和 b 特别大,但是模数 p 是质数并且比较小的情况。
当 a 大于 p 时,不能依靠乘法逆元来进行计算,一是因为时间复杂度很高,二是因为 a 大于 p 时,[1, a]中间的一些数的乘法逆元不一定存在,因为某个数 x 在模 p 下的乘法逆元存在,一定要求 x 与 p 互质。
故需要使用卢卡斯定理。
高精度
前面几种求解组合数,都是求解在模某个数下的结果,而现在需要精确的求解出组合数的结果,这个结果可能很大。
数据范围:1 <= b <= a <= 5000
思路:高精度,直接从公式出发。
可以实现一个高精度乘法,以及一个高精度除法。但是那样的计算效率比较低,也比较难以实现。所以可以换一种思路,先对
问题变成了,如何对 分解质因数呢?可以先对分子中的
分解一下质因数,看某个因子出现了多少次,再对分母中的
和
但是如果对分子分母中每个数依次分解质因数,那么时间复杂度是 ,这道题目的数据范围是 5000,这个复杂度下也是不会超时的,但效率较低(之后自己试了下,效率相差也没有很大),可以换另一种方法,我们可以先把 5000 以内的质数全部预处理出来。
然后对于如何求解 的某个质因子
第一项是所有小于 的数中,
的倍数的个数;但有的数可能是
的倍数,但第一项只加了一次;所以我们需要再加上第二项 ,同理需要加上第三项,…,假设某个数分解质因数后,包含了
,那么该数对
的次数的贡献一共是
,该数会被恰好加
次,在
时加1次,在
时加1次,…,在
所以我们的求解步骤是:
- 先用筛法求出
1-5000内的全部质数 - 对于分子和分母的阶乘表示,求出每个质数的次数
- 用高精度乘法将每个质因子乘起来
// 47ms
#include <iostream>
#include <vector>
using namespace std;
const int N = 5010;
int primes[N], cnt;
bool st[N];
int sum[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;
}
}
}
// n! 中 , p 的次数为多少
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) {
int c = 0;
vector<int> res;
for (int i = 0; i < a.size(); i++) {
c += a[i] * b;
res.push_back(c % 10);
c /= 10;
}
while (c) {
res.push_back(c % 10);
c /= 10;
}
return res;
}
int main() {
std::ios::sync_with_stdio(false);
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(b, p) - get(a - 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--) {
printf("%d", res[i]);
}
printf("\n");
return 0;
}
分别对每个数分解质因数,并计算次数,发现效率也没有相差的特别大
// 53ms
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
void process(int x, unordered_map<int, int>& m, bool add) {
for (int i = 2; i <= x / i; i++) {
while (x % i == 0) {
if (add) m[i]++;
else m[i]--;
x /= i;
}
}
if (x > 1) {
if (add) m[x]++;
else m[x]--;
}
}
vector<int> mul(vector<int> a, int b) {
vector<int> res;
int c = 0;
for (int i = 0; i < a.size(); i++) {
c += a[i] * b;
res.push_back(c % 10);
c /= 10;
}
while (c) {
res.push_back(c % 10);
c /= 10;
}
return res;
}
int main() {
std::ios::sync_with_stdio(false);
int a, b;
cin >> a >> b;
unordered_map<int, int> primes;
for (int i = a, j = 1; j <= b; j++, i--) {
process(i, primes, true);
process(j, primes, false);
}
vector<int> res;
res.push_back(1);
for (auto& [k, v] : primes) {
for (int i = 0; i < v; i++) {
res = mul(res, k);
}
}
for (int i = res.size() - 1; i >= 0; i--) {
printf("%d", res[i]);
}
printf("\n");
return 0;
}
卡特兰数
卡特兰数的应用非常广,很多方案的方案数都是卡特兰数。
比如火车进出栈问题(在某个操作序列的任意前缀中,进栈操作次数一定得大于等于出栈操作次数),合法括号序列(任意前缀中,左括号数量一定得大于等于右括号数量)。
应用题:889. 满足条件的01序列
给定 n 个 0 和 n 个 1,它们按照某种顺序排成长度为 2n 的序列,求满足任意前缀中0的个数都不小于1的个数的序列,有多少个。
我们可以先把问题进行一下转化
把任何一个排列转化成一个路径(比如一共6个1和6个0,0表示向右走,1表示向上走,那么每个排列,都可以表示为一条从坐标[0,0]走到[6,6]的路径)

如果某个排列中,任意前缀中0的个数要大于等于1的个数,也就等价于,这个排列对应的路径,路径上的任何点的坐标(x, y),都要满足x >= y,于是我们可以画一条对角线 y = x,那么只要保证,路径始终位于这条对角线的下方(可以恰好到达对角线,但不能穿过这条对角线)。根据下图,也就等价于,在任何时刻,路径都必须在红色这条线的下方(严格的下方,不能到达红色这根线)

那么,现在问题等价于,求解所有从[0, 0]走到[6, 6],且不经过红色这条线的,所有路径的个数。
那么我们可以先求解所有路径的总数,然后再求解一下经过红色这条线的路径的个数,将两者相减,即得到答案。
对于上述 n = 6,总的路径数量就是
那么经过红色这条线的路径的个数要怎么求呢?
对于任意一条经过红色这条线的路径,我们可以找到该路径与红线的第一个交点,然后把该点之后的剩余路径部分,关于红线做一个轴对称。
比如下面的橙线为一条经过红线的路径,我们将其与红线第一个交点之后的路径部分,关于红线做轴对称(加粗绿线),会发现其最终到达 [5, 7]这个点,这个点恰好和 [6, 6] 关于红线对称。
那么,任何一条经过红线的路径,我们都可以通过上述变换,将其转变为一条从[0, 0],走到[5, 7]的路径。所以,任何一条经过红线,从[0, 0]走到[6, 6]的路径,都能对应一条从[0, 0]走到[5, 7]的路径,反之同样成立。所以这两者是一一对应的,是可以等价的。

所以,经过红线从[0, 0] 走到[6, 6]的路径的个数,就等于从[0, 0] 走到 [5, 7] 的路径个数,而后者的个数计算很简单,就是 ,所以对于 n = 6,问题的答案就是
对于一般的 n,问题的答案则是:
而这个数就被称为卡特兰数
// 295ms
#include <iostream>
typedef long long LL;
using namespace std;
const int MOD = 1e9 + 7;
int qmi(int a, int b, int p) {
int res = 1;
while (b) {
if (b & 1) res = (LL) res * a % p;
a = (LL)a * a % p;
b >>= 1;
}
return res;
}
int main() {
std::ios::sync_with_stdio(false);
int n;
cin >> n;
int res = 1;
for (int i = 2 * n, j = 1; j <= n; j++, i--) {
res = (LL)res * i % MOD;
res = (LL)res * qmi(j, MOD - 2, MOD) % MOD;
}
res = (LL)res * qmi(n + 1, MOD - 2, MOD) % MOD;
printf("%d\n", res);
return 0;
}
优化:
// 20ms
#include <iostream>
typedef long long LL;
using namespace std;
const int MOD = 1e9 + 7;
int qmi(int a, int b, int p) {
int res = 1;
while (b) {
if (b & 1) res = (LL) res * a % p;
a = (LL)a * a % p;
b >>= 1;
}
return res;
}
int main() {
std::ios::sync_with_stdio(false);
int n;
cin >> n;
int x = 1, y = n + 1;
for (int i = 2 * n, j = 1; j <= n; j++, i--) {
x = (LL)x * i % MOD;
y = (LL)y * j % MOD;
}
int res = (LL)x * qmi(y, MOD - 2, MOD) % MOD;
printf("%d\n", res);
return 0;
}
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论
您可能感兴趣的博客
