Acwing - 算法基础课 - 笔记(数学知识 · 三)(补)

news2024/12/28 6:42:47

数学知识(三)

这一小节讲的是高斯消元,组合数。

高斯消元

高斯消元是用来解方程的,通常来说可以在 O ( n 3 ) O(n^3) O(n3) 的时间复杂度内,求出包含 n 个未知数的,n个方程的多元线性方程组的解。如下的方程组就称为多元线性方程组

a 11 x 1 + a 12 x 2 + . . . + a 1 n x n = b 1 a_{11}x_1+a_{12}x_2+...+a_{1n}x_n=b_1 a11x1+a12x2+...+a1nxn=b1

a 21 x 1 + a 22 x 2 + . . . + a 2 n x n = b 2 a_{21}x_1+a_{22}x_2+...+a_{2n}x_n=b_2 a21x1+a22x2+...+a2nxn=b2

a 31 x 1 + a 32 x 2 + . . . + a 3 n x n = b 3 a_{31}x_1+a_{32}x_2+...+a_{3n}x_n=b_3 a31x1+a32x2+...+a3nxn=b3

a n 1 x 1 + a n 2 x 2 + . . . + a n n x n = b n a_{n1}x_1+a_{n2}x_2+...+a_{nn}x_n=b_n an1x1+an2x2+...+annxn=bn

多元线性方程组的解,通常有三种情况:无解;有唯一解;有无穷组解。

上面的方程组,可以将其系数提取出来,形成系数矩阵,并在最右侧添加常数列,形成增广矩阵,即

[ a 11 , a 12 , . . . , a 1 n , b 1 ] [a_{11},a_{12},...,a_{1n},b_1] [a11,a12,...,a1n,b1]

[ a 21 , a 22 , . . . , a 2 n , b 2 ] [a_{21},a_{22},...,a_{2n},b_2] [a21,a22,...,a2n,b2]

[ a n 1 , a n 2 , . . . , a n n , b n ] [a_{n1},a_{n2},...,a_{nn},b_n] [an1,an2,...,ann,bn]

而矩阵具有三种初等行列变换

  1. 把某一行乘以一个非零的数
  2. 交换某2行
  3. 把某行的若干倍加到另一行上

这三种变换,不会影响方程组的解

高斯消元,可以用某种统一的步骤,通过执行上面的初等行列变换,使得方程组的系数矩阵,变成一个倒三角形的矩阵。即变成

a 11 x 1 + . . . + a 1 n x n = b 1 a_{11}x_1+...+a_{1n}x_n=b_1 a11x1+...+a1nxn=b1

a 22 x 2 + . . . + a 2 n x n = b 2 a_{22}x_2+...+a_{2n}x_n=b_2 a22x2+...+a2nxn=b2

a n n x n = b n a_{nn}x_n=b_n annxn=bn

从最后一个方程能直接求出 x n x_n xn,依次可以往上求解出 x n − 1 x_{n-1} xn1 x n − 2 x_{n-2} xn2,…, x 1 x_1 x1

当执行完高斯消元后,如果得到的方程组,是

  • 完美的阶梯型(第一行有n个未知数,第二行有n-1个未知数,最后一行有1个未知数)

    即一共有n个有效方程,此时有唯一解

  • 得到的方程组中有形如 0=非零

    此时无解

  • 得到的方程组中有形如 0=0的(这种方程是可以被其他方程表示的,是多余的无效方程)

    即有效方程的个数小于n,此时有无穷组解

高斯消元的算法步骤如下

枚举每一列(第c列)

  • 找到当前这一列系数绝对值最大的一行(从剩余的行中找)
  • 将该行交换到最上面(不是交换到第一行,而是交换到剩余行中最上面的一行)
  • 将该行的该列的系数变成1
  • 使用该行,对所有剩余行进行消元,将该列消成0

注意,每次遍历,找到当前列的绝对值最大的那一行,处理完毕后,后续的处理只针对剩余的行。

练习题:acwing - 883: 高斯消元解线性方程组

#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。则消元结束后,最后一行应当是 x n = c x_n=c xn=c

即消元结束后最后一行的常数列,直接就是未知数 x n x_n xn 的解了。我们依次带入上面的行,即可求出其他的未知数的解。

这一部分也是整个高斯消元的代码中最难以理解的部分,单独拎出来说。

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行,我们需要解出 x i x_i xi

比如我们化简后得到的方程组如下

x 0 + 1.5 x 1 + 2 x 2 = 7.5 x_0+1.5x_1+2x_2=7.5 x0+1.5x1+2x2=7.5

x 1 + 0.5 x 2 = 3.5 x_1+0.5x_2=3.5 x1+0.5x2=3.5

x 2 = 4 x_2 = 4 x2=4

一共3个方程组,我们枚举从第2行到第0行。

即,n=3,我们枚举i,从20

i=2时,j3开始,发现j < n,则直接结束;

