数学知识(三)——高斯消元、组合计数

本文详细介绍了高斯消元法在解决线性方程组和异或线性方程组中的应用,包括如何转换矩阵至阶梯形和上三角形以求解。同时,讲解了组合数的计算方法,如递推法、预处理逆元法和Lucas定理。最后提到了卡特兰数及其在特定序列问题中的应用。
摘要由CSDN通过智能技术生成

前言

算法基础课第四章 数学知识(三)

共7题,知识点如下

高斯消元
AcWing 883. 高斯消元解线性方程组
AcWing 884. 高斯消元解异或线性方程组
求组合数
AcWing 885. 求组合数 I
AcWing 886. 求组合数 II
AcWing 887. 求组合数 III
AcWing 888. 求组合数 IV
AcWing 889. 满足条件的01序列

高斯消元

是线性代数基础内容

在这里插入图片描述
在这里插入图片描述

操作123,以方程的角度理解,分别对应:

1、对某一个方程,两边乘相同的非0数

2、方程组里的某两个方程交换位置

3、把一个方程的若干倍,加到另一个方程上去

可以通过这三个操作,将上面方程转变成上三角形式,即
在这里插入图片描述
最后一行,xn可以直接求出来,倒数第二行就也是只有一个未知数了,
可求出xn-1 。同理,倒着往上一行行即可求出所有x的值
在这里插入图片描述

非完美阶梯形,即不是刚好n个方程,

如第一行n个未知数,第二行只有n-2个的,就不算完美阶梯形

非完美阶梯形分为两种情况

无解——方程出现左边没未知数,右边数非0的情况

无穷多组解——有些方程最后是0 = 0【即这些方程其实可以被其他方程表示,是多余的。如有n个未知数,最后只有n-1个方程】

模板

// a[N][N]是增广矩阵
int gauss()
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )
    {
        int t = r;
        for (int i = r; i < n; i ++ )   // 找到绝对值最大的行
            if (fabs(a[i][c]) > fabs(a[t][c]))
                t = i;

        if (fabs(a[t][c]) < eps) continue;

        for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]);      // 将绝对值最大的行换到最顶端
        for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];      // 将当前行的首位变成1
        for (int i = r + 1; i < n; i ++ )       // 用当前行将下面所有的列消成0
            if (fabs(a[i][c]) > eps)
                for (int j = n; j >= c; j -- )
                    a[i][j] -= a[r][j] * a[i][c];

        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][n]) > eps)
                return 2; // 无解
        return 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];

    return 0; // 有唯一解
}

AcWing 883. 高斯消元解线性方程组

例子
在这里插入图片描述
高斯消元步骤
在这里插入图片描述
只写系数,如下
在这里插入图片描述
①②当前列绝对值最大的,即2开头那行,换上去
在这里插入图片描述
③将换上去的那行,第一个系数变成1【此时为两边都除2】
在这里插入图片描述

④将下面所有行的 当前列 【此时是第一列】消掉,
【此时第二行减去第一行、第三行加上第一行】
在这里插入图片描述
当前行列固定【此时为第一列,第一行不变了】,
看下一列,重复高斯消元操作【此时第二行为当前的最上面】
①②③即找到绝对值最大的那列置1,并移到最上面
在这里插入图片描述
④将下面其他当前列的值消掉
在这里插入图片描述

此时是完美阶梯型,有唯一解

求解过程,倒着消,将最后一行的系数置1【其实上面过程中已经做了】,如下
在这里插入图片描述
再消去倒数第二行的xn 的系数,【此时为减去最后一行的三分之一】
在这里插入图片描述

再消去上一行的所有系数
【此时为减去第二行的二分之一,再加上第三行的1.5倍】
在这里插入图片描述

此时为消去x2的系数,还有x3未消除,消除后如下
在这里插入图片描述

换成方程形式
在这里插入图片描述
将0倍的擦掉即可
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;

const int N = 110;
const double eps = 1e-6; // 浮点数有误差,不能直接判断0,用很小的数来,比它小即0

int n;
double a[N][N]; // 系数矩阵
int gauss()  // 高斯消元,答案存于a[i][n]中,0 <= i < n
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )
    {
        int t = r;
        // 找到当前列,绝对值最大的那行
        for (int i = r; 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]; // 该行所有数除掉第一个数即可
        // 用当前行将下面所有的列消成0[当前行是r.第一轮消去第一列···]
        for (int i = r + 1; i < n; i ++ ) {
            if (fabs(a[i][c]) > eps) { // 不是0才操作
                for (int j = n; j >= c; j -- )
                    a[i][j] -= a[r][j] * a[i][c];  // 减去r行j列【当前列最顶端的数,系数为1】 乘 当前行的第一个数
            }
        }
        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][n]) > eps)
                return 2; // 无解
        return 1; // 有无穷多组解
    }
    
    // 倒着消元, 将除了x[i]的系数都消0
    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]; // 减去他的系数 乘 他的值,
            //如第一次时, 最后一列此时x[n]系数已经为1,就是倒数第二个方程的x[n-1]系数消掉
            // 即x[n-1]的系数 减去最后一列【系数1】乘 自己,就消掉了
    return 0; // 有唯一解
}

