😀大家好,我是白晨,一个不是很能熬夜😫,但是也想日更的人✈。如果喜欢这篇文章,点个赞👍,关注一下👀白晨吧!你的支持就是我最大的动力!💪💪💪
文章目录
- 前言
- 蓝桥杯复习(四)
- 一、区间DP
- 复习
- 练习:游戏
- 二、树形DP
- 复习
- 练习:病毒溯源
- 三、快速幂
- 复习
- 练习:转圈游戏
- 四、最大公约数
- 复习
- 练习:公约数
- 五、分解质因数
- 复习
- 练习:约数的个数
- 六、矩阵乘法
- 复习
- 练习:斐波那契
- 七、组合计数
- 复习
- 练习:计算系数
- 总结
前言
本文适合有一定算法基础,但是由于各种原因已经很久没有敲代码的同学。本文以复习+练习为主,旨在通过练习算法题快速复习已经遗忘的算法。即使不是参加蓝桥杯的同学,如果符合上面的条件,依然可以参考本文进行复习。
如果你是新手,可以参考白晨的算法专栏进行学习。
如果你想系统性进行复习,可关注白晨的蓝桥杯专栏进行学习。
蓝桥杯复习(四)
一、区间DP
复习
区间动态规划(Interval Dynamic Programming,简称区间DP)是一种动态规划算法,通常用于解决涉及区间范围的优化问题。这类问题通常涉及对给定的区间进行操作,以达到某种特定的目标,例如最大化区间内的价值、最小化区间内的代价等。
区间DP的基本思想是将原始区间划分为更小的子区间,并逐步构建出整个区间的最优解。通常,这种方法需要定义一个状态表示当前处理的区间,然后设计状态转移方程来描述如何从较小的区间状态转移到较大的区间状态。
以下是一个简单的区间DP问题示例:
假设给定一个长度为n的数组a[1…n],每个位置i上有一个值a[i]。现在要选取一个连续的子区间[a, b](1 ≤ a ≤ b ≤ n),使得这个子区间的和最大。
解决这个问题的一个典型的区间DP算法如下:
- 定义状态:设dp[i]表示以第i个元素结尾的子区间的最大和。
- 初始化:dp[1] = a[1]。
- 状态转移方程:dp[i] = max(dp[i-1] + a[i], a[i])。
- 最终答案:max(dp[1], dp[2], …, dp[n])。
这里的状态转移方程表示,以第i个元素结尾的子区间的最大和要么是当前元素自成一个区间,要么是以第i-1个元素结尾的子区间的最大和加上当前元素的值。最终的答案就是所有以不同元素结尾的子区间中最大的和。
通过动态规划的方式,可以高效地解决这类区间优化问题,而不需要穷举所有可能的子区间。
- 区间DP模板
// 先枚举区间长度
for (int len = 1; len <= n; ++len)
// 再枚举左端点
for (int i = 0; i + len - 1 < n; ++i)
{
int j = i + len - 1;
// 状态转移
}
练习:游戏
🍬题目链接
:游戏
🍎算法思想
:
-
状态定义:在拿下标为[l, r]区间中的分时,
(当前玩家拿的分 - 下一个玩家拿的分)
的最大值 -
状态划分:由于总分数是一定的,所以一个人分高,另一个人就分少,所以将可以将问题转化为两个玩家分差的最大值,只要求出两个玩家分差最大值,再求出总分数,就能求出两个玩家各自的分。
所以f(i, j)可以划分为:
- 如果取左边的数w[i],下一名玩家就要在[i + 1, j]中取,下一名玩家在[i + 1, j]拿的分的差值最大为f(i + 1, j),所以f(i, j) = max(f(i, j), w[i] - f(i + 1, j));
- 同理,如果取左边的数w[j],下一名玩家就要在[i, j - 1]中取,下一名玩家在[i, j - 1]拿的分的差值最大为f(i, j - 1),所以f(i, j) = max(f(i, j), w[i] - f(i, j - 1));
-
状态转移: f ( i , j ) = m a x ( w [ i ] − f [ i + 1 ] [ r ] , w [ j ] − f [ i ] [ j − 1 ] ) f(i, j) = max(w[i] - f[i + 1][r], w[j] - f[i][j - 1]) f(i,j)=max(w[i]−f[i+1][r],w[j]−f[i][j−1])
🍊具体实现
:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int n;
int w[N];
int f[N][N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &w[i]);
for (int len = 1; len <= n; ++len)
for (int i = 0; i + len - 1 < n; ++i)
{
int j = i + len - 1;
f[i][j] = max(w[i] - f[i + 1][j], w[j] - f[i][j - 1]);
}
int sum = 0, d = f[0][n - 1];
for (int i = 0; i < n; ++i) sum += w[i];
printf("%d %d", (sum + d) / 2, (sum - d) / 2);
return 0;
}
二、树形DP
复习
树形动态规划(Tree Dynamic Programming,简称树形DP)是一种应用动态规划思想解决树结构上的问题的方法。在树形DP中,通常需要在树上进行递归或者动态规划的遍历,以解决与节点相关的问题,比如最长路径、最大权值路径等等。
树形DP的基本思想是从树的叶子节点开始,逐步向上计算每个节点的状态,并在计算过程中利用子树的信息来更新父节点的状态。通常,树形DP需要设计适合问题特点的状态表示和状态转移方程。
以下是一个简单的树形DP问题示例:
假设给定一棵树,每个节点有一个权值。现在要找到树上的一条路径,使得路径上节点的权值之和最大。
解决这个问题的一个典型的树形DP算法如下:
- 定义状态:设dp[u]表示以节点u为根的子树中的最大路径权值和。
- 递归计算:从叶子节点开始向上递归计算每个节点的dp值。
- 状态转移方程:对于每个节点u,设它的子节点为v1, v2, …, vk,则dp[u] = max(0, dp[v1] + weight[u], dp[v2] + weight[u], …, dp[vk] + weight[u]),其中weight[u]表示节点u的权值。
- 最终答案:树中所有节点的dp值中的最大值即为所求的最大路径权值和。
通过树形DP,可以高效地解决诸如树上最大路径和、树上最长路径等问题,其时间复杂度通常为树的节点数量的线性或者近似线性复杂度。
- 树形DP模板
const int N = 10010;
int n;
int h[N], e[N], ne[N], idx;
bool has_father[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int main()
{
memset(h, -1, sizeof(h));
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
has_father[b] = true;
}
int root = 0;
while (has_father[root]) root++;
// 从根开始进行一些dp操作
// ...
return 0;
}
练习:病毒溯源
🍬题目链接
:病毒溯源
🍎算法思想
:
-
状态定义: 在这个问题中,我们的目标是求解树中的最长路径。因此,我们可以定义状态
dp[u]
表示以节点u
为根的子树中的最长路径长度。这里u
是树中的一个节点。 -
状态划分: 在状态划分中,我们需要考虑如何利用子问题的结果来构建更大规模的问题的解。在这个问题中,我们可以考虑划分每个节点的子树作为不同的子问题。
-
状态转移: d p [ u ] = m a x ( d p [ s o n 1 ] , d p [ s o n 2 ] , . . . , d p [ s o n k ] ) + 1 dp[u] = max(dp[son1], dp[son2], ..., dp[sonk]) + 1 dp[u]=max(dp[son1],dp[son2],...,dp[sonk])+1,递归计算u节点每一个子树的最大高度,最后选择最高的子树的高度+1就是以u为根的树的最大高度。
🍊具体实现
:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
int n;
int h[N], e[N], ne[N], idx;
int son[N];
bool has_father[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int dfs(int u)
{
int res = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int t = e[i];
int ret = dfs(t);
if (ret > res) res = ret, son[u] = t;
else if (ret == res) son[u] = min(son[u], t);
}
return res + 1;
}
int main()
{
memset(h, -1, sizeof(h));
memset(son, -1, sizeof(son));
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
int len;
scanf("%d", &len);
while (len--) {
int x;
scanf("%d", &x);
add(i, x);
has_father[x] = true;
}
}
int root = 0;
while (has_father[root]) root++;
printf("%d\n", dfs(root));
printf("%d ", root);
for (int i = son[root]; ~i; i = son[i]) {
printf("%d ", i);
}
return 0;
}
三、快速幂
复习
快速幂算法(也称为快速幂运算或指数运算)是一种用于快速计算一个数的整数次幂的算法。该算法基于分治策略,通过将幂指数不断折半来减少计算次数,从而提高了计算效率。
下面是快速幂算法的基本思想:
- 将幂指数表示为二进制形式。
- 从最低位开始,对于每一位:
- 如果该位为 1,则将结果乘以底数。
- 将底数平方。
- 继续处理下一位,直到处理完所有位。
// a为底数,k为幂数,p为模数
int quick_power(int a, int k, int p)
{
int res = 1 % p;
while (k) {
if (k & 1) res = ((long long)res * a) % p;
a = (long long)a * a % p;
k >>= 1;
}
return res;
}
练习:转圈游戏
🍬题目链接
:转圈游戏
🍎算法思想
:
非常基础的快速幂算法的应用,先将轮数用快速幂算法计算出来,再乘以m,就是每个人移动的距离,再加上x的下标,算出x位置最后移动的总距离,最后模上n,就求出了x当前的位置编号。
🍊具体实现
:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int n, m, k, x;
int quick_power(int a, int k, int p)
{
int res = 1 % p;
while (k) {
if (k & 1) res = ((long long)res * a) % p;
a = (long long)a * a % p;
k >>= 1;
}
return res;
}
int main()
{
cin >> n >> m >> k >> x;
printf("%d", (x + quick_power(10, k, n) * (long long)m) % n);
return 0;
}
四、最大公约数
复习
- 最大公约数模板
int gcd(int a, int b)
{
return b ? gcd(b, a % b): a;
}
- 试除法求约数
vector<int> get_divisions(int x)
{
vector<int> res;
for (int i = 1; i <= x / i; ++i)
{
if (x % i == 0)
{
res.push_back(i);
if (x / i != i) res.push_back(x / i);
}
}
return res;
}
- 二分模板
// 模板一
// 求满足check条件的最左下标
template<class T>
int binary_search1(T* v, int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(v[mid])) // check中 v[mid] 永远放在前面,eg. v[mid] >= a
r = mid;
else
l = mid + 1;
}
return mid;
}
// 模板二
// 求满足check条件的最右下标
template<class T>
int binary_search1(T* v, int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1; // 必须加一,避免死循环
if (check(v[mid])) // eg.v[mid] <= a
l = mid;
else
r = mid - 1;
}
return mid;
}
练习:公约数
🍬题目链接
:公约数
🍎算法思想
:
本题考查最大公约数、试除法求约数以及二分算法,很好地将三种基础算法综合了起来,很考察基本功。
这道题要推理一下,本题所要求的x
一定是a、b
最大公约数cd
的约数,可以反证一下,如果x
不为cd
的约数,那么由算数定理可得,x
一定有一个cd
没有的质因子或者有一个质因子的幂数比cd
的大,由于cd
是a、b
的最大公约数,所以cd
的质因子一定是a、b
中出现的,质因子的幂数一定是a、b
相同质因子幂数的最小值。那么,x
如果有一个cd
没有的质因子或者有一个质因子的幂数比cd
的大,说明x
不能整除a、b
。证毕。
实现分为三步:
- 计算最大公约数(gcd):首先,使用辗转相除法计算给定两个整数
a
和b
的最大公约数。 - 获取最大公约数的所有正整数因子:定义一个函数
get_divisions(int x)
,用于获取给定整数x
的所有正整数因子。这个函数通过迭代从 1 到sqrt(x)
(平方根)进行检查,如果x
能够被某个数整除,那么该数就是x
的一个因子。这些因子被存储在一个向量中,并按升序排序,以便后续的查询步骤更有效。 - 处理查询:对于每个查询,程序读取一对整数
L
和R
,表示当前查询的范围。然后,使用二分查找在已经计算好的最大公约数的因子中找到范围[L, R]
内的最大因子。二分查找是一种高效的查找方法,通过将搜索范围逐步缩小至单个元素来找到目标值或确定目标值不存在。
🍊具体实现
:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
int a, b;
int q;
int gcd(int a, int b)
{
return b ? gcd(b, a % b): a;
}
vector<int> get_divisions(int x)
{
vector<int> res;
for (int i = 1; i <= x / i; ++i)
{
if (x % i == 0)
{
res.push_back(i);
if (x / i != i) res.push_back(x / i);
}
}
// 将约数升序排列
sort(res.begin(), res.end());
return res;
}
int main()
{
scanf("%d%d%d", &a, &b, &q);
int cd = gcd(a, b);
vector<int> v = get_divisions(cd);
while (q--)
{
int L, R;
scanf("%d%d", &L, &R);
int l = 0, r = v.size() - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (v[mid] <= R) l = mid;
else r = mid - 1;
}
if (v[r] <= R && v[r] >= L) printf("%d\n", v[r]);
else puts("-1");
}
return 0;
}
五、分解质因数
复习
-
质因数公式: x = p 1 n 1 + p 2 n 2 + . . . + p k n k x = p_1^{n_1} + p_2^{n_2} + ... + p_k^{n_k} x=p1n1+p2n2+...+pknk
-
约数个数: c n t = ( n 1 + 1 ) ∗ ( n 2 + 1 ) ∗ . . . ∗ ( n k + 1 ) cnt = (n_1 + 1)*(n_2+1)*...*(n_k+1) cnt=(n1+1)∗(n2+1)∗...∗(nk+1)
-
约数之和: s u m = ( p 1 0 + p 1 1 + . . . + p 1 n 1 ) ( p 2 0 + p 2 1 + . . . + p 2 n 2 ) . . . ( p k 0 + . . . + p k n k ) sum = (p_1^0 + p_1^1 + ... + p_1^{n_1})(p_2^0 + p_2^1 + ... + p_2^{n_2})...(p_k^0 + ... + p_k^{n_k}) sum=(p10+p11+...+p1n1)(p20+p21+...+p2n2)...(pk0+...+pknk)
求质因数模板
:
unordered_map<int, int> m;
void get_divisors(int x)
{
for (int i = 2; i <= x / i; ++i)
{
if (x % i == 0)
{
int cnt = 0;
while (x % i == 0)
{
x /= i;
cnt++;
}
m[i] += cnt;
}
}
if (x > 1) m[x] += 1;
}
练习:约数的个数
🍬题目链接
:约数的个数
🍎算法思想
:
-
质因数公式: x = p 1 n 1 + p 2 n 2 + . . . + p k n k x = p_1^{n_1} + p_2^{n_2} + ... + p_k^{n_k} x=p1n1+p2n2+...+pknk
-
约数个数: c n t = ( n 1 + 1 ) ∗ ( n 2 + 1 ) ∗ . . . ∗ ( n k + 1 ) cnt = (n_1 + 1)*(n_2+1)*...*(n_k+1) cnt=(n1+1)∗(n2+1)∗...∗(nk+1)
-
约数之和: s u m = ( p 1 0 + p 1 1 + . . . + p 1 n 1 ) ( p 2 0 + p 2 1 + . . . + p 2 n 2 ) . . . ( p k 0 + . . . + p k n k ) sum = (p_1^0 + p_1^1 + ... + p_1^{n_1})(p_2^0 + p_2^1 + ... + p_2^{n_2})...(p_k^0 + ... + p_k^{n_k}) sum=(p10+p11+...+p1n1)(p20+p21+...+p2n2)...(pk0+...+pknk)
直接分解质因子,然后求约数个数即可。
🍊具体实现
:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>
using namespace std;
int n;
unordered_map<int, int> um;
void get_divisors(int x)
{
for (int i = 2; i <= x / i; ++i)
{
if (x % i == 0)
{
int cnt = 0;
while (x % i == 0)
{
cnt++;
x /= i;
}
um[i] += cnt;
}
}
if (x > 1) um[x] += 1;
}
int main()
{
scanf("%d", &n);
while (n--)
{
um.clear();
int x;
scanf("%d", &x);
get_divisors(x);
int res = 1;
for (auto e : um)
{
res *= e.second + 1;
}
printf("%d\n", res);
}
return 0;
}
六、矩阵乘法
复习
当我们进行矩阵乘法时,我们需要计算两个矩阵的乘积。假设我们有两个矩阵 A A A 和 B B B,它们的尺寸分别为 m × n m \times n m×n 和 n × p n \times p n×p,结果矩阵 C C C 的尺寸为 m × p m \times p m×p。矩阵乘法的公式如下所示:
C i j = ∑ k = 1 n A i k ⋅ B k j C_{ij} = \sum_{k=1}^{n} A_{ik} \cdot B_{kj} Cij=k=1∑nAik⋅Bkj
其中, C i j C_{ij} Cij 是结果矩阵 C C C 的第 i i i 行第 j j j 列的元素, A i k A_{ik} Aik 是矩阵 A A A 的第 i i i 行第 k k k 列的元素, B k j B_{kj} Bkj 是矩阵 B B B 的第 k k k 行第 j j j 列的元素。
在代码中,我们有两个 2 × 2 2 \times 2 2×2 的矩阵 A A A 和 B B B,它们的乘积 C C C 的计算如下:
C i j = A i 1 ⋅ B 1 j + A i 2 ⋅ B 2 j C_{ij} = A_{i1} \cdot B_{1j} + A_{i2} \cdot B_{2j} Cij=Ai1⋅B1j+Ai2⋅B2j
对于一个 2 × 2 2 \times 2 2×2 的矩阵, C C C 的计算如下所示:
C = ( c 11 c 12 c 21 c 22 ) = ( a 11 ⋅ b 11 + a 12 ⋅ b 21 a 11 ⋅ b 12 + a 12 ⋅ b 22 a 21 ⋅ b 11 + a 22 ⋅ b 21 a 21 ⋅ b 12 + a 22 ⋅ b 22 ) C = \begin{pmatrix}c_{11} & c_{12} \\c_{21} & c_{22}\end{pmatrix}= \begin{pmatrix} a_{11} \cdot b_{11} + a_{12} \cdot b_{21} & a_{11} \cdot b_{12} + a_{12} \cdot b_{22} \\ a_{21} \cdot b_{11} + a_{22} \cdot b_{21} & a_{21} \cdot b_{12} + a_{22} \cdot b_{22} \end{pmatrix} C=(c11c21c12c22)=(a11⋅b11+a12⋅b21a21⋅b11+a22⋅b21a11⋅b12+a12⋅b22a21⋅b12+a22⋅b22)
在代码中,我们通过三重循环来计算每个 C i j C_{ij} Cij 的值,即 c i j c_{ij} cij。
模板如下:
void mul(int a[][K], int b[][m])
{
int c[N][M] = {0};
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
for (int k = 0; k < K; ++k)
c[i][j] = (c[i][j] + a[i][k] * b[k][j]) % mod;
memcpy(a, c, sizeof c);
}
练习:斐波那契
🍬题目链接
:斐波那契
🍎算法思想
:
直接使用斐波那契数列递推的一次计算量可能为 2 ∗ 1 0 9 2*10^9 2∗109,100个测试数据就为 2 ∗ 1 0 11 2*10^{11} 2∗1011,C++每秒最多 1 0 9 10^9 109计算量,所以不能直接使用递推进行计算。
斐波那契数列可以使用矩阵乘法来进行迭代计算。设矩阵 A ( 0 ) A(0) A(0) 表示斐波那契数列的前两项:
A ( 0 ) = ( 0 1 ) A(0) = \begin{pmatrix} 0 & 1 \end{pmatrix} A(0)=(01)
我们知道斐波那契数列的递推关系式为 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1) + f(n-2) f(n)=f(n−1)+f(n−2),可以表示为矩阵形式:
(
f
(
n
)
f
(
n
+
1
)
)
=
(
f
(
n
−
1
)
f
(
n
)
)
(
0
1
1
1
)
\begin{pmatrix} f(n) & f(n+1) \end{pmatrix}= \begin{pmatrix} f(n-1) & f(n) \end{pmatrix} \begin{pmatrix} 0 & 1 \\ 1 & 1 \end{pmatrix}
(f(n)f(n+1))=(f(n−1)f(n))(0111)
因此,我们可以得到如下的矩阵迭代公式:
(
f
(
n
)
f
(
n
+
1
)
)
=
(
f
(
n
−
1
)
f
(
n
)
)
(
0
1
1
1
)
=
A
(
0
)
(
0
1
1
1
)
n
=
(
0
1
)
(
0
1
1
1
)
n
\begin{pmatrix} f(n) & f(n+1) \end{pmatrix}= \begin{pmatrix} f(n - 1) & f(n) \end{pmatrix} \begin{pmatrix} 0 & 1 \\ 1 & 1\end{pmatrix}= A(0) \begin{pmatrix} 0 & 1 \\ 1 & 1\end{pmatrix}^n= \begin{pmatrix} 0 & 1 \end{pmatrix} \begin{pmatrix} 0 & 1 \\ 1 & 1 \end{pmatrix}^n
(f(n)f(n+1))=(f(n−1)f(n))(0111)=A(0)(0111)n=(01)(0111)n
这个公式表示,要得到斐波那契数列的第
n
n
n 项,只需将初始矩阵
A
(
0
)
A(0)
A(0) 与矩阵
(
0
1
1
1
)
\begin{pmatrix} 0 & 1 \\ 1 & 1 \end{pmatrix}
(0111)相乘
n
n
n 次,最终得到的结果的第一个元素即为第n个斐波那契数列元素。
满足结合律的矩阵乘法可以使用快速幂思想快速求矩阵乘积 ( 0 1 1 1 ) n \begin{pmatrix} 0 & 1 \\ 1 & 1 \end{pmatrix}^n (0111)n,所以可以以时间复杂度为 8 l o g n 8logn 8logn的时间复杂度求出结果,本题目使用此方法大约计算量为5w次计算量,随便就能通过。
🍊具体实现
:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int mod = 10000;
// 矩阵乘法函数
void mul(int a[][2], int b[][2])
{
int c[2][2] = {0}; // 用于存储乘积结果的临时矩阵
// 矩阵乘法:a * b = c
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j)
for (int k = 0; k < 2; ++k)
c[i][j] = (c[i][j] + a[i][k] * b[k][j]) % mod; // 求和并取模
memcpy(a, c, sizeof c); // 将乘积结果拷贝回矩阵 a
}
// 计算斐波那契数列的第 n 项
int fib(int n)
{
int a[2][2] = {0, 1, 0, 0}; // 初始矩阵 [F(1), F(0)]
int f[2][2] = {0, 1, 1, 1}; // 递推矩阵 [F(n), F(n-1)]
// 使用快速幂加速矩阵乘法
while (n)
{
if (n & 1) mul(a, f); // 如果 n 是奇数,将 a 与 f 相乘
mul(f, f); // 将 f 自乘,相当于 f^2
n >>= 1; // 右移一位,相当于 n/2
}
return a[0][0]; // 返回斐波那契数列的第 n 项
}
int main()
{
int n;
// 循环读入 n,直到输入为 -1
while (cin >> n, n != -1)
cout << fib(n) << endl; // 输出斐波那契数列的第 n 项
return 0;
}
七、组合计数
复习
- 求组合数I :高查询,小范围(底数为 1 0 3 10^3 103级别),直接用 C [ a ] [ b ] = C [ a − 1 ] [ b ] + C [ a − 1 ] [ b − 1 ] C[a][b] = C[a - 1][b] + C[a - 1][b - 1] C[a][b]=C[a−1][b]+C[a−1][b−1]递推,预处理出全部结果,时间复杂度O(n^2)
const int N = 2010, mod = 1e9 + 7;
int C[N][N];
void init()
{
for (int i = 0; i <= 2000; ++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;
}
int main()
{
int n;
scanf("%d", &n);
init();
while (n--)
{
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", C[a][b]);
}
return 0;
}
- 求组合数II:中数据范围(底数为 1 0 5 10^5 105级别),较高访问,取模的数为质数,预处理出所有需要用的阶乘及其逆元,按照定义求解即可
typedef long long LL;
const int N = 100010, mod = 1e9 + 7;
int fact[N], infact[N];
int qmi(int a, int k)
{
int ret = 1;
while (k)
{
if (k & 1) ret = (LL)ret * a % mod;
a = (LL)a * a % mod;
k >>= 1;
}
return ret;
}
int main()
{
int n;
scanf("%d", &n);
fact[0] = 1;
infact[0] = 1;
for (int i = 1; i <= 100000; ++i)
{
fact[i] = (LL)fact[i - 1] * i % mod;
infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2) % mod;
}
while (n--)
{
int a, b;
scanf("%d%d", &a, &b);
int res = (LL)fact[a] * infact[b] % mod * infact[a - b] % mod;
printf("%d\n", res);
}
return 0;
}
- 求组合数III:大数据范围(底数为 1 0 18 10^{18} 1018级别),低访问,模的数为质数且数据较小,利用卢卡斯定理进行递推 C [ a ] [ b ] = C [ a % p ] [ b % p ] ∗ C [ a / p ] [ b / p ] % p C[a][b] = C[a \% p][b \% p] * C[a / p][b / p] \% p C[a][b]=C[a%p][b%p]∗C[a/p][b/p]%p
typedef long long LL;
int p;
int qmi(int a, int k)
{
int ret = 1;
while (k)
{
if (k & 1) ret = (LL)ret * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return ret;
}
int C(int a, int b)
{
if (b > a) return 0;
int res = 1;
for (int i = 1, j = a; i <= b; ++i, --j)
{
res = (LL)res * j % p;
res = (LL)res * qmi(i, p - 2) % p;
}
return res;
}
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;
}
int main()
{
int n;
cin >> n;
while (n--)
{
LL a, b;
cin >> a >> b >> p;
cout << lucas(a, b) << endl;
}
return 0;
}
- 求组和数IV:较小数据(底数为 1 0 3 10^3 103级别),但是没有取模,结果数字很大,需要高精度算法
// 求组和数IV:较小数据,但是没有取模,结果数字很大,需要高精度算法
// 最简单的想法:实现高精度乘法和除法,按照定义进行组合数计算即可。
// 但是,实现两种高精度算法比较麻烦,我们可以将定义式分子分母分解质因数,将上下分子分母的质因数约去,只用求分子剩下的质因数的乘积
// a!中质因数b的个数为 a/b + a/(b^2) + a/(b^3) + a/(b^4) + ......
// 由于分子和分母本来乘数个数就相等,再加上分子的乘数较大,分子的质因数是完全包含分母的质因数的,具体证明这里略
const int N = 5010;
int primes[N], cnt;
bool book[N];
int sum[N];
void get_primes(int n)
{
for (int i = 2; i <= n; ++i)
{
if (!book[i]) primes[cnt++] = i;
for (int j = 0; primes[j] <= n / i; ++j)
{
book[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
vector<int> mul(vector<int>& v, const int p)
{
vector<int> c;
int t = 0;
for (int i = 0; i < v.size(); ++i)
{
t += v[i] * p;
c.push_back(t % 10);
t /= 10;
}
while (t)
{
c.push_back(t % 10);
t /= 10;
}
return c;
}
int get(int a, int p)
{
int res = 0;
while (a)
{
res += a / p;
a /= p;
}
return res;
}
int main()
{
int a, b;
cin >> a >> b;
get_primes(a);
for (int i = 0; i < cnt; ++i)
{
sum[i] = get(a, primes[i]) - get(b, primes[i]) - get(a - b, primes[i]);
}
vector<int> res(1, 1);
for (int i = 0; i < cnt; ++i)
{
for (int j = 0; j < sum[i]; ++j)
{
res = mul(res, primes[i]);
}
}
for (auto rit = res.rbegin(); rit != res.rend(); ++rit)
{
cout << *rit;
}
return 0;
}
练习:计算系数
🍬题目链接
:计算系数
🍎算法思想
:
首先,先复习一下二项式定理:
二项式定理的思想在于展开一个二项式的幂,即如何计算类似 ( a + b ) n (a + b)^n (a+b)n 的表达式。其核心思想涉及两个方面:
二项式的多项式展开:二项式是两个项的和,而二项式定理告诉我们如何将一个二项式的幂展开为多个项的和。例如, ( a + b ) n (a + b)^n (a+b)n 可以展开为多个项的和,其中每个项的形式都是 a m ⋅ b k a^m \cdot b^k am⋅bk,其中 m + k = n m + k = n m+k=n。
组合数的应用:在展开中,每个项的系数是由组合数 C ( n , k ) C(n, k) C(n,k)确定的,表示从 n n n 个不同元素中选择 k k k 个元素的方式数。这是因为在展开 ( a + b ) n (a + b)^n (a+b)n 中, a a a 的幂从 n n n 递减到 0 0 0,而 b b b 的幂从 0 0 0 递增到 n n n,每一项的幂次之和都是 n n n,所以需要计算每一项的系数,即组合数。
因此,二项式定理的思想是利用组合数的性质,按照特定的规则将二项式的幂展开为一系列项的和,每个项的幂次之和等于原始幂,并且每个项的系数是由组合数确定的。这使得我们能够轻松地处理二项式的高次幂,从而简化了许多代数计算。
这道题就是使用二项式定理求 x n y m x^ny^m xnym这一项的系数—— C k n ∗ a n ∗ b m % 10007 C^n_k*a^n*b^m\%10007 Ckn∗an∗bm%10007。
由于这道题的底数范围只有1000,所以直接使用上面复习的求组合数的方法一,暴力求解即可,复习一下这种暴力做法的思想:
给定一个 C ( n , k ) C(n, k) C(n,k),它可以通过以下递推公式计算:
C ( n , k ) = C ( n − 1 , k − 1 ) + C ( n − 1 , k ) C(n, k) = C(n-1, k-1) + C(n-1, k) C(n,k)=C(n−1,k−1)+C(n−1,k)
其中 C ( n , k ) C(n, k) C(n,k) 表示从 n n n 个元素中选择 k k k 个元素的组合数。这个递推公式的意义在于,一个组合数可以由其上方的两个组合数的和来计算得到。这个递推公式也是由组合数的性质推导出来的。
🍊具体实现
:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 10007;
typedef long long LL;
int C[N][N];
// 快速幂求a^n和b^m
int quick_power(int a, int k, int p) // 求a^k mod 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 main()
{
int a, b, k, n, m;
cin >> a >> b >> k >> n >> m;
// 暴力组合数递推
for (int i = 0; i <= k; ++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;
cout << C[k][n] * quick_power(a, n, mod) % mod * quick_power(b, m, mod) % mod << endl;
return 0;
}
总结
本周我们复习了:
- 区间DP
- 树形DP
- 快速幂
- 最大公约数
- 分解质因数
- 矩阵乘法
- 组合计数
如果解析有不对之处还请指正,我会尽快修改,多谢大家的包容。
如果大家喜欢这个系列,还请大家多多支持啦😋!
如果这篇文章有帮到你,还请给我一个大拇指
👍和小星星
⭐️支持一下白晨吧!喜欢白晨【蓝桥杯】系列的话,不如关注
👀白晨,以便看到最新更新哟!!!
我是不太能熬夜的白晨,我们下篇文章见。