i=1时,j2开始,我们计算 a[1][3] -= a[1][2] * a[2][3],即相当于把 x 1 + 0.5 x 2 = 3.5 x_1+0.5x_2=3.5 x1+0.5x2=3.5 移向,变成 x 1 = 3.5 − 0.5 x 2 x_1=3.5-0.5x_2 x1=3.50.5x2

i=0时,j1开始,先把 1.5 x 1 1.5x_1 1.5x1 减掉, x 1 x_1 x1 的值直接取下一行的常数列 a[1][3];随后 j=2,再尝试把 2 x 2 2x_2 2x2 减掉, x 2 x_2 x2 的值直接取第2行的常数列 a[2][3]

我们可以发现,从最后一行往上计算,每一行的常数列其实都是某个未知数的解。

比如最后一行的常数列,一定是最后一个未知数的解;倒数第二行的常数列,是倒数第二个未知数的解。

更一般的,在我们循环到第 i 行时,其实需要求解的就是 x i x_i xi。而这一行中,还有剩下的 x i + 1 x_{i+1} xi+1 x n − 1 x_{n-1} xn1 个未知数需要消掉,这些未知数的解,在先前求解 i 下面的行时,已经求解了出来(是i下面那些行的常数列)。所以我们遍历 j ∈ [ i + 1 , n − 1 ] j \in [i+1,n-1] j[i+1,n1],依次消掉这些未知数,即用这一行的常数列 a[i][n] 来依次 减掉 x i + 1 x_{i+1} xi+1 x n − 1 x_{n-1} xn1 这些未知数的项即可。

对于 j ∈ [ i + 1 , n − 1 ] j \in [i+1,n-1] j[i+1,n1] x j x_j xj 就是先前已经求解出来的第 j j j 行的常数列,即 a[j][n],它的系数就是a[i][j]。所以只需要减掉

a[i][j] * a[j][n] 即可。

所以,我们处理某一行 i,求解 x i x_i xi 时,只需要用这一行的常数列 a[i][n],依次减掉 x j x_j xj j ∈ [ i + 1 , n − 1 ] j \in [i+1,n-1] j[i+1,n1])这些项。而某个未知数 x j x_j xj 的值为 a[j][n],系数为 a[i][j]。所以更新的公式就是 a[i][n] -= a[i][j] * a[j][n]

最后,所有行的常数列,就是方程组的解。即 x 0 x_0 x0 = a[0][n] x 1 x_1 x1 = a[1][n],…, x n − 1 x_{n-1} xn1 = 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;
}

组合数

求组合数有很多种方式,需要根据数据范围来选择使用哪种方式。

递推

对于组合数 C a b C_a^b Cab 可以用一个递推式来表达,即 C a b = C a − 1 b + C a − 1 b − 1 C_a^b = C_{a-1}^b+C_{a-1}^{b-1} Cab=Ca1b+Ca1b1

所以每个组合数 C a b C_a^b Cab 都可以用更小的组合数来表示

这样我们就可以根据上面的递推式,预处理出数据范围内全部的组合数,查询时,直接查表即可。

针对递推式 C a b = C a − 1 b + C a − 1 b − 1 C_a^b = C_{a-1}^b+C_{a-1}^{b-1} Cab=Ca1b+Ca1b1 的一种通俗的解释:

考虑一个口袋里一共有 a a a 个苹果🍎,我们从中选 b b b 个苹果,则总共的选法就是 C a b C_a^b Cab,我们考虑将其中一个苹果打上标记(比如称其为金苹果)。那么总共的选法可以分为2大类:包含这个金苹果;不包含这个金苹果。

包含这个金苹果的选法,则是从余下的苹果中,再选 b − 1 b-1 b1 个,即 C a − 1 b − 1 C_{a-1}^{b-1} Ca1b1

不包含这个金苹果的选法,则是从余下的苹果中,再选 b b b 个,即 C a − 1 b C_{a-1}^{b} Ca1b

所以 C a b = C a − 1 b + C a − 1 b − 1 C_a^b = C_{a-1}^{b} + C_{a-1}^{b-1} Cab=Ca1b+Ca1b1

练习题:acwing - 885: 求组合数I

数据范围: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开始,需要算出 C 0 0 = 1 C_0^0=1 C00=1,而针对 C a b C_a^b Cab ,当 b > a b \gt a b>a 的情况,不用处理,因为数组初始化为0。比如

C 5 5 = C 4 5 + C 4 4 = 0 + 1 C_5^5 = C_4^5+C_4^4=0+1 C55=C45+C44=0+1

递推法的复杂度是 O ( n 2 ) O(n^2) O(n2),对于这道题的数据范围,计算量就是2000^2 = 4 × 10^6

预处理阶乘

组合数的公式为 C a b = a ! ( a − b ) ! × b ! C_a^b = \frac{a!}{(a-b)! × b!} Cab=(ab)!×b!a!

需要的都是某个数的阶乘,那么我们把所有数的阶乘预处理一下,阶乘数组用fact来表示,即fact[i] = i! mod 10^9 + 7