int main() {
    // 读入方程组
    cin >> n;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n + 1; j++)
            cin >> a[i][j];
    }

            
    int t = gauss();
    if (t == 0) { 
        for (int i = 0; i < n; i++) {
            if (fabs(a[i][n]) < eps) a[i][n] = 0;  // 去掉输出 -0.00 的情况
            printf("%.2lf\n", a[i][n]); // 保留两位小数
        }
    }
    else if (t == 1) puts("Infinite group solutions");
    
    else puts("No solution");
    
    return 0;
}

AcWing 884. 高斯消元解异或线性方程组

在这里插入图片描述
样例如下

在这里插入图片描述

异或别名:不进位加法

相同为0,不同为1

与普通的高斯消元步骤一样,忘了就去复习一下
在这里插入图片描述

选第一列,①②③默认做了
在这里插入图片描述
④下面消0【异或一下最后一行】
在这里插入图片描述
枚举第二列,消0后
在这里插入图片描述
已经是上三角矩阵了,表示有唯一解

消完需要倒着再消一遍,将答案推出来

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n;
int a[N][N];

int gauss() {
    int c, r; // c:列 , r:行
    for (r = c = 0; c < n; c++) { // 从0开始枚举列,枚举n次
        int t = r; // 从当前行开始
        for (int i = r; i < n; i++) // 往下找
            if (a[i][c]) { // 当前行的第c列不是0,即找到了非0行
                t = i;
                break;
            }
        if (!a[t][c]) continue; // 都是0,没找到非0行,这轮不做了
        // 找到非0行了,交换上去
        for (int i = c; i <= n; i++) swap(a[t][i], a[r][i]);
        // 将当前行的下面所有行的当前列消0
        for (int i = r + 1; i < n; i++) 
            if (a[i][c]) // 不是0才消
                for (int j = c; j <= n; j++) 
                    a[i][j] ^= a[r][j];  // 第r行异或到第i行去即可
        r++; // 下一行
    }
    
    if (r < n) { // 上三角矩阵行数小于n
        for (int i = r; i < n; i++)
            if (a[i][n]) // 等式左边都是0,若等式右边不为0则矛盾,无解
                return 2;
        return 1; // 无穷多组解
    }
    // 倒着推一遍把解算出来
    for (int i = n - 1; i >= 0; i--)    // 第n-1行开始往上推
        for (int j = i + 1; j < n; j++) // 把后面的所有系数消掉
            a[i][n] ^= a[i][j] & a[j][n]; // 系数*x[j]的值 ,*或&都可
            
    return 0; //唯一解
}

int main() 
{
    cin >> n;
    for (int i = 0; i < n; i++) 
        for (int j = 0; j < n + 1; j++) 
            cin >> a[i][j];
            
    int res = gauss();
    
    if (res == 0) {
        for (int i = 0; i < n; i++) cout << a[i][n] << endl;
    }
    else if (res == 1) puts("Multiple sets of solutions");
    else puts("No solution");
    
    return 0;
}

与普通版基本一样

组合计数

求组合数
在这里插入图片描述

从a个苹果中选出b个的,方案数

选出b中的一个苹果,可以根据包含这个苹果和不包含分为两类,

两类加起来和原来方案数一致

模板——递推法求组合数

// c[a][b] 表示从a个苹果中选b个的方案数
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;

AcWing 885. 求组合数 I

通过预处理可以降低数据量

在这里插入图片描述

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 2010, 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; // 中选0个,  返回1
            else c[i][j] = (c[i-1][j] + c[i-1][j-1]) % mod; // 结果要取余
        }
    }
}

int main() {
    init();
    
    int n;
    scanf("%d", &n);
    while(n--) {
        int a, b;
        scanf("%d%d", &a, &b);
        printf("%d\n", c[a][b]);
    }
    return 0;
}

模板——通过预处理逆元的方式求组合数

首先预处理出所有阶乘取模的余数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;
}

// 预处理阶乘的余数和阶乘逆元的余数
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;
}

AcWing 886. 求组合数 II

在这里插入图片描述
除就相当于乘逆元,fact[i]i的阶乘

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;
// 预处理10w个数[题目给的1w是照顾java]
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;
}

int main() {
    fact[0] = infact[0] = 1; // 0的阶乘都是1
    for (int i = 1; i < N; i++) {
        // 每个数的范围是1e9, 转成LL防止爆int
        fact[i] = (LL)fact[i-1] * i % mod;
        // 求逆元,忘了可以复习上节课
        infact[i] = (LL)infact[i-1] * qmi(i, mod - 2, mod) % mod;
    }
    
    int n;
    scanf("%d", &n);
    while(n--) {
        int a, b;
        scanf("%d%d", &a, &b);
        // 两个1e9相乘不会溢出LL,三个就会溢出了,需要及时mod
        printf("%d\n", (LL)fact[a] * infact[b] % mod * infact[a -b] % mod);
    }
    
    return 0;
}

