Acwing-基本算法
基本算法
排序算法:
a.快速排序:
主要思想:
-
确定分界点:
x = a[l]
x = a[r]
q = a[(l + r) / 2] -
调整范围:
左边<=x
右边>x -
递归处理左边和右边
模板:
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x); // 可换成 while(q[ ++ i ] < x);
do j -- ; while (q[j] > 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(a, 0, n - 1);
说明:
- 如果x选取q[l],则递归时参数范围选择(l, j)和(j + 1, r)
- 如果x选取q[r],则递归时参数范围选择(l, i - 1)和(i, r)
- 如果x选取q[l + r >> 1],则递归时参数范围选择哪种都行
- 快速排序在对含有重复元素的数组排序时是不稳定的,但可以把元素值和其下标组成二元组{q[i], i}后再排序,这样就能使排序结果稳定。
b.归并排序:
主要思想:
- 确定分界点 mid = (l + r) / 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;
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];
}
说明:
在归并步骤时,如果碰到相同元素的插入,每次都选择第1段(左边)的元素插入,则能使归并算法稳定。
二分法:
二分法本质:对于一个区间(l,r),若其满足这样的性质:对于某个边界点的左边,他满足一个性质;对于另一个边界点,其右边又满足另一个性质;(即只要存在某种性质判定能把序列分成连续的两段,典型:区间上的单调性)则二分算法可以找出这两个边界点。
a.整数二分:
对于区间(l,r),不妨设其左边满足的性质为性质1,右边满足的性质为性质2,两者用check()函数来判定,则有:
<1>对于满足性质1的边界点(左边区间的右边界点):
<2>对于满足性质2的边界点(右边区间的左边界点):
模板:
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用(对应于<2>,即区间上check的true段在右,false在左):
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;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用(对应于<1>,即区间上check的true段在左,false在右):
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;
}
给定一个按照升序排列的长度为 n的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 kk的起始位置和终止位置(位置从 0开始计数)。
如果数组中不存在该元素,则返回 -1 -1
。
输入格式
第一行包含整数 n和 q,表示数组长度和询问个数。
第二行包含 n个整数(均在 1∼100001∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 kk,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1
。
数据范围
1≤n≤1000001≤n≤100000
1≤q≤100001≤q≤10000
1≤k≤100001≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
题解:
#include<cstdio>
using namespace std;
const int N=100010;
int t[N];
int n,q;
int main(){
scanf("%d%d",&n,&q);
for(int i=0;i<n;i++) scanf("%d",&t[i]);
while(q--){
int k;
scanf("%d",&k);
int l=0,r=n-1;
while(l<r){
int mid =(l+r)>>1;
if(t[mid]>=k) r=mid;
else l=mid+1;
}
if(t[l]!=k) printf("-1 -1\n");
else{
printf("%d ",l);
int l=0,r=n-1;
while(l<r){
int mid=l+r+1>>1;
if(t[mid]<=k) l=mid;
else r=mid-1;
}
printf("%d\n",l);
}
}
return 0;
}
b.浮点二分:
模板:
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;//注意都是double型
if (check(mid)) r = mid;
else l = mid;
}
return l;
前缀和&差分:
前缀和与差分是一对逆运算,类似于离散状态下积分与微分
a.前缀和:
一维前缀和:
定义:
模板:
int a[N], S[N];
for (int i = 1; i <= n; i++) S[i] = S[i - 1] + a[i]; // 给定数组a,初始化前缀和数组S
cout << S[r] - S[l - 1] << endl; // 计算a[l] + ... + a[r]
notes:
- 复杂度由O(n)降为O(1)
- 数组a和S的第1个元素都不存储(下标为0),而从第2个元素开始存储(下标为1)
- 注意遍历范围是1 ~ n
- 在一些不涉及a[i]的题目中,不必要存储a[i]的值,只需要存储S[i]就足够
二维前缀和:
模板:
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++) {
scanf("%d", &a[i][j]);
S[i][j] = S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1] + a[i][j];
}
cout << S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1] << endl; // 使用
notes
-
假设数组a中行下标或列下标为0的项都是0
-
复杂度由O(m * n)降为O(1)
-
读入数组a和初始化前缀和数组S的过程可以合并在一起
-
注意遍历范围是1 ~ n
-
在一些不涉及a[i]的题目中,不必要存储a[i][j]的值,只需要存储S[i]就足够
b.差分:
差分是前缀和的逆运算,即若a[n]为b[n]前缀和序列,则b[n]为a[n]差分序列。
一维差分
给a[n]区间l,r中的每个数加上c,即在其差分数组吧[n]的b[l]+c,b[r+1]-c从而讲O(n)化为O(1):
模板:
int a[N], B[N];
void insert(int l, int r, int c) {
B[l] += c;
B[r + 1] -= c;
}
// 初始化差分数组
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
insert(i, i, a[i]);
}
// 输出前缀和数组
for (int i = 1; i <= n; i++) {
B[i] += B[i - 1];
printf("%d ", B[i]);
}
二维差分
给x1,y1的每个数加上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];
notes
- insert()函数规律: 下标出现2的部分都+1
- 范围最大最小的+=c,其它-=c
双指针算法:
核心思想:
对于一个暴力求解的算法:
for(int i=0;i<n;i++){
for(int j=0;j<n;j++)
}
之类的,复杂度为$ O_{(n^2)} O_{(n)}$的算法,常见有快排,归并,KMP都有用到。
模板:
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
位运算:
求n的二进制第k位数字: n >> k & 1
返回n的最后一位1的位置:lowbit(n) = n & -n
离散化:
核心思想:
对于一个稀疏的序列,先把元素存储在vector
模板:
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素
//unique函数的去重过程实际上就是不停的把后面不重复的元素移到前面来,也可以说是用不重复的元素占领重复元素的位置,返回值是一个迭代器,它指向的是去重后容器中不重复序列的最后一个元素的下一个元素.
// 二分求出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
}
Example:
题解:
#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)//即找到alls在a[]中的映射值
{
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()
{
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), 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;
}
区间和:
目的:合并所有存在交集的区间
步骤:
- 先按左端点排序,然后再合并
- 选取第2个区间时,可分为两大类情况
有交集(包括“包含”和“相交但不包含”两种情况)
无交集
对于有交集的情况,只需保留最大的右端点即可
对于无交集的情况,首先判断是否是空区间(st == -2e9),非空则保存当前区间,并跳至下一个区间 - 由于循环内部是先发现新的无交集区间才保存当前指向的区间,因此在循环结束后,还需要单独保存当前区间(注意判断是否为空区间)
2.合并
// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end());
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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】