同时我们注意到,阶乘项也出现在分母中,而由于我们需要对结果取一个模,所以我们可以对i的阶乘,求一下乘法逆元。把除法转化为乘法。某个数的阶乘在模10^9 + 7下的乘法逆元,我们用infact[i]来表示。

于是用这两个预处理出的数组,就能在 O ( 1 ) O(1) O(1) 时间内求出任意的组合数。

由于模的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,根据裴蜀定理,这个二元线性方程一定有解,并且能用扩展欧几里得算法进行求解。

练习题:acwing - 886: 求组合数II

数据范围: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;
}

对于预处理时,求解factinfact数组时,

fact[i] = fact[i - 1] * i 这个很好理解,但是逆元数组 infact的递推怎么理解呢?

可以这样想,对于阶乘 a ! a! a! ,其逆元为 ( a ! ) − 1 (a!)^{-1} (a!)1,有 a ! × ( a ! ) − 1 ≡ 1 m o d    P a! \times (a!)^{-1} \equiv 1 \mod P a!×(a!)11modP

对于阶乘 ( a + 1 ) ! (a + 1)! (a+1)!,我们将其拆分为 a ! × ( a + 1 ) a! \times (a + 1) a!×(a+1),则有 a ! × ( a + 1 ) × ( a ! ) − 1 × x ≡ 1 m o d    P a! \times (a + 1) \times (a!)^{-1} \times x \equiv 1 \mod P a!×(a+1)×(a!)1×x1modP

( a + 1 ) ! (a + 1)! (a+1)! 的逆元为 ( a ! ) − 1 × x (a!)^{-1} \times x (a!)1×x,根据模运算的特性,我们将上式进行一下组合变换,有

  • a ! × ( a ! ) − 1 ≡ 1 m o d    P a! \times (a!)^{-1} \equiv 1 \mod P a!×(a!)11modP
  • ( a + 1 ) × x ≡ 1 m o d    P (a + 1) \times x \equiv 1 \mod P (a+1)×x1modP

那么很明显的, x = ( a + 1 ) − 1 x = (a + 1)^{-1} x=(a+1)1

也就是说,我们只需要求解一下 a + 1 a + 1 a+1的逆元

于是就能得到 infact数组的递推式: i n f a c t [ a + 1 ] = i n f a c t [ a ] × ( a + 1 ) − 1 infact[a + 1] = infact[a] \times (a+1)^{-1} infact[a+1]=infact[a]×a+11

预处理出2个阶乘数组,时间复杂度为 O ( n ) O(n) O(n),实际性能瓶颈在于计算 infact[i]时,需要使用快速幂计算 iMOD - 2次方,需要的复杂度是 O ( log ⁡ M O D ) O(\log MOD) O(logMOD),而本题的 MOD = 10^9 + 7log MOD大概等于30,所以实际上复杂度是 30n

注意,只有在模数 p p p 为质数,并且 a a a b b b 都小于 p p p 时,才能用预处理阶乘的方式求解。若 a > p a > p a>p,则可能 [1, a]中有的数和 p 不互质,不互质的话,就不存在乘法逆元。

卢卡斯定理

参考:

  • https://zhuanlan.zhihu.com/p/452976974

  • https://www.cnblogs.com/onlyblues/p/15339937.html

  • https://blog.csdn.net/qq_40679299/article/details/80489761

  • https://zhuanlan.zhihu.com/p/116698264

定义

对于非负整数 a a a b b b 和质数 p p p,有 C a b ≡ ∏ i = 0 k C a i b i   m o d    p C_a^b \equiv \prod_{i=0}^kC_{a_i}^{b_i} \space \mod p Cabi=0kCaibi modp,其中

a = a k p k + a k − 1 p k − 1 + . . . + a 1 p + a 0 a = a_kp^k+a_{k-1}p^{k-1} + ... + a_1p+a_0 a=akpk+ak1pk1+...+a1p+a0 b = b k p k + b k − 1 p k − 1 + . . . + b 1 p + b 0 b = b_kp^k+b_{k-1}p^{k-1}+...+b_1p+b_0 b=bkpk+bk1pk1+...+b1p+b0

一般,使用的是下面的式子

C a b ≡ C a m o d    p b m o d    p × C a / p b / p ( m o d    p ) C_a^b ≡ C_{a \mod p}^{b \mod p} × C_{a / p}^{b / p} (\mod p) CabCamodpbmodp×Ca/pb/p(modp),其中 p p p 为质数

