文章目录
- 前言
- 第一章 基础算法
- 1.1 快速排序
- 1.1.1 相关练习 第k个数
- 1.2 归并排序
- 1.2.1 相关练习:逆序对的数量
- 1.3 二分
- 1.3.1 整数二分
- 1.3.1.1 相关练习:数的范围
- 1.3.2 浮点数二分
- 1.3.2.1 相关练习:数的三次方根
- 1.4 高精度模拟
- 1.4.1 高精度加法
- 1.4.2 高精度减法
- 1.4.3 高精度乘法
- 1.4.4 高精度除法
- 1.5 前缀和与差分
- 1.5.1 相关练习:前缀和(一维前缀和)
- 1.5.2 相关练习:子矩阵的和(二维前缀和)
- 1.5.3 相关练习:差分(一维差分)
- 1.5.4 相关练习:差分矩阵(二维差分)
- 1.6 双指针
- 1.6.1 相关练习 最长连续不重复子序列
- 1.6.2 相关练习 数组元素的目标和
- 1.6.3 相关练习 判断子序列
- 1.7 位运算
- 1.7.1 相关练习 二进制中1的个数
- 1.8 离散化
- 1.8.1 相关练习 区间和
- 1.9 区间合并
- 第二章 数据结构
- 第三章 搜索与图论
- 第四章 数学知识
- 第五章 动态规划
- 第六章 贪心
前言
- 🌰欢迎大家来到OpenAll_Zzz的博客,在这里我们一起努力,共同进步!
- 🎁这一期分享的内容有点庞大——算法,主要分享目前十分稳定的算法模板,主要帮助笔者复习使用。
- 📝如果大家对某个算法模板有疑惑,欢迎在评论区发表你的问题。
- 🎄本文内容为OpenAll_Zzz原创,转载的小伙伴还请标注一下来源。
- 📃本文章于2021年12月4日开篇,最近一次更新为2021年12月12日。
- 🎁欢迎大家评论📖、转发📤、关注👓,如果文章对你有帮助的话,希望能得到你的一个大大的赞👍。
第一章 基础算法
1.1 快速排序
快排是众多排序中整体性能最优的排序。
C++模板
#include <iostream>
using namespace std;
const int N = 10e6 + 10; // 对一百万的数据量进行测试,Accepted。
int n;
int q[N];
// 当前快排模板(很稳定)
void quick_sort(int* q, int l, int r){
if(l >= r) return;
int x = q[l + r >> 1], i = l - 1, j = r + 1; // 选取中间的值作为分界点
while( i < j ){
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
quick_sort(q, 0, n - 1);
for (int i = 0; i < n; i ++ ) printf("%d ", q[i ]);
return 0;
}
1.1.1 相关练习 第k个数
#include <iostream>
using namespace std;
const int N = 100010;
int n, k;
int q[N];
int quick_select(int l, int r, int k) // 快选算法借助了快排的思想
{
if(l == r) return q[l];
int x = q[l + r >> 1], i = l - 1, j = r + 1;
while(i < j)
{
while(q[ ++ i] < x);
while(q[ -- j] > x);
if(i < j) swap(q[i], q[j]);
}
int sl = j - l + 1;
if(k <= sl) return quick_select(l, j, k);
return quick_select(j + 1, r, k - sl);
}
int main()
{
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i ++) scanf("%d", &q[i]);
cout << quick_select(0, n - 1, k) << endl;
return 0;
}
1.2 归并排序
C++模板
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10e6 + 10;
int n;
int q[N], tmp[N];
void meger_sort(int* q, int l, int r)
{
if(l >= r) return;
int mid = l + r >> 1;
meger_sort(q, l, mid), meger_sort(q, mid + 1, r); // 分治的 "分"
int k = 0, i = l, j = mid + 1; // 分治的 "治"
while(i <= mid && j <= r)
{
if(q[i] <= q[j]) tmp[k ++] = q[i ++];
else tmp[k ++] = q[j ++];
}
// 收尾
while(i <= mid) tmp[k ++] = q[i ++];
while(j <= r) tmp[k ++] = q[j ++];
// 整合到原数组,使其部分有序,再去将其归并
for(i = l, j = 0; i <= r; i ++, j ++) q[i] = tmp[j];
}
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &q[i]);
meger_sort(q, 0, n - 1);
for(int i = 0; i < n; i++) printf("%d ", q[i]);
return 0;
}
1.2.1 相关练习:逆序对的数量
C++代码
#include <iostream>
#include <cstring>
#include <algorithm>
typedef long long LL;
using namespace std;
const int N = 100010;
int n;
int q[N], tmp[N];
LL meger_sort(int l, int r)
{
if(l == r) return 0;
int mid = l + r >> 1; // + 号优先级高于 >>
LL res = meger_sort(l, mid) + meger_sort(mid + 1, r);
// 归并的过程 ([l , mid] 和 [mid + 1, r] 均为有序的)
int k = 0, i = l, j = mid + 1;
while(i <= mid && j <= r)
{
if(q[i] <= q[j]) tmp[k ++] = q[i ++];
else
{
tmp[k ++] = q[j ++];
// 在归并的过程中前半部分的某个数x大于后半部分的某个数y,则x后面所有的数均大于y,则有关y的逆序对为
// mid - i + 1 (mid为前半部分的右端点)
res += mid - i + 1;
}
}
// 扫尾
while(i <= mid) tmp[k ++] = q[i ++];
while(j <= r) tmp[k ++] = q[j ++];
// tmp数组返还到原数组q,使其在区间[l, r]有序
for(i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
return res;
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
cout << meger_sort(0, n - 1) << endl;
return 0;
}
1.3 二分
1.3.1 整数二分
C++模板
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1; // l为mid时需要将mid设置为l + r + 1 / 2,否则会死循环(在l=r-1时)
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
1.3.1.1 相关练习:数的范围
C++代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10e6 + 10;
int n, T, x;
int q[N];
int main()
{
scanf("%d%d", &n, &T);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
while(T --)
{
scanf("%d", &x);
int l = 0, r = n - 1;
while(l < r)
{
int mid = l + r >> 1;
if(q[mid] >= x) r = mid;
else l = mid + 1;
}
if(q[l] != x) cout << "-1 -1" << endl;
else{
cout << l << " ";
int l = 0, r = n - 1;
while(l < r)
{
int mid = l + r + 1>> 1;
if(q[mid] <= x) l = mid;
else r = mid - 1;
}
cout << l << endl;
}
}
return 0;
}
1.3.2 浮点数二分
C++模板
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
1.3.2.1 相关练习:数的三次方根
C++代码
#include <iostream>
using namespace std;
int main()
{
double x;
cin >> x;
double l = -100, r = 100; // double %lf; float %f.
while(r - l > 1e-8)
{
double mid = (l + r) / 2;
if(mid * mid * mid >= x) r = mid;
else l = mid;
}
printf("%.6lf", l);
return 0;
}
1.4 高精度模拟
1.4.1 高精度加法
C++模板
#include <iostream>
#include <vector>
using namespace std;
vector<int> add(vector<int>& A, vector<int>& B)
{
vector<int> C;
int t = 0; // 定义进位变量,初始时为0,因为第一位是第一个产生进位的位次,进位是由计算前一位时产生的。
for(int i = 0; i < A.size() || i < B.size(); i ++ )
{
if(i < A.size()) t += A[i];
if(i < B.size()) t += B[i];
C.push_back(t % 10); // 计算每一位相加的后的值
t /= 10; // 计算进位
}
if(t) C.push_back(t);
return C;
}
int main()
{
vector<int> A, B;
string a, b;
cin >> a >> b;
for(int i = a.size() - 1; i >= 0; i --) A.push_back(a[i] - '0');
for(int i = b.size() - 1; i >= 0; i --) B.push_back(b[i] - '0');
auto C = add(A, B);
for(int i = C.size() - 1; i >= 0; i --) cout << C[i];
return 0;
}
// 1. 高精度加法讲究的是将大数转化为数组表示
// 2. 利用数组的每一位为数的位数进行模拟手动加法
// 3. 考虑每一位的进位以及最后的边界情况,边界情况即最后一位可能需要进位
1.4.2 高精度减法
C++模板
#include <iostream>
#include <vector>
using namespace std;
// 判断是否有 A >= B
bool cmp(vector<int>& A, vector<int>& B)
{
if(A.size() != B.size()) return A.size() > B.size();
else{
for(int i = A.size() - 1; i >= 0; i--) // 从高位开始判断
{
if(A[i] != B[i]) return A[i] > B[i];
}
}
return true;
}
// 减法是从低位开始模拟的,和手动计算一致
vector<int> sub(vector<int>& A, vector<int>& B) // A >= B的情况
{
vector<int> C;
for(int i = 0, t = 0; i < A.size(); i ++ )
{
t = A[i] - t;
if(i < B.size()) t -= B[i];
C.push_back((t + 10) % 10);
if(t < 0) t = 1;
else t = 0;
}
while(C.size() > 1 && C.back() == 0) C.pop_back(); // 消除前导0
return C;
}
int main()
{
string a, b;
cin >> a >> b;
vector<int> A, B;
for(int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
for(int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
vector<int> C;
if(cmp(A, B))
{
C = sub(A, B);
}
else {
C = sub(B, A);
printf("-");
}
for(int i = C.size() - 1; i >= 0; i-- ) cout << C[i];
return 0;
}
1.4.3 高精度乘法
C++模板
#include <iostream>
#include <vector>
using namespace std;
vector<int> mul(vector<int> &A, int b) // A * b A为大数,b比较小
{
vector<int> C;
for(int i = 0, t = 0; i < A.size() || t; i++)// 乘法是从最低位开始计算,和手动计算两数相乘一致
{
if(i < A.size()) t += A[i] * b; // 注意是+=,才会实现乘法的逐位进位
C.push_back(t % 10);
t /= 10;
}
while(C.size() > 1 && C.back() == 0) C.pop_back(); // 除去前导0
return C;
}
int main()
{
string a;
int b;
cin >> a >> b;
vector<int> A;
for(int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
auto C = mul(A, b);
for(int i = C.size() - 1; i>= 0; i--) cout << C[i];
return 0;
}
1.4.4 高精度除法
C++模板
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
vector<int> div(vector<int>& A, int b, int& r) // A / b : C是商,r是
{
vector<int> C;
for(int i = A.size() - 1; i >= 0; i -- )
{
r = r * 10 + A[i];
C.push_back(r / b);
r %= b;
}
reverse(C.begin(), C.end()); // 保持形式一致,同时便于除去前导0
while(C.size() > 1 && C.back() == 0) C.pop_back(); // 除去前导0
return C;
}
int main()
{
string a;
int b;
vector<int> A;
cin >> a >> b;
for(int i = a.size() - 1; i >= 0; i --) A.push_back(a[i] - '0');
int r = 0;
auto C = div(A, b, r);
for(int i = C.size() - 1; i >= 0; i --) cout << C[i];
cout << endl << r;
return 0;
}
1.5 前缀和与差分
前缀和与差分 ,是一组互逆的关系
例如数组a = {1, 3, 4, 6, 8}
,其前缀和数组为s = {1, 4, 8, 14, 22}
此时有a
的前缀和数组为s
,s
的差分数组(第二项起,每一项减去前一项的值)是a
1.5.1 相关练习:前缀和(一维前缀和)
C++代码
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int s[N]; // 直接在原数组上进行修改即可,减少数组a的内存开销
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) scanf("%d", &s[i]);
for(int i = 1; i <= n; i ++) s[i] += s[i - 1]; // 计算前缀和 原始--s[i] = s[i - 1] + a[i]
while(m -- )
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", s[r] - s[l - 1]);
}
return 0;
}
1.5.2 相关练习:子矩阵的和(二维前缀和)
C++代码
#include <iostream>
using namespace std;
const int N = 1010;
int n, m, q;
int s[N][N];
int main()
{
scanf("%d%d%d", &n,&m,&q);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d",&s[i][j]);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++) // 原始--s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1]; // 计算前缀和
while(q --)
{
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
printf("%d\n", s[x2][y2] - s[x2][y1 - 1] - s[x1 -1][y2] + s[x1 -1][y1 - 1]); // 计算部分和
}
return 0;
}
1.5.3 相关练习:差分(一维差分)
差分的核心就是:插入原数组来构造差分数组
对差分数组进行增加变换即可对应到原数组上
C++代码
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
void insert(int l, int r, int c) // 在一维差分数组中插入
{
b[l] += c;
b[r + 1] -= c;
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++) insert(i, i, a[i]);
int l, r, c;
while(m --){
scanf("%d%d%d", &l, &r, &c);
insert(l, r, c);
}
for(int i = 1; i <= n; i ++){
b[i] += b[i - 1]; // 计算前缀和,a数组其实可以不用
}
for(int i = 1; i <= n; i ++) cout << b[i] << ' ';
return 0;
}
1.5.4 相关练习:差分矩阵(二维差分)
C++代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c) // 画矩阵图来理解
{
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
int main()
{
scanf("%d%d%d", &n, &m, &q);
// for(int i = 1; i <= n; i ++)
// for(int j = 1; j <= m; j ++)
// scanf("%d", &b[i][j]); // 其实不用a数组,原地操作差分数组b即可
int x = 0;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
cin >> x;
insert(i, j, i, j, x); // 在差分数组b中进行插入操作
}
while(q --)
{
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
insert(x1, y1, x2, y2, c);
}
for(int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1]; // 计算自身的前缀和就是成为原数组
for(int i = 1; i <= n; i ++)
{
for(int j = 1; j <= m; j ++) printf("%d ", b[i][j]); // 此时b数组就是原数组
puts("");// puts函数自动换行
}
return 0;
}
1.6 双指针
双指针是一种优化思想,通过降低问题的维数来优化时间复杂度。
通常借助问题的单调性、唯一性、可证性来优化成双指针算法
1.6.1 相关练习 最长连续不重复子序列
C++代码
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int a[N], s[N];
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; i ++) scanf("%d", &a[i]);
// 如果是加进来的数a[i]导致重复,我们只能
// 通过减少区间中数的个数来去除这个重复的数a[i]
// 即j ++,同时减少计数数组中a[j]的个数
int res = 0;
for(int i = 0, j = 0; i < n; i ++)
{
s[a[i]] ++; // 只会是加进来的a[i]导致重复
while(s[a[i]] > 1)
{
s[a[j]] --;
j ++; // 当i == j 时,区间中只有一个数,循环结束,不会出现越界
}
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
1.6.2 相关练习 数组元素的目标和
C++代码
#include <iostream>
using namespace std;
const int N = 100010;
int n, m, x;
int A[N], B[N];
int main()
{
scanf("%d%d%d", &n, &m, &x);
for(int i = 0; i < n; i ++) scanf("%d", &A[i]);
for(int i = 0; i < m; i ++) scanf("%d", &B[i]);
for(int i = 0, j = m - 1; i < n; i ++) // 第二个指针指向数组的最后,保持单调性
{
while(j >= 0 && A[i] + B[j] > x) j --;
if(A[i] + B[j] == x)
{
printf("%d %d\n", i, j);
break;
}
}
return 0;
}
1.6.3 相关练习 判断子序列
C++代码
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
int main()
{
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i ++) scanf("%d", &a[i]);
for(int i = 0; i < m; i ++) scanf("%d", &b[i]);
int i = 0, j = 0;
while(i < n && j < m) // 可证性,双指针所找出的数匹配时,不会影响到后序的数是否被匹配到
{
if(a[i] == b[j]) i ++;
j ++;
}
if(i == n) puts("Yes");
else puts("No");
return 0;
}
1.7 位运算
位运算内容比较单一,核心操作只有两个。
🌰1. 获取一个二进制位中第k位是多少:x>> k & 1
🌰2. 返回二进制中最后一个以1开头的二进制位(lowbit函数)
:x & -x
。
Ps: -x == ~x + 1(一个数的负数等于其按位取反后加一)
在🌰2中,x取反加上1后,x中最后一个1的左边的二进制位与原来相反,x中最后一个1右边的二进制位全为0,与(&)运算后,即可得到结果。
1.7.1 相关练习 二进制中1的个数
C++代码
#include <iostream>
using namespace std;
int lowbit(int x)
{
return x & -x;
}
int main()
{
int n;
cin >> n;
while(n --)
{
int x;
scanf("%d", &x);
int res = 0;
while(x) res ++, x -= lowbit(x);
cout << res << ' ';
}
return 0;
}
1.8 离散化
🌰离散化指的:操作一组在数轴上数据范围特别特别大的数据,但是操作它们的个数与它们的范围比起来小很多很多。
📃即我们需要操作的这组数据数量很大(当然和它们的数据范围比起来要小很多),我们需要用数组来存储它们,这个时
📃候我们需要来想办法存储这一组数据,这个办法的思想就是离散化。(类似哈希映射的思想)
1.8.1 相关练习 区间和
C++代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N]; // 离散化后的数组、及其前缀和
vector<int> alls; // 在数轴上所有需要离散化的点
vector<PII> add, query; // 增加的信息、查询的信息
int find(int x) // 从小到大出现的次序作为映射值,作为离散化后数组中的坐标
{
int l = 0, r = alls.size() - 1;
while(l < r)
{
int mid = l + r >> 1;
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
int main()
{
scanf("%d%d", &n, &m);
// 读入需要增加的点和增加的值
for(int i = 0; i < n; i ++)
{
int x, c;
scanf("%d%d", &x, &c);
alls.push_back(x);
add.push_back({x, c});
}
// 读入查询的区间
for(int i = 0; i < m; i ++)
{
int l, r;
scanf("%d%d", &l, &r);
alls.push_back(l);
alls.push_back(r);
query.push_back({l, r});
}
// 离散化
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
// 增加操作
for(auto item : add)
{
int x = find(item.first);
a[x] += item.second;
}
// 预处理前缀和
for(int i = 1; i <= alls.size(); i ++) s[i] = s[i - 1] + a[i];
// 处理每一个查询
for(auto item : query)
{
int l = find(item.first), r = find(item.second);
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
1.9 区间合并
区间合并算是一个特殊的算法思想,总体上是模拟的同时来维护一个区间
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 100010;
int n;
vector<PII> segs; // 区间段 segment
// C++排序pair时先排序第一个后排序第二个,可以不用写cmp
// int cmp(PII a, PII b)
// {
// return a.first < b.first;
// }
void meger(vector<PII> &segs)
{
vector<PII> res;
int st = -2e9, ed = -2e9; // 定义边界来比较
for(auto seg : segs)
{
if(ed < seg.first)
{
if(st != -2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else
{
ed = max(ed, seg.second);
}
}
if(st != -2e9) res.push_back({st, ed}); //精髓所在
segs = res;
}
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; i ++)
{
int l, r;
scanf("%d%d", &l, &r);
segs.push_back({l, r});
}
sort(segs.begin(), segs.end());
meger(segs);
cout << segs.size() << endl;
return 0;
}