第一章 基础算法
一、排序
1.快速排序
AcWing 785. 快速排序
AcWing 786. 第k个数
基本思想:取任意中间值x,通过每次循环将数组分为大于等于x和小于等于x的两部分,再递归遍历两边,直到每一部分都只有一个元素,序列即有序。
优点:不需要额外空间,算法稳定
缺点:时间复杂度
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n),但最坏情况下会变为
O
(
n
2
)
O(n^2)
O(n2)
void quick_sort(int q[],int l,int r){
if(l>=r) return;//只有一个元素时已经有序
int x=q[l],i=l-1,j=r+1;//x是任意找的中间值,以q[l]为例
//i和j是从两端开始的指针
while(i<j){
do i++; while(q[i]<x);//从最左边开始寻找第一个大于x的数 i指针指向这个数
do j--; while(q[j]>x);//从最右边开始寻找第一个小于x的数 j指针指向这个数
if(i<j) swap(q[i],q[j]);//如果之战没有相遇则交换
}
quick_sort(q,l,j);//左半边递归
quick_sort(q,j+1,r);//右半边递归
}
2.归并排序
AcWing 787. 归并排序
AcWing 788. 逆序对的数量
基本思想:将数组二分为单个元素,通过两个指针遍历使其两两合并,最终得到有序序列。
优点:算法稳定,时间复杂度稳定在
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n)
缺点:需要
O
(
n
)
O(n)
O(n)额外空间
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,i=l,j=mid+1;//k为插入tmp数组的计数变量
//i和j为二分的两个数组的指针
while(i<=mid&&j<=r){
if(q[i]<=q[j]) tmp[k++]=q[i++];
else tmp[k++]=q[j++];//小的插入中转数组tmp
}
while(i<=mid) tmp[k++]=q[i++];
while(j<=r) tmp[k++]=q[j++];//剩下的放在最后
for(int i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];//从tmp数组回到q
}
二、二分
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;
else l=mid+1;
}
return l;//返回l和r都可以
}
//区间[l,r]被分为[l,mid-1]和[mid,r]时
int bsearch_2(intl,int r){
while(l<r){
int mid=l+r+1>>1;//如果不+1 当l=r-1时会死循环
if(check(mid)) l=mid;
else r=mid-1;
}
return l;//返回l和r都可以
}
2.浮点数二分
double bsearch_3(double l, double r){
const double eps = 1e-8;//eps表示精度,取决于题目对精度的要求,一般比精度多两位,保证结果能够正确
while (r-l>eps){//另一种方法:直接for循环100次,结果为n/(2^100),精度足够,但不建议
double mid=(l+r)/2;
if (check(mid)) r=mid;
else l=mid;
}
return l;//l和r都可以
}
三、高精度
1.高精度加法
//C=A+B
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;//有进位 => t=1
//无进位 => t=0
}
if(t) C.push_back(1);//判断最高位进位
return C;
}
2.高精度减法
//C=A-B (A>=B)
vector<int> sub(vector<int> &A,vector<int> &B){
vector<int> C;
int t=0;//借位初始值为0
for(int i=0;i<A.size();i++){
t=A[i]-t;
if(i<B.size()) t-=B[i];//总公式为 t=A[i]-B[i]-t
C.push_back((t+10)%10);//t>0 => t
//t<0 => t+10
if(t<0) t=1;
else t=0;//判断是否借位
}
while(C.size()>1&&C.back()==0) C.pop_back();//去除前导0
return C;
}
3.高精度乘低精度
//C=A*b
vector<int> mul(vector<int> &A,int b){
vector<int> C;
int t=0;//进位初始值为0
for(int i=0;i<A.size()||t;i++){//t为0时循环结束
if(i<A.size()) t+=A[i]*b;//循环A的每一位 与b整体相乘
C.push_back(t%10);
t/=10;//下一位的进位
}
while(C.size()>1&&C.back()==0) C.pop_back();//去除前导0
return C;
}
4.高精度除以低精度
//A/b=C...r
vector<int> div(vector<int> &A,int b,int &r){//余数r通过地址传出
vector<int> C;
r=0;//余数初始值为0
for(int i=A.size()-1;i>=0;i--){
r=r*10+A[i];//每一位余数=前一位余数*10+当前位
C.push_back(r/b);
r%=b;
}
reverse(C.begin(),C.end());//调换顺序
while(C.size()>1&&C.back()==0) C.pop_back();//去除前导0
return C;
}
四、前缀和
1.一维前缀和
AcWing 795. 前缀和
定义a[i]存储数据(a[0]=0),s[i]表示从a[1]到a[i]所有元素之和
即:
s
i
=
∑
n
=
1
i
a
n
s_i=\sum_{n=1}^ia_n
si=n=1∑ian
题目要求获得区间[l,r]所有元素之和
则有:
∑
i
=
l
r
a
i
=
s
r
−
s
l
−
1
\sum_{i=l}^ra_i=s_r-s_{l-1}
i=l∑rai=sr−sl−1
优点:当有m个询问时,时间复杂度从O(mn)降为O(m+n)
2.二维前缀和
AcWing 796. 子矩阵的和
定义a[i][j]存储数据,s[i][j]表示s[i][j]及其左上方所有元素之和
即:
s
i
,
j
=
∑
n
=
1
i
∑
m
=
1
j
a
n
,
m
s_{i,j}=\sum_{n=1}^i\sum_{m=1}^ja_{n,m}
si,j=n=1∑im=1∑jan,m
获取s[i][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}+a_{i,j}
si,j=si−1,j+si,j−1−si−1,j−1+ai,j
题目要求获得从(
x
1
,
y
1
x_1,y_1
x1,y1)到(
x
2
,
y
2
x_2,y_2
x2,y2)间所有元素之和
则有:
∑
n
=
x
1
x
2
∑
m
=
y
1
y
2
a
n
,
m
=
s
x
2
,
y
2
−
s
x
1
−
1
,
y
2
−
s
x
2
,
y
1
−
1
+
s
x
1
−
1
,
y
1
−
1
\sum_{n=x_1}^{x_2}\sum_{m=y_1}^{y_2}a_{n,m}=s_{x_2,y_2}-s_{x_1-1,y_2}-s_{x_2,y_1-1}+s_{x_1-1,y_1-1}
n=x1∑x2m=y1∑y2an,m=sx2,y2−sx1−1,y2−sx2,y1−1+sx1−1,y1−1
优点:当有m个询问时,时间复杂度从
O
(
m
n
2
)
O(mn^2)
O(mn2)降为
O
(
m
+
n
2
)
O(m+n^2)
O(m+n2)
五、差分
(其实就是前缀和的逆运算)
优点同样也是降低时间复杂度
1.一维差分
AcWing 797. 差分
类比一维前缀和,定义s[i]存储数据,且令a[i]+s[i-1]=s[i]
则有:
s
i
=
∑
n
=
1
i
a
n
s_i=\sum_{n=1}^ia_n
si=n=1∑ian
题目要求给区间[l, r]中的每个数加上c:
s[l]+=c; //l后面都加c
s[r+1]-=c;//r+1后面都减c
2.二维差分
AcWing 798. 差分矩阵
类比二维前缀和,定义s[i][j],a[i][j]
则有:
s
i
,
j
=
∑
n
=
1
i
∑
m
=
1
j
a
n
,
m
s_{i,j}=\sum_{n=1}^i\sum_{m=1}^ja_{n,m}
si,j=n=1∑im=1∑jan,m
获取a[i][j]方法:
a
i
,
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}
ai,j=si,j−si−1,j−si,j−1+si−1,j−1
题目要求给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
s[x1, y1] += c;
s[x2 + 1, y1] -= c;
s[x1, y2 + 1] -= c;
S[x2 + 1, y2 + 1] += c;
六、双指针算法
AcWIng 799. 最长连续不重复子序列
AcWing 800. 数组元素的目标和
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
八、离散化
AcWing 802. 区间和
当题目给定一个稀疏序列时,要将每个所需元素对应的下标映射到从1到n,从而降低时空复杂度
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到n
}
九、区间合并
// 将所有存在交集的区间合并
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;
}