证明

  • 引理1:对于组合数 C p i C_p^i Cpi,其中 p p p 为质数,始终满足 C p i ≡ 0 ( m o d    p )    ( 0 < i < p ) C_p^i \equiv 0 (\mod p) \space \space (0 < i < p) Cpi0(modp)  (0<i<p)

    这个引理很好证明,根据组合数公式,有 C p i = p × ( p − 1 ) × . . . × ( p − i + 1 ) i ! C_p^i = \frac{p \times(p-1) \times ... \times (p-i+1)}{i!} Cpi=i!p×(p1)×...×(pi+1),而 p p p 是质数,其只存在 1 1 1 p p p 两个因子,所以分子中的 p p p 一定不会被消去,故计算出的结果一定是 p p p 的某个倍数,故其在 m o d    p \mod p modp 下为 0 0 0

  • 引理2:对于整数 x x x 和质数 p p p,始终满足 ( 1 + x ) p ≡ ( 1 + x p ) m o d    p (1+x)^p \equiv (1 + x^p) \mod p (1+x)p(1+xp)modp

    ( 1 + x ) p (1+x)^p (1+x)p进行二项式展开,得到 C p 0 x 0 + C p 1 x + C p 2 x 2 + . . . + C p p x p C_p^0x^0 + C_p^1x + C_p^2x^2 + ... + C_p^px^p Cp0x0+Cp1x+Cp2x2+...+Cppxp,根据引理1,中间项在 m o d    p \mod p modp下都为0,可以消去,最后得到 1 + x p 1 + x^p 1+xp

  • Lucas定理

    我们先将 a a a b b b 转换为对应的 p p p 进制数,即

    • a = a k p k + a k − 1 p k − 1 + . . . + a 1 p + a 0 a = a_kp^k+a_{k-1}p^{k-1} + ... + a_1p+a_0 a=akpk+ak1pk1+...+a1p+a0

    • b = b k p k + b k − 1 p k − 1 + . . . + b 1 p + b 0 b = b_kp^k+b_{k-1}p^{k-1}+...+b_1p+b_0 b=bkpk+bk1pk1+...+b1p+b0

    接着,有

    KaTeX parse error: {align*} can be used only in display mode.

    ( 1 + x ) a = ( 1 + x p k ) a k ⋅ ( 1 + x p k − 1 ) a k − 1 ⋅ . . . ⋅ ( 1 + x ) a 0   m o d    p (1+x)^a = (1+x^{p^k})^{a_k} \cdot (1+x^{p^{k-1}})^{a_{k-1}} \cdot ... \cdot (1+x)^{a_0} \space \mod p (1+x)a=(1+xpk)ak(1+xpk1)ak1...(1+x)a0 modp

    根据二项式展开,容易知道 C a b C_a^b Cab 就是 ( 1 + x ) a (1+x)^a (1+x)a 展开式中的 x b x^b xb 的系数,而在上述等式右侧中,就是要知道 x b k p k + b k − 1 p k − 1 + . . . + b 0 x^{b_kp^k+b_{k-1}p^{k-1}+...+b_0} xbkpk+bk1pk1+...+b0 的系数。

    对于等式右侧的方程 ( 1 + x p k ) a k ⋅ ( 1 + x p k − 1 ) a k − 1 ⋅ . . . ⋅ ( 1 + x ) a 0 (1+x^{p^k})^{a_k} \cdot (1+x^{p^{k-1}})^{a_{k-1}} \cdot ... \cdot (1+x)^{a_0} (1+xpk)ak(1+xpk1)ak1...(1+x)a0

    • C a 0 b 0 C_{a_0}^{b_0} Ca0b0 即为 ( 1 + x ) a 0 (1+x)^{a_0} (1+x)a0 x b 0 x^{b_0} xb0 的系数
    • C a 1 b 1 C_{a_1}^{b_1} Ca1b1 即为 ( 1 + x p ) a 1 (1+x^p)^{a_1} (1+xp)a1 x b 1 p x^{b_1p} xb1p 的系数
    • C a k b k C_{a_k}^{b_k} Cakbk 即为 ( 1 + x p k ) a k (1+x^{p^k})^{a_k} (1+xpk)ak x b k p k x^{b_kp^k} xbkpk 的系数

    C a 0 b 0 x b 0 C_{a_0}^{b_0}x^{b_0} Ca0b0xb0 C a 1 b 1 x b 1 p C_{a_1}^{b_1}x^{b_1p} Ca1b1xb1p C a 2 b 2 x b 2 p 2 C_{a_2}^{b_2}x^{b_2p^2} Ca2b2xb2p2,…, C a k b k x b k p k C_{a_k}^{b_k}x^{b_kp^k} Cakbkxbkpk 乘起来,得到 C a 0 b 0 C a 1 b 1 . . . C a k b k ⋅ x b 0 x b 1 p . . . x b k p k = C a 0 b 0 C a 1 b 1 . . . C a k b k ⋅ x b C_{a_0}^{b_0}C_{a_1}^{b_1}...C_{a_k}^{b_k} \cdot x^{b_0}x^{b_1p}...x^{b_kp^k} = C_{a_0}^{b_0}C_{a_1}^{b_1}...C_{a_k}^{b_k} \cdot x^b Ca0b0Ca1b1...Cakbkxb0xb1p...xbkpk=Ca0b0Ca1b1...Cakbkxb

    所以右式中 x b x^b xb 的系数为 C a 0 b 0 C a 1 b 1 . . . C a k b k C_{a_0}^{b_0}C_{a_1}^{b_1}...C_{a_k}^{b_k} Ca0b0Ca1b1...Cakbk,所以有 C a b ≡ C a 0 b 0 C a 1 b 1 . . . C a k b k m o d    p C_a^b \equiv C_{a_0}^{b_0}C_{a_1}^{b_1}...C_{a_k}^{b_k} \mod p CabCa0b0Ca1b1...Cakbkmodp

    为了将 C a b ≡ C a 0 b 0 C a 1 b 1 . . . C a k b k m o d    p C_a^b \equiv C_{a_0}^{b_0}C_{a_1}^{b_1}...C_{a_k}^{b_k} \mod p CabCa0b0Ca1b1...Cakbkmodp 转化为这样的形式: C a b ≡ C a m o d    p b m o d    p × C a / p b / p   m o d    p C_a^b ≡ C_{a \mod p}^{b \mod p} × C_{a / p}^{b / p} \space \mod p CabCamodpbmodp×Ca/pb/p modp

    我们先把 C a 0 b 0 C a 1 b 1 . . . C a k b k C_{a_0}^{b_0}C_{a_1}^{b_1}...C_{a_k}^{b_k} Ca0b0Ca1b1...Cakbk 拆分成 C a 0 b 0 C_{a_0}^{b_0} Ca0b0 C a 1 b 1 . . . C a k b k C_{a_1}^{b_1}...C_{a_k}^{b_k} Ca1b1...Cakbk 两部分,首先对于 C a 0 b 0 C_{a_0}^{b_0} Ca0b0,它就等于 C a m o d    p b m o d    p C_{a \mod p}^{b \mod p} Camodpbmodp,因为我们将 a a a b b b 写成 p p p 进制表示时,最后的余数就是 a 0 a_0 a0 b 0 b_0 b0 。接着,我们只需要证明一下 C a 1 b 1 . . . C a k b k = C a / p b / p C_{a_1}^{b_1}...C_{a_k}^{b_k} = C_{a / p}^{b / p} Ca1b1...Cakbk=Ca/pb/p 即可。

    由于我们是先将 a a a b b b,分别写成了 p p p 进制表示,

    • a = a k p k + a k − 1 p k − 1 + . . . + a 1 p + a 0 a = a_kp^k+a_{k-1}p^{k-1} + ... + a_1p+a_0 a=akpk+ak1pk1+...+a1p+a0
    • b = b k p k + b k − 1 p k − 1 + . . . + b 1 p + b 0 b = b_kp^k+b_{k-1}p^{k-1}+...+b_1p+b_0 b=bkpk+bk1pk1+...+b1p+b0

    随后推出的 C a b ≡ C a 0 b 0 C a 1 b 1 . . . C a k b k m o d    p C_a^b \equiv C_{a_0}^{b_0}C_{a_1}^{b_1}...C_{a_k}^{b_k} \mod p CabCa0b0Ca1b1...Cakbkmodp

    现在,我们将 p p p 进制下的 a a a b b b 分别右移一位(相当于除以 p p p),得到的是

    • ⌊ a p ⌋ = a k p k − 1 + a k − 1 p k − 2 + . . . + a 1 \lfloor\frac{a}{p}\rfloor = a_kp^{k-1}+a_{k-1}p^{k-2} + ... + a_1 pa=akpk1+ak1pk2+...+a1
    • ⌊ b p ⌋ = b k p k − 1 + b k − 1 p k − 2 + . . . + b 1 \lfloor\frac{b}{p}\rfloor = b_kp^{k-1}+b_{k-1}p^{k-2}+...+b_1 pb=bkpk1+bk1pk2+...+b1

    我们只要将 ⌊ a p ⌋ \lfloor\frac{a}{p}\rfloor pa ⌊ b p ⌋ \lfloor\frac{b}{p}\rfloor pb 带入前面的公式,就能得到如下式子(下面用 a / p a/p a/p 替代 ⌊ a p ⌋ \lfloor\frac{a}{p}\rfloor pa b / p b/p b/p 同理 )

    • C a / p b / p = C a 1 b 1 C a 2 b 2 . . . C a k b k C_{a/p}^{b/p} = C_{a_1}^{b_1}C_{a_2}^{b_2}...C_{a_k}^{b_k} Ca/pb/p=Ca1b1Ca2b2...Cakbk

    于是,整个卢卡斯定理得证,对于非负整数 a a a b b b 以及质数 p p p ,有如下等式成立

    C a b ≡ C a 0 b 0 ⋅ C a 1 b 1 ⋅ . . . ⋅ C a k b k ≡ C a m o d    p b m o d    p ⋅ C a / p b / p    ( m o d    p ) C_a^b \equiv C_{a_0}^{b_0} \cdot C_{a_1}^{b_1} \cdot ... \cdot C_{a_k}^{b_k} \equiv C_{a \mod p}^{b \mod p} \cdot C_{a / p}^{b / p} \space \space (\mod p) CabCa0b0Ca1b1...CakbkCamodpbmodpCa/pb/p  (modp)

