这里仅仅简述基础模板
一、排序
1.快排
(1)基本定义及思想
快速排序是由C. A. R. Hoare在1962年提出,对冒泡排序的一种改进,采用了一种分治的策略。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分是所以数据都比另外一部分的数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到数据变成有序序列,平均状况下时间复杂度为O(nlogn)。分治算法都分为三步:分成子问题
、递归处理子问题
、子问题合并
(2)动图演示
(3)模板
void quick(int q[], int l,int r)
{
if(l >= r) return ;//递归结束
//第一步:分成子问题
int i = l - 1, j = r + 1;
int x = q[l + r >> 1]; //用x分割成两部分
while(i < j)
{
do i++; while(q[i] < x); //找到左边大于x的数
do j--; while(q[j] > x); //找到右边小于x的数
if(i < j ) swap(q[i], q[j]); //交换
}
//第二步:递归处理子问题
quick(q, l, j); //将分割的左边部分进行递归处理
quick(q, j + 1, r); //将分割的右边部分进行递归处理
//第三步:这里不需要
}
练习题:AcWing 785. 快速排序
2.归并
归并算法也是在分治算法的基础上设计出来的一种排序算法,它可以对指定序列完成升序或降序排序,跟快排的不同主要在于归并问题需要额外空间,而且在最后多了合并子问题
这一步骤
(1)动图演示
(2)模板
void merge_sort(int q[], int l, int r)
{
//递归的终止情况
if(l >= r) return;
//第一步:分成子问题
int mid = l + r >> 1;
//第二步:递归处理子问题
merge_sort(q, l, mid ), merge_sort(q, mid + 1, r);
//第三步:合并子问题
int k = 0, i = l, j = mid + 1, tmp[r - l + 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(k = 0, i = l; i <= r; k++, i++) q[i] = tmp[k];
}
练习题:AcWing 787. 归并排序
二、二分模板
(1)整数二分
// 区间[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;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
练习题:789. 数的范围
(2)浮点数二分
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;
}
练习题:790. 数的三次方根
三、高精度
(1)高精度概念:
什么是高精度算法?它是处理大数字的数学计算方法、在一般的科学计算中,会经常算到小数点后近百位或者更多,当然也可能是几千亿几百亿的大数字。一般这类数字我们统称为高精度数,因其运算的范围大大超出了标准数据类型能表示的运算范围,为了处理高进度,我们可以使用vector数组来存储高精度数中的每一位数字。
(2)模板:高精度加法、减法、乘法、除法
>>高精度加法
vector<int> add(vector<int> &A, vector<int>&B)
{
vector<int> C; //开一个vector数组来存储加和后的每一位数字
int t = 0; //每次存储并加上第i位的数字
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(1);
return C;
}
>> 高精度减法
//比较函数,判断输入数据的大小
bool cmp(vector<int>&A, vector<int>&B)
{
if(A.size() != B.size())return A.size() > B.size();
for(int i=A.size()-1 ; i>=0 ; i--)
{
if(A[i]!=B[i])return A[i]>B[i];
}
return true; //判断 a > b 是否为真
}
//减法操作
vector<int> sub(vector<int>&A, vector<int>&B) //传引用是为了提高效率
{
vector<int> C; //开一个vector数组来存储减去后的每一位数字
for(int i = 0, t = 0 ; i < A.size() ;i++) //除法操作需要从最高位开始,即i = 0
{
t = A[i] - t;
if(i < B.size()) t -= B[i];
C.push_back((t + 10) % 10); //保证插入的数据为正值
if(t < 0) t = 1; //减去的结果为负,借1位
else t = 0;
}
while(C.size() > 1 && C.back() == 0) C.pop_back(); //除去前导零
return C;
}
>> 高精度乘法
//乘法操作
vector<int> multi(vector<int>&A, vector<int>&B) //传引用是为了提高效率
{
vector<int> C(A.size() + B.size() + 5, 0); //开一个vector数组来存储乘上的数字,初始化为0
for(int i = 0 ; i < A.size() ; i++)
for(int j = 0 ; j < B.size(); j++)
C[i + j] += A[i] * B[j]; //用C[i + j] 存储对应位置乘上所得到的数
int t = 0;
for(int i = 0 ; i < C.size(); i ++)
{
t += C[i];
C[i] = t % 10; //插入数值
t /= 10; //更新t
}
//除去前导零, 这里是因为我们开vector数组C的长度大于了A, B 之和
while(C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
>> 高精度除法
//比较函数
bool cmp(vector<int> &A, vector<int> &B)
{
if(A.size() != B.size()) return A.size() > B.size();
for(int i = A.size() - 1 ;i >= 0 ; i--)
{
if(A[i] != B[i]) return A[i] > B[i];
}
return true; //a > b
}
//减法操作
vector<int> sub(vector<int> &A,vector<int> &B)
{
vector<int> C;
int t = 0;
for(int i = 0;i < A.size() || t; 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(); //除去前导零
return C;
}
//除法操作
vector<int> div(vector<int> &A, vector<int> &B, vector<int> &r)
{
vector<int> C; //存储商的每一位数字
if(!cmp(A,B)) // a < b 直接插入0,余数为a
{
C.push_back(0);
r=A;
return C;
}
int j = B.size();
r.assign(A.end()-j, A.end()); //将区间元素赋值到当前的容器里
while(j <= A.size())
{
int k=0;
while(cmp(r, B)) //当前余数r > 除数
{
r = sub(r, B); r = r - b
k ++; //k增加1
}
C.push_back(k);
if(j < A.size()) r.insert(r.begin(),A[A.size()-j-1]);
if(r.size() > 1&& r.back() == 0) r.pop_back();
j++;
}
reverse(C.begin(), C.end());
while(C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
练习题:AcWing 791. 高精度加法 、AcWing 792. 高精度减法、AcWing 793. 高精度乘法 、AcWing 794. 高精度除法
四、前缀和与差分
(1)前缀和概念:
前缀和是指某序列的前 n 项和,可以把它理解为数学上的数列前 n 项和,二差分可以看作成前缀和的逆运算。合理的使用前缀和与差分,可以将某些复杂的问题简单化。
简单来说,我们可以有一个原数组 a[n] 和一个前缀和数组 s[n] ,他们说满足以下条件:
s[0] = a[0]
s[1] = a[0] + a[1]
s[2] = a[0] + a[1] + a[2]
...
依次类推,即可得公式 s[n] = a[n] + a[n - 1] + a[n - 1] + ... + a[1] + a[0]
也即 s[n] = s[n - 1] + a[n]
(2)一维前缀和、二维前缀和
- 一维前缀和的得到非常简单,也很好理解,我们只需要在每次遍历的时候,把前一次计算的和 s[n - 1] 加到当前正在计算的位置 s[n] 上,就能够构造出来,即:
for(int i = 0 ; i <= n ; i++) cin >> a[i], s[i] = s[i - 1] + a[i];
- 二维前缀和的得到则需要根据图中二维矩阵的运算规律进行推导
由此,我们就可以使用前缀和快速的求出某个区间所有元素的加和、矩阵中某个子矩阵的所有元素之和,如:AcWing 795. 前缀和 || AcWing 796. 子矩阵的和
(3)差分的介绍
差分是求前缀和的逆操作,类似于数学中的求导和积分,对于原数组 a[n] ,构造出一个数组 b[n] ,使得 a[n] 为 b[n] 的前缀和。一般用于快速的对整个数组进行操作,比如将原数组中的 [l, r] 部分的数据全部加上一个常数 c 。如果使用暴力的话,时间复杂度至少为O(n),而使用差分算法的话就可以将时间复杂度降低到O(1)。
(4)差分的运用:一维差分、二维差分
一维差分的构造方法:
a[0] = 0
b[1] = a[1] - a[0]
b[2] = a[2] - a[1]
b[3] = a[3] - a[2]
...
依次类推, 即可得 b[i] = a[i] - a[i - 1]
由于 a 数组为 b 数组的前缀和,每次在 b[i] 位置上加上或者减去一个数,转化为原数组 a 后都会对该位置后面的元素产生相同的影响,所以我们可以利用这个性质,在时间复杂度为O(1)的基础上,对某一个区间执行加上或者减去一个数的操作,即
b[l] + c 后,效果使得 a 数组中a[l] 后面的数组都会加上 c 红色部分,a[r] 后面的数据也会改变,所以再让 b[r + 1] - c 绿色部分,就实现了对区间 [l, r] 进行加减的操作,时间复杂度为O(1), 大大提高了效率。
对于二维差分矩阵,我们改如何去构造呢?
对应练习题:AcWing 797. 差分 、AcWing 798. 差分矩阵
五、双指针
(1)概念
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同的方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。一般来说,在数组遍历的过程中需要注意以下几个细节:双指针的初始位置
、
双指针的移动方式
、遍历的结束条件
。
(2)模板
这里以800. 数组元素的目标和为例
//两指针i,j
// i的初始位置为0, 往后搜索
//j的初始位置为m - 1, 往前搜索
//遇到满足条件的状态(a[i] + b[j] = q)时结束
for(int i = 0 , j = m - 1; i < n ; i ++)
{
while(j > 0 && b[j] + a[i] > q) j--; //j指针不会回退
if(a[i] + b[j] == q) //check(a, b)判断条件
{
cout << i << " " << j << endl;
return 0;
}
本例还可以用二分思想来做
for(int i = 0 ; i < n ; i++ )
{
int x = q - a[i]; //二分
int l = 0, r = m - 1;
while(l <= r)
{
int mid = l + r >> 1;
if(x == b[mid])
{
cout << i << " " << mid << endl; return 0;
}
else if(b[mid] > x) r = mid - 1;
else l = mid + 1;
}
练习题:AcWing 799. 最长连续不重复子序列 、AcWing 2816. 判断子序列
六、位运算
(1)简介
现代计算机中所有的数据都是以二进制(0、1)的形式存储在设备中,位运算就是直接对整数在内存中的二进制位进行操作,因此执行效率非常高,在程序中尽量使用位运算进行操作,能够大大提高程序的性能。
(2)图表概览
符号 | 描述 | 运算规则 |
---|---|---|
& | 按位与 | 两个位都为1时,结果才为1,如:1001 & 1101 = 1001 |
| | 按位或 | 两个位都为0时,结果才为0,如:1001 & 1101 = 1101 |
^ | 异或 | 两个位相同为0,相异为1,如:1001 & 1101 = 0100 |
~ | 取反 | 0 -> 1 、 1 -> 0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
(3)模板 AcWing 801. 二进制中1的个数
while(n--)
{
int x, res = 0;
cin >> x;
for(int i = 0 ; i <= 31 ; i ++) //每次右移一位
if(x >> i & 1) res++; // x的第k位数字: x >> k & 1若是1,则res++
cout << res << " ";
}
这里补充一下计算机中原码、补码和反码的内容
- 正数的原码、反码、补码相同
- 负数的原码将对应正数的符号位变为1、反码根据原码按位取反、补码为反码+1。
[+9] 原 = 0000 0000 0000 0000 0000 0000 0000 1001
[+9] 反 = 0000 0000 0000 0000 0000 0000 0000 1001
[+9] 补 = 0000 0000 0000 0000 0000 0000 0000 1001
[-9] 原 = 1000 0000 0000 0000 0000 0000 0000 1001
[-9] 反 = 1111 1111 1111 1111 1111 1111 1111 0110
[-9] 补 = 1111 1111 1111 1111 1111 1111 1111 0111
七、离散化
离散化的本质是映射,即将间隔很大的点,映射到相邻的数组元素中,可以减少对空间的要求。这里用一道模板题区间和来理解
其实映射最大的难点是前后的映射关系,如何能够将不连续的点映射到连续的数组的下标。解决办法就是开辟额外的数组存放原来的数组下标,或者说下标标志,本文是原来上的数轴上的非连续点的横坐标。此处的做法是是对原来的数轴下标进行排序,再去重,为什么要去重呢,因为本题提前考虑了前缀和的思想,其实很简单,就是我们需要求出的区间内的和的两端断点不一定有元素,提前加如需要求前缀和的两个端点,有利于我们进行二分搜索,其实二分搜索里面我们一般假定有解的,如果没解的话需要特判,所以提前加入了这些元素,从而导致可能出现重复元素。
AC代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 300010; //n次插入和m次查询相关数据量的上界
int n, m;
int a[N];//存储坐标插入的值
int s[N];//存储数组a的前缀和
vector<int> alls; //存储(所有与插入和查询有关的)坐标
vector<pair<int, int>> 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 = 1; i <= n; i++) {
int x, c;
scanf("%d%d", &x, &c);
add.push_back({x, c});
alls.push_back(x);
}
for (int i = 1; i <= m; i++) {
int l , r;
scanf("%d%d", &l, &r);
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
//排序,去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
//执行前n次插入操作
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];
//处理后m次询问操作
for (auto item : query) {
int l = find(item.first);
int r = find(item.second);
printf("%d\n", s[r] - s[l-1]);
}
return 0;
}