对基础课第一章的总结
第一章内容包括:
快速排序,归并排序,
二分(两种二分划定)
高精度
前缀差分(一维二维)
双指针算法的思路
位运算
离散化 区间的合并
快速排序
思路:
1.先任意选择数组内的一个数字x作为快排的划分点。 利用两个指针i,j ,将小于x的数字移动到x的左边,大于x的数字放在x的右边。
2.再通过分治思路,将x及左边的数组进行1.操作的步骤。x右边的数组也同样进行1.操作的步骤。
最后不用合并数组,因为一步一步往小区间的划分排序,就会使整个数组最终呈现有序的状态
时间复杂度 一般是O(nlogn) ,最坏O(n^2)
代码:
#include <iostream>
using namespace std;
const int maxn=1e6+10;
int q[maxn];
void quick_sort(int l,int r){
if(l>=r) return; //终止递归的条件
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(l,j);
quick_sort(j+1,r);
}
int main() {
int n;
scanf("%d",&n);
for(int i=0; i<n; i++) scanf("%d",&q[i]);
quick_sort(0,n-1);
for(int i=0;i<n;i++) printf("%d ",q[i]);
return 0;
}
归并排序
思路:与快排相反,它是一个先划分区间,再排序,最后合并区间的过程
但归并排序需要一个额外的临时数组tem,用来存放小区间内排序好的数字,最后将tem的数字放在真正数组的 left与right区间
代码:
#include<iostream>
using namespace std;
const int maxn=1e6+5;
int p[maxn];
int tem[maxn];
void merey_sort(int p[],int l,int r){ //p排序数组 l当前p的左边界 r当前p的右边界
if(l>=r) return;
int mid=l+r>>1; //因为+的优先级比>>高,所以不需要加括号
merey_sort(p,l,mid); merey_sort(p,mid+1,r);//将数组先划分一层一层 的 一对区间
int k=0,i=l,j=mid+1; //k是tem数组的一个指针,表示当前区间已经排序了的数字个数
//i是指向左半边区间的第一个数的位置 j指向右半边区间的第一个数的位置
while(i<=mid && j<=r)
if(p[i]<p[j]) tem[k++]=p[i++];
else tem[k++]=p[j++];
while(i<=mid) tem[k++]=p[i++];
while(j<=r) tem[k++]=p[j++];
for(int i=l,j=0;i<=r;i++,j++) p[i]=tem[j]; //i指向p数组的左边界,将tem数组排序后的值放在p的[l,r]区间中
}
int main(){
int n;
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&p[i]);
merey_sort(p,0,n-1); //初始边界为l:0 r:n-1
for(int i=0;i<n;i++)printf("%d ",p[i]);
return 0;
}
二分
有单调性的序列 肯定可以二分 。但二分的本质不在于单调性 ,而是边界。 意思是如果我们能找到这样一个性质,每次都可以使得该性质在这一半区间中成立,而在另一半中不成立,那么我们就可以二分,将这个边界点二分出来。
二分分为两种二分方法:
第一中是寻找大于等于 x 的首位置
第二种是寻找大于 x 的 的首位置
具体模板:
我们要注意的是,如果在二分中,l=mid ,那么在取中间值mid的时候,就需要向上整除 即 mid = l + r + 1 >> 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;
}
也可以用c++库函数的
lower_bound 与upper_bound
int* p = lower_bound(a, a + a.size(), x)-a; // 查找数组在指定范围内大于等于x的元素下标(要求数组有序)
int* p = upper_bound(a, a + a.size(), x)-a; // 查找数组在指定范围内大于x的元素下标(要求数组有序)
对于浮点数的二分:
这个就很简单了,不用考虑边界,直接 l =mid 以及 r = mid 就行,
但要注意的就是不同于整型的 while(l<r) 循环规则变成lwhile(r-l>exp) //exp为精度 比如.1e-6
高精度
这个就是多手写模板才能记忆了,思路就是我们日常手算思路的模拟
下面给出模板
高精度加法
// C = A + B, A >= 0, B >= 0
vector<int> add(vector<int> &A, vector<int> &B)
{
if (A.size() < B.size()) return add(B, A);
vector<int> C;
int t = 0;
for (int i = 0; i < A.size(); i ++ )
{
t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
}
if (t) C.push_back(t);
return C;
}
高精度减法
// C = A - B, 满足A >= B, A >= 0, B >= 0
vector<int> sub(vector<int> &A, vector<int> &B)
{
vector<int> C;
for (int i = 0, t = 0; i < A.size(); 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;
}
高精度乘法
// 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;
}
高精度除法
// 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);
r %= b;
}
reverse(C.begin(), C.end());
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
前缀与差分
一维前缀:
s[i] = s[i-1] + a[i]
每个s[i]中存放的都是数组a从 0 到 i 的和
举个例子,如果我们要求a数组[l,r]区间内的和
我们用前缀和的思想,求[l,r]的和,就可以表示为 s[r] - s[l-1] 时间复杂度就是O(1)
二维前缀: 二维前缀对应的s[i][j]储存的是i,j及其左上角的矩阵和
公式:s[i] [j] = s[i-1] [j] + s[i] [j-1] - s[i-1] [j-1] + a[i] [j];
一维差分:
假设a[] b[] a储存原数组 b是a的差分数组
那么a数组是b数组的前缀和 ,b数组是a数组的差分数组
比如
首先对b进行初始化,即在每个(i,i) 的位置都在b[i]中插入a[i]
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c
二维差分:
对 b[i][j] 加减,是对矩阵右下角进行的加减 ,可拓展为任意一子矩阵的操作
例子:
S为差分矩阵
给以(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
双指针算法思路:
可以先想一个朴素的思路,然后利用双指针将O(n^2) 转化为O(n)
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
位运算
求n的第k位数字: n >> k & 1
//这里的第k位数字是指从右往左数第012…位数字
返回n的最后一位1:lowbit(n) = n & -n
举个例子: 10的二进制是:1010 ,则 lowbit(10) = 2 ,2对应的是二进制的10,
再比如 101000 ,对lowbit操作后, 结果为1000,也就是十进制的8
离散化:
如果我们用平时的数组哈希方法来表示该数字是否存在,如果有一个数是1e18,
那我们肯定不能开a[1e18]的数组。所以就可以用离散化的方法,将每个数字表示在对应123456…为下标的区间内,便于访问
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
}
区间合并:
习题:acwing 803区间合并
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int n;
typedef pair<int,int> p;
vector<p> s;
void merge(){
vector<p> res;
int st=-2e9,en=-2e9;
for(int i=0;i<s.size();i++){
if(en<s[i].first){
if(st!=-2e9) res.push_back({st,en});
st=s[i].first;en=s[i].second;
}
else en=max(en,s[i].second);
}
if(st!=-2e9) res.push_back({st,en});
s=res;
}
int main(){
cin>>n;
for(int i=0;i<n;i++){
int l,r; cin>>l>>r;
s.push_back({l,r});
}
sort(s.begin(),s.end());
merge();
cout<<s.size();
return 0;
}