练习题:求组合数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 ,递归的层数就是 log ⁡ p a \log_pa logpa,每层中的复杂度就是计算 C(a % p, b % p) 的复杂度,如果是优化之前,计算 C(a, b)最多需要算 b 次乘法,每次还需要求解个逆元,复杂度应该是 b ⋅ log ⁡ 2 p b \cdot \log_2p blog2p,优化后,只需要在最后求解一次逆元,复杂度是 b + log ⁡ 2 p b + \log_2p b+log2p ,我们将 b b b 看成最大能取到 a a a,而 a a a 实际最大只能取到 p − 1 p - 1 p1,那么,优化前的复杂度是 O ( log ⁡ p N ⋅ p ⋅ log ⁡ 2 p ) O(\log_pN \cdot p \cdot \log_2p) O(logpNplog2p),优化后的复杂度是 O ( log ⁡ p N ⋅ ( p + l o g 2 p ) ) O(\log_pN \cdot (p + log_2p)) O(logpN(p+log2p)) ,实际上由于在 b > a - b时,做了 b = a - b的优化,所以b实际最多取到 a / 2,优化后实际的复杂度会更低,实际只需要 log ⁡ p N ⋅ ( p 2 + log ⁡ 2 p ) \log_pN \cdot (\frac{p}{2} + \log_2p) logpN(2p+log2p),其中的 N N N 表示 a a a 的取值,这道题目的数据范围, N N N 最大能取到 1 0 18 10^{18} 1018