模板——Lucas定理

若p是质数,则对于任意整数 1 <= m <= n,有:
    C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)

int qmi(int a, int k, int p)  // 快速幂模板
{
    int res = 1 % p;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}

int C(int a, int b, int p)  // 通过定理求组合数C(a, b)
{
    if (a < b) return 0;

    LL x = 1, y = 1;  // x是分子,y是分母
    for (int i = a, j = 1; j <= b; i --, j ++ )
    {
        x = (LL)x * i % p;
        y = (LL) y * j % p;
    }

    return x * (LL)qmi(y, p - 2, p) % p;
}

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;
}

AcWing 887. 求组合数 III

在这里插入图片描述

log p大的时候logp 很小

卢克斯定理简单证明

先将a和b变成类似p进制的的东西,p是质数

用生成函数证明

在这里插入图片描述
(1+x)p 展开模p,中间的可以都消掉,p上面加上k次也是一样的
在这里插入图片描述

等式两边 xb 的系数 [a的里面选b个x]
在这里插入图片描述
若bi > ai,即 x= 0

在这里插入图片描述
记住这个式子就行,证明了解即可

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

int p;

// 快速幂
int qmi(int a, int k) {
    int res = 1;
    while(k) {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}

// 排列组合公式
int C(int a, int b) {
    
    int res = 1;
    for (int i = 1, j = a; i <= b; i++, j--) { // 如C(6,2)
        res = (LL)res * j % p; // 6 * 5
        res = (LL)res * qmi(i, p - 2) % p; // 6 * 5 / 1 * 2
    } 
    return res;
}

// 卢卡斯定理,需要传LL
int lucas(LL a, LL b) { 
    if (a < p && b < p) return C(a, b);
    return (LL)C(a % p, b % p) * lucas(a / p, b / p) % p; // C别写掉了
}

int main() {
    int n;
    cin >> n;
    while (n --) {
        LL a, b;
        cin >> a >> b >> p;
        cout << lucas(a, b) << endl;
    }
    return 0;
}

模板——分解质因数法求组合数

当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
    1. 筛法求出范围内的所有质数
    2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + ...
    3. 用高精度乘法将所有质因子相乘

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)       // 求n!中的次数
{
    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;
}

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]);

AcWing 888. 求组合数 IV

在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 5010;

int primes[N], cnt;
int sum[N];
bool st[N];

// 筛质数
void get_primers(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) {
    vector<int> c;
    int t = 0; // 表示进位的数
    for (int i = 0; i < a.size(); i++) {
        t += a[i] * b;       // 当前位*b
        c.push_back(t % 10); // 当前位
        t /= 10;
    }
    
    // t不是0,就把t的个位拿出来
    while (t) {
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}

int main() {
    int a, b;
    cin >> a >> b;
    
    get_primers(a);
    // 求每个质数的次数
    for (int i = 0; i < cnt; i++) {
        int p = primes[i];
        // 当前数包含的p的次数
        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]);
    }
    puts("");
    
    return 0;
}

模版——卡特兰数

给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,
满足任意前缀中0的个数都不少于1的个数的序列的数量为:
 Cat(n) = C(2n, n) / (n + 1)

AcWing 889. 满足条件的01序列

卡特兰数

非常巧妙,一般很难想到,是个定理
可以将任何一组数列转化成路径

应用非常广泛

在这里插入图片描述

每一个排列对应用一个路径
路径也对应排列

在这里插入图片描述

一定不能走到红颜色那条路上,更不能超过,最多到绿线【x=y】

相当于求从(0, 0) 走到 (n, n)的,并且不经过红边的,所有路径个数
在这里插入图片描述
任何从(0, 0) 走到 (n, n)的路径,从与红边相交的点,将后面的路径
沿着红线做轴对称,最后一定会走到(5, 7);【不合法路径】

假设此时n=6

剩下的就是没有经过红边的点

所以为n时,答案为C(2n, n) - C(2n, n - 1);

在这里插入图片描述
左边上下乘n+1,右边上下乘n,转成相同除数
在这里插入图片描述

在这里插入图片描述

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int  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 = 2 * n, b = n;
    int res = 1;
    // C(2n, n) / n + 1
    // C(2n, n): (2n)*(2n-1)···(2n -n) / n*(n-1)···(1)
    for (int i = a; i > a - b; i--) res = (LL)res * i % mod; //  res = (2n)*(2n-1)···(2n -n)
    //  res = res / n*(n-1)···(1)
    for (int i = 1; i <= b; i++) res = (LL)res * qmi(i, mod-2, mod) % mod;
    //  res = res / n-1
    res = (LL)res * qmi(n + 1, mod - 2, mod) % mod;
    
    cout << res << endl;
    
    return 0;
}

卡特兰数是一定要学会的问题

火车进出栈问题

合法括号序列问题等

都是卡特兰数的应用

习题课的题不单独开一篇了,后面直接到相应模块补上

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值