1. 基础算法
注:基础算法部分普遍简单,主要看注释,不做过多赘述和解释
1.1 排序
思想:分治
1.1.1 快速排序
步骤分解:
-
确定分界点
x = q[l]或q[r]或q[l + r / 2]或随机 -
调整范围:
⑴ 开辟额外数组
① a[], b[]
② 扫描q[l ~ r],的部分存入a[],的部分存入b[]
③ 先将a[]中的元素放入q[],再将b[]中的元素放入q[]
⑵ 双指针
① 指针 指向q[]首元素的前位,指针 指向q[]末元素的后位
② 右移指针 ,直到其指向的元素
③ 左移指针 ,直到其指向的元素
④ 交换指针 和 所指向的元素,并重复步骤②③,直到 和 相遇
- 递归处理左右两端
模板
void quick_sort(int q[], int l, int r)// l和r是闭区间边界
{
// 边界:如果数组内没有数或只有一个数,直接return
if (l >= r) return ;
// 确定双指针的初始位置和分界点x
int i = l - 1, j = r + 1, x = q[l + r >> 1];
// 如果i和j未相遇,则进行下一次迭代
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);
}
1.1.2 归并排序
步骤分解:
-
确定分界点——必须取中点
mid = (l + r) / 2 -
递归排序
-
归并——合二为一
① 双指针分别指向两个有序的数列的首元素
② 比较两个指针指向元素的大小,将较小的元素存入新数组,并将该指针前移一位
③ 重复步骤②,直到其中一个指针走到终点,最后将另一个指针后面剩余的部分直接存入新数组
模板
void merge_sort(int q[], int l, int r)// l和r是闭区间边界
{
// 边界:如果数组内没有数或只有一个数,直接return
if (l >= r) return;
// 确定双指针的初始位置和分界点mid,即区间的中点
int mid = l + r >> 1;
merge_sort(q, l, mid);// 递归排序左部分
merge_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 ++ ];
// 将tmp中的元素存回q
for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}
1.2 二分
1.2.1 整数二分
注意:判断左边界还是右边界,是否需要给边界加一
通用模板
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;// 上取整
if(check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
应用模板
// 1.在升序、无重复元素的数组中,查找值为k的元素的数组下标
const int N = ;
// 在给定数组中查找k值所在位置,若不存在则返回-1
// l为二分下界,r为二分上界
int binarySearch(int a[], int l, int r, int k)
{
int mid;
// 此处的循环条件为 l <= r 的原因:
// 若l > r,即不为闭区间时,作为元素不存在的依据,直接返回-1
while (l <= r)
{
// 此处也可使用 mid = l+(r-l)/2; 语句避免l+r溢出int
mid = (l + r) / 2;
if(a[mid] == k) return mid;
else if(a[mid] > k) r = mid - 1;
else l = mid + 1;
}
return -1;
}
int k, n;
int a[N];
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
scanf("%d", &k);
// 此处传入闭区间[0, n-1]
int pos = binarySearch(a, 0, n-1, k);
printf("%d\n", pos);
// 2.在升序、有重复元素的数组中,查找第一个大于等于k值的元素的数组下标
const int N = ;
// 在给定数组中查找第一个大于等于k值的元素的数组下标
// 若该元素不存在,则返回假设它存在,应该在的位置
// l为二分下界,r为二分上界
int lowerBoundL(int a[], int l, int r, int k)
{
while (l < r)
{
int mid = (l + r) / 2;
// 此处是r = mid,这是由判断条件a[mid]有可能等于k决定的
if(a[mid] >= k) r = mid;
else l = mid + 1;
}
// 此处返回r或l皆可,因为r == l
return l;
}
int k, n;
int a[N];
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
while(~scanf("%d", &k))
{
// 此处传入闭区间[0, n]
// 因为可能数组中所有值都小于k,则返回该元素的应在位置n
int pos = lowerBoundL(a, 0, n, k);
printf("%d\n", pos);
}
// 3.在升序、有重复元素的数组中,查找最后一个小于等于k值的元素的数组下标
const int N = ;
// 在给定数组中查找最后一个小于等于k值的元素的数组下标
// 若该元素不存在,则返回下标0
// l为二分下界,r为二分上界
int lowerBoundR(int a[], int l, int r, int k)
{
while (l < r)
{
// 注意此处对(l + r) / 2做上取整,防止仅剩两个数时发生死循环
// 注意:只在l = mid时才做如此处理
int mid = (l + r + 1) / 2;
if(a[mid] <= k) l = mid;
else r = mid - 1;
}
return l;
}
int k, n;
int a[N];
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
while(~scanf("%d", &k))
{
// 此处传入区间[0, n - 1]
int pos = lowerBoundR(a, 0, n - 1, k);
// 由于查找元素不存在时也返回0,故要对返回的数组下标做判断
if(pos != 0 || (pos == 0 && a[0] <= k)) printf("%d\n", pos);
else printf("-1\n");
}
// 4.在升序、有重复元素的数组中,查找第一个大于k值的元素的数组下标
const int N = ;
// 在给定数组中查找第一个大于k值的元素的数组下标
// 若该元素不存在,则返回假设它存在,应该在的位置
// l为二分下界,r为二分上界
int upperBound(int a[], int l, int r, int k)
{
while (l < r)
{
int mid = (l + r) / 2;
// 相较于模板二,只是在此处去掉了等号
if(a[mid] > k) r = mid;
else l = mid + 1;
}
return l;
}
int k, n;
int a[N];
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
while(~scanf("%d", &k))
{
// 此处传入区间[0, n]
// 原因见模板二
int pos = upperBound(a, 0, n, k);
printf("%d\n", pos);
}
1.2.2 浮点数二分
注意:浮点数的精度要求
模板
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 高精度
1.3.1 高精度加法
模板
// C = A + B, A >= 0, B >= 0
vector<int> add(vector<int> &A, vector<int> &B)// 用可变数组vector倒序存储大整数值,同时做引用可以避免对于整个vector的拷贝,提升效率
{
if (A.size() < B.size()) return add(B, A);// 判断A和B的长度,保证A比B长
vector<int> C;// 声明返回结果
int t = 0;// 记录每一次的进位数
for (int i = 0; i < A.size(); i ++ )
{
t += A[i];
if (i < B.size()) t += B[i];// 判断当前B位上是否存在数字
C.push_back(t % 10);// 保留
t /= 10;// 进位
}
if (t) C.push_back(t);// 判断最后一位是否有进位
return C;
}
string a, b;// 用string读入大整数
cin >> a >> b;
vector<int> A, B;// 倒序存入vector中,便于从低位开始计算
// 同时将字符转成数字,亦可写成A.push_back(a[i] - '0')
for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - 48);
for(int i = b.size() - 1; i >= 0; i--) B.push_back(b[i] - 48);
auto res = add(A, B);
for(int i = res.size(); i >= 0; i--) cout << res[i];// 正序输出
1.3.2 高精度减法
模板
// C = A - B, 满足A >= B, A >= 0, B >= 0
bool flag;
bool cmp(vector<int> &A, vector<int> &B)// 判断A,B大小,A大于等于B返回true,否则返回false
{
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;
}
vector<int> sub(vector<int> &A, vector<int> &B)
{
vector<int> C;// 声明返回结果
for (int i = 0, t = 0; i < A.size(); i ++ )// t记录每一次是否退位
{
t = A[i] - t;
if (i < B.size()) t -= B[i];// 判断当前B位是否存在
C.push_back((t + 10) % 10);// 此时t有 ≥ 0 和 < 0 两种情况,将其合二为一,即先加10,再模10
if (t < 0) t = 1;// 判断是否退位,退位则向上一位借1
else t = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();// 去掉多余的前导零
return C;
}
string a, b;// 用string读入大整数
cin >> a >> b;
vector<int> A, B, C;// 倒序存入vector中,便于从低位开始计算
// 同时将字符转成数字,亦可写成A.push_back(a[i] - '0')
for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - 48);
for(int i = b.size() - 1; i >= 0; i--) B.push_back(b[i] - 48);
flag = cmp(A, B);// 给flag赋值
if(flag) C = sub(A, B);
else cout << '-', C = sub(B, A);
for(int i = C.size() - 1; i >= 0; i--) cout << C[i];// 从高位依次输出
1.3.3 高精度乘法(高精度乘低精度)——类比高精度加法
模板
// 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);// 存入个位数,即对10取模
t /= 10;// 保留本次进位数
}
while (C.size() > 1 && C.back() == 0) C.pop_back();// 去掉多余的前导零
return C;
}
string a;// 用string读入大整数
int b;
cin >> a >> b;
vector<int> A, C;// 倒序存入vector中,便于从低位开始计算
// 同时将字符转成数字,亦可写成A.push_back(a[i] - '0')
for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - 48);
C = mul(A, b);
for(int i = C.size() - 1; i >= 0; i--) cout << C[i];
1.3.4 高精度除法(高精度除以低精度)
模板
// A / b = C ... r, A >= 0, b > 0
vector<int> div(vector<int> &A, int b, int &r)
{
vector<int> C;// 声明返回结果
r = 0;// 记录每一次的余数
for (int i = A.size() - 1; i >= 0; i -- )
{
r = r * 10 + A[i];// 上一位的余数到下一位计算时翻了十倍
C.push_back(r / b);// 存入整除b的结果
r %= b;// 保留本次余数
}
reverse(C.begin(), C.end());// 翻转数组C,保证低位在前
while (C.size() > 1 && C.back() == 0) C.pop_back();// 去掉多余的前导零
return C;
}
string a;// 用string读入大整数
int b, r;// 被除数和余数
cin >> a >> b;
vector<int> A, C;// 倒序存入vector中,便于从低位开始计算
// 同时将字符转成数字,亦可写成A.push_back(a[i] - '0')
for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - 48);
C = div(A, b, r);
for(int i = C.size() - 1; i >= 0; i--) cout << C[i];
cout << endl;
cout << r << endl;
1.4 前缀和与差分
1.4.1 前缀和
一、一维前缀和
一维前缀和简单来说就是中学数学中的数列 的前 项和
原数组:
前缀和:
边界:
关系:
作用:快速求出原数组中某一段元素的和 (时间复杂度为 )
模板
const int N = 1e5 + 10;
int n, m;
int a[N], S[N];
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i++) S[i] = S[i - 1] + a[i];//前缀和的初始化
while(m--)
{
int l, r;
scanf("%d%d", &l,&r);
printf("%d", S[r] - S[i - 1]);//前缀和的计算
}
二、二维前缀和
原矩阵:
前缀和矩阵:
关系: = 原矩阵第 行 列格子左上部分所有元素的和
作用:快速求出原矩阵中以 为左上角, 为右下角的子矩阵的和
模板
const int N = ;
int n, m, q;
int a[N][N], S[N][N];
scanf("%d%d%d", &n, &m, &q);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d", &a[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];//初始化前缀和
while(q--)
{
int xi, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
prinf("%d\n", S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1]);//计算子矩阵和
}
1.4.2 差分——前缀和的逆运算
一、一维差分
再次类比中学数学中的数列,差分可以理解成对于原数列 我们能否寻找到一个数列 使得 恒成立
原数组:
差分:
关系:
作用:给原数组里区间 中的每个元素加上 ,操作是B[l] += c, B[r + 1] -= c
模板
const int N = ;
void insert(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
int n, m;
int a[N], b[N];
scanf("%d%d", &n, &m);
//初始化原数组
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
//初始化差分数组,原理是把a[i]看作一个小区间,则l = r,故直接 insert(i, i, a[i]);
for(int i = 1; i <= n; i++) insert(i, i, a[i]);
while(m--)
{
int l, r, c;
scanf("%d%d%d", &l, &r, &c);
intsert(l, r, c);
}
//求前缀和
for(int i = 1; i <= n; i++) b[i] += b[i - 1];
//输出
for(int i = 1; i <= n; i++) printf("%d ", b[i]);
二、二维差分
原矩阵:
差分矩阵:
关系: = 差分矩阵中第 行 列格子左上部分所有元素的和
作用:给以 为左上角, 为右下角的子矩阵中的所有元素加上 ,操作是S[x1][y1] += c, S[x2 + 1][y1] -= c, S[x1][y2 + 1] -= c, S[x2 + 1][y2 + 1] += c
模板
const int N = ;
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[x1 + 1][y2 + 1] += c;
}
scanf("%d%d%d", &n, &m, &q);
//初始化原矩阵
for(int i = 1; i <= n; i++)
for(int j = 1; i <= m; i++)
scanf("%d", &a[i][j]);
//初始化差分矩阵,原理是把a[i][j]看作是一个1×1的小矩阵,则x1 = x2,y1 = y2,故直接 insert(x1, y1, x2 ,y2, c);
for(int i = 1; i<= n; i++)
for(int j = 1; i <= m; j++)
insert(i, j, i, j, a[i][j]);
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]);
put("");
}
1.5 双指针算法——核心:优化
双指针算法的本质就两个指针同时在序列上单调移动,相对于暴力求解的内外循环嵌套。因此关键就在于从序列中寻找到合理的单调关系,进而进行优化
常见问题分类:
-
对于一个序列,用两个指针维护一段区间
-
对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
作用:将朴素算法(暴力求解)的时间复杂度 降为
模板
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
题解
const int N = 1e5 + 10;
int a[N], s[N];// a记录原数组,s记录原数组中在当前区间中每个元素的数量
int n;
cin >> n;
for(int i = 0; i < n; i ++) cin >> a[i];
int res = 0;
for(int i = 0, j = 0; i < n; i++)// 双指针维护区间:[j, i]
{
s[a[i]]++;// 表示区间从右端点增加一个数
while(s[a[i]] > 1)
{
s[a[j]]--;// 表示区间从左端点减去一个数
j++;
}
res = max(res, i - j + 1);// 取二者较大值
}
cout << res << endl;
题解
const int N = 1e5 + 10;
int a[N], b[N];
int n, m, x;
cin >> n >> m >> x;
for(int i = 0; i < n; i++) cin >> a[i];
for(int i = 0; i < m; i++) cin >> 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)
{
cout << i << ' ' << j << endl;
break;
}
}
1.6 位运算
作用1:十进制数n的二进制表示中第k位的数字
原理:
① 先把第k位移到末位 n >> k
② 看个位数是几 n & 1
结合①和②得:n >> k & 1
作用2:lowbit函数,即返回二进制数的最后一位1
操作:x & (~x + 1)
原理:
x = 10100001000
~x = 01011110111
~x + 1 = 01011111000
x & (~x + 1) = 00000001000
1.7 离散化(针对整数)——核心:映射
定义:离散化,就是当我们只关心数据的大小关系时,用排名代替原数据进行处理的一种预处理方法
本质:离散化本质上是一种哈希,它在保持原序列大小关系的前提下把其映射成正整数
特点:数据的值域很大,同时元素存在负数、小数时,难以作为数组下标,但内部元素的分布通常很稀疏(和哈希表的特点很类似)
模板
vector<int> alls;// 存储所有待离散化的值
sort(alls.begin(), alls.end());// 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());// 去掉重复元素
// 二分求出x对应的离散化的值
int find(int x)// 找到第一个大于等于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;// 映射到1, 2, ...n
}
题解
const int N = 300010;
typedef pair<int, int> PII;
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;
}
cin >> n >> m;
for(int i = 0; i < n; i++)
{
int x, c;
cin >> x >> c;
add.push_back({x, c});
alls.push_back(x);
}
for(int i = 0; i < m; i++)
{
int l, r;
cin >> 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());
// 处理插入
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;
}
std::unique()的返回值是一个迭代器(对于数组来说就是指针了),它表示去重后容器中不重复序列的最后一个元素的下一个元素。所以可以这样作差求得不重复元素的数量。而对于 Python 或 JAVA 等没有unique函数,可以将其写成以下函数,然后进行调用
vector<int>::iterator unique(vector<int> &a)
{
int j = 0;
for(int i = 0; i < n; i++)
//对于一个有序数组,如果是第一个元素(即a[0])或下一个元素和当前元素不同(a[i] ≠ a[i - 1]),则说明该元素首次出现
if(!i || a[i] != a[i - 1])
a[j++] = a[i];
//此时a[0] ~ a[j - 1]为原数组中所有不重复的元素
return a.begin() + j;
}
离散化也不一定要从小到大排序,有时候也需要从大到小
1.8 区间合并
步骤:
(1) 按每个区间左端点值从小到大排序
(2) 设当前区间的左端点为 ,右端点为 ,下一个区间的左端点 ,右端点 ,则会出现三种情况:
① 区间更新:不变
② 区间更新:右端点 换成 ,即取二者的并集
③ 区间更新:区间 到 整体更新为 到
模板
const int INF = ;
typedef pair<int, int> PII;
// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end());// 对所有pair排序(左端点y
int st = -INF, ed = -INF;
for(auto item : segs)
if(ed < item.first)// ③ st < ed < l < r 区间更新:区间st到ed整体更新为l到r
{
// 需要先判断保证维护区间不是初始区间
if(st != -INF) res.push_back({st, ed});
st = item.first, ed item.second;
}
// ① st <= l < r <= ed区间更新:不变
// ② st <= l <= ed < r区间更新:右端点ed换成r,即取二者的并集
else ed = max(ed, item.second);
if(st != -INF) res.push_back({st, ed});// 将最后的区间加入最终答案
segs = res;// segs更新为res
}
参考资料:
[1] yxc. AcWing算法基础课常用代码模板1——基础算法. 原文链接
[2] MokylinJay. 整数二分详解. 原文链接
部分图片素材来自网络,如有侵权请联系删除