卢卡斯定理,适用于 a 和 b 特别大,但是模数 p 是质数并且比较小的情况。

当 a 大于 p 时,不能依靠乘法逆元来进行计算,一是因为时间复杂度很高,二是因为 a 大于 p 时,[1, a]中间的一些数的乘法逆元不一定存在,因为某个数 x 在模 p 下的乘法逆元存在,一定要求 x 与 p 互质。

故需要使用卢卡斯定理。

高精度

前面几种求解组合数,都是求解在模某个数下的结果,而现在需要精确的求解出组合数的结果,这个结果可能很大。

求组合数IV

数据范围:1 <= b <= a <= 5000

思路:高精度,直接从公式出发。

C a b = a × ( a − 1 ) × ( a − 2 ) × . . . × ( a − b + 1 ) b × ( b − 1 ) × . . . × 1 = a ! b ! × ( a − b ) ! C_a^b = \frac{a × (a - 1) × (a - 2) × ... × (a - b + 1)}{b × (b - 1) × ... × 1} = \frac{a!}{b! × (a - b)!} Cab=b×(b1)×...×1a×(a1)×(a2)×...×(ab+1)=b!×(ab)!a!

可以实现一个高精度乘法,以及一个高精度除法。但是那样的计算效率比较低,也比较难以实现。所以可以换一种思路,先对 C a b C_a^b Cab 分解一下质因数,然后只需要实现一个高精度乘法即可。

问题变成了,如何对 C a b C_a^b Cab分解质因数呢?可以先对分子中的 a ! a! a! 分解一下质因数,看某个因子出现了多少次,再对分母中的 b ! b! b! ( a − b ) ! (a - b)! (ab)! 分解一下质因数,把对应的因子的次数减掉。

但是如果对分子分母中每个数依次分解质因数,那么时间复杂度是 O ( N ⋅ N ) O(N \cdot \sqrt{N}) O(NN ),这道题目的数据范围是 5000,这个复杂度下也是不会超时的,但效率较低(之后自己试了下,效率相差也没有很大),可以换另一种方法,我们可以先把 5000 以内的质数全部预处理出来。

然后对于如何求解 a ! a! a! 的某个质因子 p p p 的次数,可以用这个公式进行计算

⌊ a p ⌋ + ⌊ a p 2 ⌋ + ⌊ a p 3 ⌋ + . . . + . . . \lfloor\frac{a}{p}\rfloor + \lfloor\frac{a}{p^2}\rfloor + \lfloor\frac{a}{p^3}\rfloor + ... + ... pa+p2a+p3a+...+...

第一项是所有小于 a a a 的数中, p p p 的倍数的个数;但有的数可能是 p 2 p^2 p2 的倍数,但第一项只加了一次;所以我们需要再加上第二项 ,同理需要加上第三项,…,假设某个数分解质因数后,包含了 p k p^k pk ,那么该数对 p p p 的次数的贡献一共是 k k k,该数会被恰好加 k k k 次,在 ⌊ a p ⌋ \lfloor\frac{a}{p}\rfloor pa 时加1次,在 ⌊ a p 2 ⌋ \lfloor\frac{a}{p^2}\rfloor p2a 时加1次,…,在 ⌊ a p k ⌋ \lfloor\frac{a}{p^k}\rfloor pka 时加1次

