一、排序
二、二分
三、高精度
四、前缀和与差分
五、位运算
六、双指针算法
七、离散化
八、区间合并
一、排序
(一)快速排序算法:
由冒泡排序改进,在冒泡排序过程中,只对相邻的两个记录进行比较,每次交换只能消除一个逆序;而快速排序时对不一定相邻的两个记录进行比较,每次交换可以消除多个逆序。
快速排序是不稳定的。
时间复杂度:
最差:O(n^2);
平均:O ( n l o g 2 n ) 。
1.确定分界点 q[l] , q[(l+r)/2] , q[r]; //随机选取
2.调整区间,使小于等于x的在左边 l,大于等于x 的在右边 r;
3.递归处理x的左右段;
定义指针 i ,j 使得 i, j 都向中间读取值,当 i 的读取值大于x,并且 j 的的值小于x,交换两个数的值,直到 i, j 两个数相等。
如果left>=right,则退出;
在数组中随即确定一个分界点x,假设x=(left+right)/2;
设指针i,j,分别指向left,right;
当i<j时,循环执行5,6,7步;
如果i指针所指向的数小于x,则i++;否则,跳出循环;
如果j指针所指向的数大于x,则j--;否则,跳出循环;
如果i<j,则swap(arr[i],arr[j]);
调整区间,递归处理左右两段,quick_sort(arr,left,i);quick_sort(arr,i+1,right);
模板 :
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);
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.确定分界点,(r+l)/2=mid;
2.递归排序左右两边; //两个有序的序列
3.归并,合二为一;
归并排序是稳定的。
时间复杂度:O ( n l o g 2 n ) 。
分成两段,比较min1,min2,去更小值min3,放到一个新的序列中,指针依次往后走,直到一个序列排完,然后将剩余的序列放到后面, 依次进行。
模板 :
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];
}
二、二分:
- 首先对数组进行排序。
- 将要查找值x与数组中间值mid相比较;
- 如果
x=mid
,则找到x,退出程序; - 如果
x<mid
,则将边界设为[left,mid]; - 如果
x>mid
,则将边界设为[mid+1,right]; - 循环执行4,5两步骤,若直到数组长度为1,还没找打x,则数组中不存在x。
整数二分算法:
选择答案所在区域,确定使用哪个模板
模板:
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.保留小数问题的情况下,往后提2个精度
2.一般情况下,可以用for(int i=0;i<100;i++)代替;
模板:
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.用字符串读入两个大整数a,b;
2.用vector<int> 分别倒序存储a,b,得到A,B,设和数向量C;
3.如果是减法则执行步骤6;
4.设中间变量t来存储进位,t分别加A[i],B[i],将t%10 push到C,令t/=10;
5.循环执行4,直到i大于A,B两向量的长度。
6.首先根据A,B的长度,高位的值比较大小;
7.设中间变量t来存储进位,A[i]-B[i]-t,将(t+10)%10 push到C,若t<0,则令t=1,否则令t=0;
乘除法:
1.用字符串读入大整数a,整型读入整数b;
2.用vector<int> 倒序存储a,得到A,设向量C;
3.如果是除法则执行步骤6;
4.设中间变量t来存储进位,t加A[i]*b,将t%10 push到C,令t/=10;
5.循环执行4,直到i大于A的长度。
6.设余数r,r=r*10+A[i],将r/bpush到C,令r%=b;
7.将C中的值反转,去除前置0。
高精度的常见场景:
两个比较大的整数相加 A+B,位数约为10的6次方
两个比较大的整数相减 A+B,位数约为10的6次方
一个大整数乘一个小整数 A*a,A的位数<=10的6次方,a的数值<=10的9次方
一个大整数除以一个小整数 A/a
大整数是如何存储的呢?其实是把大整数的每一位存入数组里面。
数组下标为0的位置存储最低位。
为什么?
因为存在进位的可能,那么就需要在最高位补上一个数。而在数组的最末尾是最容易增加数据的。(push_back)
高精度加法
运算是模拟人工加法的过程。
先相加个位,得出结果和进位。
用代码模拟:Ai+Bi+进位t(0或者1)
(1.)存储数字:123456789
数组:0 1 2 3 4 5 6 7 8
9 8 7 6 5 4 3 2 1 //位数倒存
(2.) 不存在A或B时,将其当作0.
将 t 当作进位
高精度加法 模板:
// C = A + B, A >= 0, B >= 0
vector<int> add(vector<int> &A, vector<int> &B) //数组A 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;
}
高精度的输入方法:
int main()
{
string a,b;
vector <int >A.B;
cin>>a>>b; //输入“123456”
for(int i=a.size()-1;i>-0;i--) A.push_back(a[i]-'0')
for(int i=b.size()-1;i>-0;i--) B.push_back(a[i]-'0')//遍历之后的A={6,5,4,3,2,1};
auto C=add(A,B); // auto 自行判断数据的类型,类似于vector<int>
for(int i=c.size()-1;i>-0;i--) printf("%d",c[i]);
return 0;
}
高精度减法
如果一个位够减就直接减
低位如果不够减,就需要向高位借位
模板:
// 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 用来借位
{
t = A[i] - t;
if (i < B.size()) t -= B[i]; //判断B是否有这一位
C.push_back((t + 10) % 10);
if (t < 0) t = 1;
else t = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back(); // 去掉结果前缀和前面的0
return C;
}
判断计算c-a-b时要先判断是否有a>=b
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 ture;
}
if(cmp(A,B))
{
auto c=sub(A,B)
for(int i=c.siz()-1;i>=0;i--) printf("%d",c[i]);
}
else
{
auto c=sub(B,A);
printf("-");
for(int i=c.size()-1;i.=0;i--)
printf("%d",c[i]);
}
return 0;
高精度乘低精度
模板:
// 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(); //去掉前缀和中的0
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;
}
四、前缀和与差分
(一)前缀
一维前缀和
作用:
能快速地求出原数组里一段数的和。可以只用一次运算就求出来任意区间的一段和。
前缀和可以快速地求出原数组里从l到r,即区间[l,r]这一段数的和 。
如果没有前缀和数组,就要循环一遍,时间复杂度为O(n);如果有前缀和数组,只需要s[r]-s[l-1],时间复杂度为O(1)
思想:
- 前缀和数组,原数组的下标均由1开始。
- 求前缀和:将S[0]置0,
S[i]=S[i-1]+a[i]
。 - 区间[l,r]的数组的和等于
S[r]-S[l-1]
。
模板:
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
二维前缀和
- 先求前缀和:
- 求子矩阵的和:
模板:
S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
(二)差分:
若a[n]是b[n]的前缀和,则b[n]是a[n]的差分。
使区间[l,r]中的每一个a[i]+c,则只需b[l]+c
,b[r+1]-c
。
思想:
- 假设a数组初始值全为0,则b数组的值也全为0。
- 将a[i]的值插入a数组,则可以理解为在区间[i,i]中a[i]+c,那么需对
b[i]+a[i],b[i+1]-a[i]
。 - 最终对b[i]求前缀和即可。
原数组:
a1,a2,a3,a4,a5,a6.......an;
构造数组:
b1,b2,b3,b4,b5,b6......bn;
使得:
an=b1+b2+b3+b4+......bn;
b称为a的差分,a称为b的前缀和;
b1=a1;
b2=a2-a1;
b3=a3-a2;
.
bn=an-an-1;
区间[l,r], al后面到ar 加上c,ar后面的不加c;
一维差分
模板:
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c
二维差分
模板:
给以(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
位运算(二进制中)
- 求n的二进制表示中第k+1位是几,n>>k&1。
- 返回x的最后一位1是多少,x&-x,x=1010,lowbit(x)=2。(求x的二进制中多少个1)
把k位移到最后一位 n>>k
看个位是多少 x&1
求n的第k位数字: n >> k & 1
返回n的最后一位1:lowbit(n) = n & -n
双指针算法
- 设一个计数数组S[n],记录在[i,j]区间内数每个数出现的次数。
- i每向右移一位,S[a[i]]++;j向右移之前,先S[a[j]]–。
- 记录不重复子数组的最长长度。
模板:
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
离散化
- 用到前缀和求区间内值的和。
- 去重:先将数组进行排序,利用unique,erase去重。
- 二分查找。
常见问题:
1.a[]中可能有重复元素;
2.如何算出a[]离散后的值;
模板:
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
}
在x 的下标加上c
vector<PII>add,query;
add.push_back({x,c});
区间合并
- 按照区间左端点排序。
- 初始化左st右ed边界为-2e9。
- 比较ed与当前所在区间左端点,更新区间,并将更新后的区间记录下来。
- 返回数组长度。
模板:
// 将所有存在交集的区间合并
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;
}