算法基础
第 一 讲
快速排序
知识点
1.确定分界点 (x) 数组 q 的 中间值
x = a[(l + r) / 2] //一般以中位数为分界点,很少 x = a[l] / a[r] / 随机
2.调整范围:<重点>
3.递归 处理 左边 和 右边
模板
void quick_sort(int q[], int l, int r)//快速排序
{
if (l >= r)
return;
int x = q[(l + r) / 2]; // 分界点
int i = l - 1 ; // 因为下面的是最开始就将i,j移动一位,再判断,所以要在边界的两端前一位开始
int j = r + 1 ;
while (i < j)
{
while(q[ ++ i ] < x);
while(q[ -- j ] > x);
if (i < j)
swap(q[i], q[j]);
}
quick_sort(q, l, j); //递归遍历左边
quick_sort(q, j + 1, r); //递归遍历右边
}
quick_sort(q, 0, n - 1); // mian函数调用快排方法
归并排序
1.主要思想
// 自底向上
1. 确定分界点 mid = (l + r) / 2 ; //中间 下标 位置
2. 递归处理左,右两段
3. 归并(双指针算法,指针表示剩余部分中最小元素的位置)
2.模板
void merge_sort(int q[], int l, int r) // 归并排序
{
if (l >= r)
return;
int mid = (l + r) / 2;
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);
// 归并过程 !
int k = 0 ; // 临时数组 tmp 的下标
int i = l, j = mid + 1;
while(i <= mid && j <= r)
{
if(q[i] <= q[j])
tem[k++] = q[i++];
else
tem[k++] = q[j++];
}
while (i <= mid) // 扫尾
tem[k++] = q[i++];
while (j <= r)
tem[k++] = q[j++];
for (i = l ,j = 0; i <= r; i ++ ,j++ ) // 物归原主
q[i] = tem[j];
}
merge_sort(q, 0, n - 1); // 调用方法
二分
知识点
整数二分二分模板
1.循环必须是l < r
2.if判断条件看是不是不满足条件, 然后修改上下界
3.若if else后是r = mid - 1,则前面mid 语句要加1
4.出循环一定是l == r,所以l和r用哪个都可以
二分只有下面两种情况
1:找 [ >= X ] 的第一个位置 (满足某个条件的第一个数)
2:找 [ <= X ]的最后一个数 (满足某个条件的最后一个数)
二分的流程:
1.确定二分的边界
2.编写二分的代码框架
3.设计一个check(性质)
4.判断一下区间如何更新
5.如果更新方式写的是l = mid, r = mid - 1,那么就在算mid的时候加上1
模板
// 判断条件很复杂时用check函数,否则if后直接写条件即可
bool check(int mid) {
...
return ...;
}
/*
能二分的题一定是满足某种性质,分成左右两部分
if的判断条件是让mid落在满足你想要结果的区间内
*/
// 模板1: 找满足某个条件的第一个数 即右半段的第一个值
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid))
r = mid;
else
l = mid + 1;
}
return l;
}
// 模板2: 找满足某个条件的最后一个数 即左半段的最后一个值
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;
}
//浮点数二分
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;
}
//说明:
浮点数二分不需要考虑mid是否 + 1,else后是否 + 1。
没有固定的浮点数序列,因此要考虑精度eps,一般比题目要求多1位小数就行
需要自己确定l和r的值,即查找范围
例题
/*
leetcode 69:x的平方根
实现 int sqrt(int x) 函数。计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
说明: 8 的平方根是 2.82842…, 由于返回类型是整数,小数部分将被舍去。
*/
class Solution {
public:
int mySqrt(int x) {
int l = 0, r = x;
while (l < r) {
int mid = l + r + 1ll >> 1;
if (mid <= x / mid)
l = mid;
else
r = mid - 1;
}
return l;
}
};
// 两个int相加减会溢出 中间加个长整型常量
// 少用乘法,用除法可以防止溢出
/*
解题思路:
1. 首先我们要思考采用它是属于哪一种类型的模板。
2. 这道题只能采用模板2,即答案 X 在左边的情况。
3. 因为我们这道题要求的时 “由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。” 因此如果结果时 2.333, 2.999, 2.123 之类的最后输出都只能是 2
4. 在确定了属于哪种类型后就直接套模板写代码即可。
*/
第 二 讲
- 大整数存储
vector<int> A;
string a;
cin >> a;
for (int i = a.size() - 1; i >= 0; i--)
A.push_back(a[i] - '0');
- 大整数比较
// A >= B返回true,否则返回false
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;
}
高精度运算
说明:
- 大整数低位存放在数组低地址处,高位存放在数组高地址处
- 数组地址由低到高(0→n - 1)
- 整数位数最左边是高位,最右边是低位(高位→低位)
- 注意处理最高位进位
- 读取数组时反向(n-1→0)遍历,运算时正向(0→n-1)遍历
数组下标 | 0 | 1 | 2 | 3 |
---|---|---|---|---|
原来的数 | 8 | 7 | 6 | 5 |
数组存储 | 5 | 6 | 7 | 8 |
// 加法
#include<iostream>
#include<vector>
using namespace std;
vector<int> add(vector<int> &A, vector<int> &B) // 加引用避免再拷贝整个数组
{
vector<int> C;
int t = 0;
for (int i = 0; i < A.size() || i < B.size(); i++) // 从个位开始遍历
{
if(i < A.size()) // 每一次用t表示Ai,Bi与上一个数的进位这三个数的和
t += A[i];
if(i < B.size())
t += B[i];
C.push_back(t % 10); // 当前这一位输出t除以10的余数
t /= 10; // t是否进位
}
if(t) C.push_back(1); // 如果最高位有进位则补1
return C;
}
int main()
{
string a, b; // 使用字符串读入
vector<int> A, B;
cin >> a >> b; //a = "123456"
for (int i = a.size() - 1; i >= 0; i--) // 使用vector逆序读入,变成整数需要减去偏移量0,A = [6,5,4,3,2,1]
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--)
printf("%d", C[i]); // 倒序输出
return 0;
}
// 个位放在索引0的位置:出现进位的情况时需要在高位补上1,这样在数组后端补数比较容易
// 乘法
// C = A * b, A >= 0, b > 0
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
int t = 0;
for (int i = 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();
return C;
}
前缀和
一维前缀和
/*
笔记:
1. 假设S0=a0=0
2. 复杂度由O(n)降为O(1)
3. 数组a和S的第1个元素都不存储(下标为0),而从第2个元素开始存储(下标为1)
4. 注意遍历范围是1 ~ n < 重点 >
5. 在一些不涉及a[i]的题目中,不必要存储a[i]的值,只需要存储S[i]就足够
*/
int a[N], S[N];
for (int i = 1; i <= n; i++)
S[i] = S[i - 1] + a[i]; // 给定数组a,初始化前缀和数组S
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i])
S[i] = S[i - 1] + a[i]; // 未给定数组a,可合并读入和初始化的过程
}
int l, r;
cout << S[r] - S[l - 1] << endl; // 计算a[l] + ... + a[r]
二维前缀和
/*
说明:
假设数组a中行下标或列下标为0的项都是0
复杂度由O(m * n)降为O(1)
读入数组a和初始化前缀和数组S的过程可以合并在一起
注意遍历范围是1 ~ n ! < 重点 >
在一些不涉及a[i][j]的题目中,不必要存储a[i][j]的值,只需要存储S[i][j]就足够
*/
int a[N][N], S[N][N];
// 给定数组a
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];
// 没有给定数组a,需要读入并初始化前缀和数组,则可以合并读入和初始化的过程
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
cin >> a[i][j] ;
S[i][j] = S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1] + a[i][j];
}
//( x1, y1 ) , ( x2, y2 )
res = S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1] // 使用
差分
一维差分
给一维数组 a 的区间 [ l , r ] 中的每个数加上C:
//原数组就是差分数组的前缀和
原始数组:9 3 6 2 6 8
差分数组:9 -6 3 -4 4 2
void insert(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
注意:下标也是从 1 开始
- 一个例子:
// 操作 b 数组, 初始化为 0
void insert(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
//a[i]
insert(l, r, c); // 操作: 将在数组 a 的区间 l - r 的所有数加上 c , 实际上是在 数组 b 上操作,a 数组不变
for (int i = 1; i <= n; i ++ ) //求数组 b 的前缀和
b[i] += b[i - 1];
for (int i = 1; i <= n; i ++ ) { // a 数组 + b 的前缀和 为答案
a[i] += b[i];
cout << a[i] <<' ';
}
二维差分
给 [x1, y1] 与 [x2, y2] 构成的矩形范围内的每个数加上c:
b[ x1 ][ y1 ] += c 蓝色 矩形面积的元素都 加上 c
b[ x1 ][y2 + 1] -= c 绿色 矩形面积的元素再 减去 c, 使其内元素不发生改变
b[x2 + 1][ y1 ] -= c 紫色 矩形面积的元素再 减去 c, 使其内元素不发生改变
b[x2 + 1][y2 + 1] += c 红色 矩形面积的元素再 加上 c, 红色内的相当于被减了两次,再加上一次c,才能使其恢复
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 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;
}
// 构造(无需额外的数组a)
int tmp;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
scanf("%d", &tmp);
insert(i, j, i, j, tmp);
}
}
// 转换成二维前缀和数组
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];
int 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;
}
// 构造(无需额外的数组a)
int tmp;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
scanf("%d", &tmp);
insert(i, j, i, j, tmp);
}
}
// 转换成二维前缀和数组
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];