所以我们的求解步骤是:

  1. 先用筛法求出1-5000内的全部质数
  2. 对于分子和分母的阶乘表示,求出每个质数的次数
  3. 用高精度乘法将每个质因子乘起来
// 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]的路径)

image-20230116160740306

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

image-20230116161514084

那么,现在问题等价于,求解所有从[0, 0]走到[6, 6],且不经过红色这条线的,所有路径的个数。

那么我们可以先求解所有路径的总数,然后再求解一下经过红色这条线的路径的个数,将两者相减,即得到答案。

对于上述 n = 6,总的路径数量就是 C 12 6 C_{12}^{6} C126 ,就是一共需要走12步,其中需要挑6步往上走。

那么经过红色这条线的路径的个数要怎么求呢?

对于任意一条经过红色这条线的路径,我们可以找到该路径与红线的第一个交点,然后把该点之后的剩余路径部分,关于红线做一个轴对称。

比如下面的橙线为一条经过红线的路径,我们将其与红线第一个交点之后的路径部分,关于红线做轴对称(加粗绿线),会发现其最终到达 [5, 7]这个点,这个点恰好和 [6, 6] 关于红线对称。

那么,任何一条经过红线的路径,我们都可以通过上述变换,将其转变为一条从[0, 0],走到[5, 7]的路径。所以,任何一条经过红线,从[0, 0]走到[6, 6]的路径,都能对应一条从[0, 0]走到[5, 7]的路径,反之同样成立。所以这两者是一一对应的,是可以等价的。

image-20230116162433781

所以,经过红线从[0, 0] 走到[6, 6]的路径的个数,就等于从[0, 0] 走到 [5, 7] 的路径个数,而后者的个数计算很简单,就是 C 12 5 C_{12}^5 C125,所以对于 n = 6,问题的答案就是 C 12 6 − C 12 5 C_{12}^6 - C_{12}^5 C126C125

对于一般的 n,问题的答案则是: C 2 n n − C 2 n n − 1 = C 2 n n n + 1 C_{2n}^n - C_{2n}^{n-1} = \frac{C_{2n}^{n}}{n + 1} C2nnC2nn1=n+1C2nn

而这个数就被称为卡特兰数

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/170771.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人机界面石油行业应用:一个设备构建石油罐区状态监测系统

一、应用背景 石油罐区是石油石化企业重要的生产设施&#xff0c;负责存储和输送各类油品&#xff0c;而石油罐区状态参数的监控是生产管理的重要部分&#xff0c;不仅可以及时准确地获取现场设备数据&#xff0c;保证罐区的正常运行&#xff0c;还可以防止安全事故的发生。 …

Python如何解决“快手滑块验证码”(4)

前言 本文是该专栏的第32篇,后面会持续分享python的干货知识,记得关注。 很多时候,我们打开一个页面还没开始进行浏览,就跳出一个滑块验证的图片,需要拖到滑块至缺口处,才可以正常浏览。这对于我们正常人浏览页面来说,几乎没什么难度,但是当我们需要用到脚本去实现的时…

本地服务器如何让外网远程桌面连接?

远程访问是远程办公和服务器管理常用的网络应用场景。那么&#xff0c;当我们需要面对远程目标主机是内网服务器电脑时&#xff0c;在不是同个局域网的跨网环境下&#xff0c;内网可以远程控制电脑吗&#xff1f;答案是可以&#xff0c;使用快解析内网映射方案就能实现将本地服…

MyBatis一级缓存 二级缓存

MyBatis一级缓存 二级缓存什么是缓存?一级缓存一级缓存失效的四种情况二级缓存怎样开启二级缓存使二级缓存失效的情况二级缓存相关配置缓存查询的数据顺序整合第三方缓存EHCache&#xff08;代替二级缓存&#xff09;什么是缓存? 这是一个地图软件上的根据城市查询模块,对于那…

Freemarker页面静态化开发

4.5 页面静态化 4.5.1 什么是页面静态化 根据课程发布的操作流程&#xff0c;执行课程发布后要将课程详情信息页面静态化&#xff0c;生成html页面上传至文件系统。 什么是页面静态化&#xff1f; 课程预览功能通过模板引擎技术在页面模板中填充数据&#xff0c;生成html页…

Mybatis Plus轻松上手

Mybatis Plus 今日目标&#xff1a; 了解mybatisplus的特点能够掌握mybatisplus快速入门能够掌握mybatisplus常用注解能够掌握mybatisplus常用的增删改查能够掌握mybatisplus自动代码生成 Mybatis: ✔需要程序员编写sql语句程序员可以干预sql对sql进行调优(优化) MybatisPlu…

强化学习笔记:基于价值的学习方法之价值估计(python实现)

目录 1. 前言 2. 数学原理 3. 代码实现 3.1 游戏设定 3.2 class State 3.3 class Action 3.4 Class Agent 3.5 Class Environment 4. 仿真结果及其分析 4.1 play() 4.2 value_evaluation_all_states(grid, max_steps) 4.3 value_evaluation_one_state(grid, s) 4.4…

ZYNQ FPGA嵌入式开发 - 小梅哥(二)

创建工程打开Xilinx SDK创建工程Next 创建Empty Application添加文件编写代码参考文档 UG585 Zynq 7000 Technical Reference Manual寄存器说明 Appx.B: Registe Detial查看帮助文档Import Examples跨平台使用&#xff1a;头文件&#xff1a;unistd.h 每个平台都会提供sleep() …

论文阅读笔记:Attention is All You Need

论文标题&#xff1a;Attention is All You Need 目录 论文标题&#xff1a;Attention is All You Need 1.摘要 2.前言 3.模型结构 自注意力机制 多头自注意力机制 注意力机制在Transformer中的应用 1.摘要 过去最优的模型是带有attention连接的encoder-decoder模型&…

string的应用和模拟实现(上)

目录 string的应用 insert插入元素 erase删除元素 assign赋值&#xff1a; replace代替函数的一部分 find&#xff1a;从string对象中找元素 c_str:得到c类型的字符串的指针 substr&#xff1a;取部分元素构建成新的string对象 rfind find_first_of:从string查找元素 stri…

JVM【类的加载过程(类的生命周期)详解】

概述 在 Java 中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义&#xff0c;引用数据类型则需要进行类的加载。 按照 Java 虚拟机规范&#xff0c;从 class 文件到加载到内存中的类&#xff0c;到类卸载出内存为止&#xff0c;它的整个生命周期包括如…

软件测试之python学习

1、pycharm的常用配置 1.1修改主题配置 1、点击菜单file,选择settings选项2、选择editor&#xff0c;点击color scheme配色方案3、在右侧选择对应的主题配置1.2修改背景颜色 1、点击菜单file,选择settings选项2、选择appearance&#xff0c;点击Theme 1.3调整字体大小 1、点…

基于K8S+eureka的java应用快速上下线的WEB平台

刚进公司时&#xff0c;由于历史原因&#xff0c;应用发布通过&#xff1a;发布新版&#xff08;新老并存&#xff09;->下线老版->删除老版的方式&#xff0c;每次通过手工处理&#xff0c;蛋疼&#xff08;不方便且高风险&#xff09;。于是马上写了比较直观的脚本方案…

关于java移位运算的一点讨论

框架乱飞的年代&#xff0c;时常还得往框架源码里看&#xff0c;对内在原理没点理解&#xff0c;人家就会认为你不太行。平时开发你可能没咋用过位移运算&#xff0c;但往源码里一看&#xff0c;就时常能看到它。我也是看着看着&#xff0c;突然仔细一琢磨&#xff0c;又不由得…

C++缺省参数与函数重载

目录 一.缺省参数 1. 基本概念 2.多参函数中使用缺省参数的情形分类 二.函数重载 (1)形参类型不同构成的重载 (2)形参个数不同构成的重载 (3)形参类型顺序不同构成的重载 函数重载的注意事项&#xff1a; 三.C支持函数重载的底层原理--函数名修饰 编译器生成可执行程序…

选购自主可控全国产交换机时, IP防护等级多少比较合适?

本期武汉海翎光电的小编要为大家介绍的是《选购自主可控全国产交换机时IP防护等级多少比较合适&#xff1f;》首先我们要了解自主可控全国产交换机的工作场景&#xff0c;加固交换机会比工业交换机的IP等级更高一些&#xff0c;而工业交换机又会比普通交换机的IP等级要求高一些…

Unity 工具 之 Jenkins 打包自动化工具的下载/安装/基本操作/任务创建执行/Unity打包自动化简单搭建的相关整理

Unity 工具 之 Jenkins 打包自动化工具的下载/安装/基本操作/任务创建执行/Unity打包自动化简单搭建的相关整理 目录 Unity 工具 之 Jenkins 打包自动化工具的下载/安装/基本操作/任务创建执行/Unity打包自动化简单搭建的相关整理 一、简单介绍 二、Jenkins 的下载 三、Jenk…

代码随想录--链表相关题目整理

代码随想录–链表相关题目整理 1. LeetCode203 移除链表中指定元素 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val…

如何免费创建PDF文档?创建PDF文档的9个工具

PDF 创建器是一种程序、应用程序或软件&#xff0c;旨在制作或创建 PDF 文档。自可移植文档格式 ( PDF ) 出现以来&#xff0c;文档共享和存储变得更加容易。PDF 还使文件交换更加安全。由于 PDF 格式的众多优点&#xff0c;PDF 文档被全球范围内的人们广泛使用。因此&#xff…

Java数据结构(List介绍和顺序表)

1、List的介绍 在集合框架中&#xff0c;List是一个接口&#xff0c;继承自Collection&#xff08;也是一个接口&#xff09;。 Collection也是一个接口&#xff0c;该接口中规范了后序容器中常用的一些方法&#xff0c;Iterable也是一个接口&#xff0c;表示实现该接口的类是可…