template:base
快排
一般使用c++
stl即可,同时可以使用自定义cmp函数。std::sort
在最坏情况下保持O(nlogn)时间复杂度,主流编译器采用introsort
(快速排序+堆排序混合算法)实现
1 |
|
归并排序
所谓归并,先递归,再合并。递归的解决两个子数组的排序的子问题,再合并出最终答案。
每一层需要合并n个元素。比如,第一层是合并两个n/2的数组,总共有n个元素。不管层数如何,每一层的合并操作都是O(n)的时间。而总共有logn层,所以总的时间复杂度应该是O(nlogn)
1 |
|
归并排序的经典应用是求给定数组中数组逆序对的个数
采用归并的思想, 注意到逆序对可以分成三类:
- 两个元素都在左边;
- 两个元素都在右边;
- 两个元素一个在左一个在右;
归的过程依然包含了前两种逆序对
因此我们在并的过程中考虑两个元素一个在左一个在右这一情况贡献的新逆序对即可
注意我们在统计逆序对的过程中要同时进行排序,这样做的好处是保持左右子数组有序的同时,可以快速判断剩余元素的逆序关系:当 a[i] > a[j] 时,逆序对增量= mid − i + 1.
1 |
|
逆序对可以做很多事,比如最少交换次数问题:
超市货架上商品当前排列顺序为 [5,3,2,4,1](商品ID),目标顺序为 [1,2,3,4,5]。每次只能交换相邻商品,求最小交换次数?
因为每次交换相邻元素最多减少1个逆序对,所以此问题只需要求出逆序对个数即可。
二分
二分法是基于解空间单调性提出的经典算法
二分是重要且朴素的思想,大致分为浮点二分和整数二分问题
浮点二分很好写,不断判断l和r谁更新为mid即可
1
2
3
4
5
6
7
8
9
10
11
12
13//求三次方根
double b_search(double n)
{
double l=-10000,r=10000,mid=0;
while((r-l)>1e-8)
{
double mid =(l+r)/2;
if(mid*mid*mid>n) r=mid;
else l=mid;
}
return r;
}
整数二分需要考虑边界问题,对应有两种模版在下面写出。笔者认为采用一种名为”自然写法的”编码方式逻辑更清楚。所谓自然写法,意指写好check函数后自然地判断其后跟随的更新法则,根据r = mid还是l = mid,判断下一句是l = mid + 1亦或r = mid − 1,这种逻辑上的必然性是由check
函数必须是闭集合的约定带来的。这样做有三个好处。
- 递归结束后必然有l = r = mid
- 两种不同写法都可找到满足题目需求的解,同时使用两种模版可以解决一种区间问题(见其后的例题)
- 只需要记忆:当
check
后面跟的是l=mid
,则前面mid的更新需要+1变为(mid = (l+r+1)>>1
),如此便可以完全防止死循环!
1 |
|
考虑一道经典的问题
给定一个按照升序排列的长度为 n的整数数组,以及 q个查询。 对于每个查询,返回一个元素 k的起始位置和终止位置(位置从 0开始计数)。 如果数组中不存在该元素,则返回 -1 -1。
输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n个整数(均在 1∼10000范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k,表示一个询问元素。
输出格式
共 q行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1
一般模版
1 |
|
前缀与差分
不用刻意背,作为一种思路可以写出来,记住要写前缀或者差分的时候,下标从1开始
一维前缀和 —— 模板题 AcWing 795. 前缀和
1 |
|
二维前缀和 —— 模板题 AcWing 796. 子矩阵的和
1 |
|
一维差分 —— 模板题 AcWing 797. 差分
1 |
|
二维差分 —— 模板题 AcWing 798. 差分矩阵
1 |
|
位运算
1 |
|
双指针算法
双指针算法的本质是利用了问题的某种单调性,使得两个指针仅有可能单向移动,对大循环搜索进行了优化,将时间复杂度从O(n2)优化至O(n)
我们需要考虑的是,如何移动指针,可以利用上问题的某种单调性。写法的基本框架就是在某一个指针的大循环下写另一个指针的while移动
1
2
3
4
5
6for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
给定两个升序排序的有序数组A和B,以及一个目标值x。
数组下标从 0 开始。
请你求出满足A[i] + B[j] = x的数对(i, j)。
(保证问题只有一个解)
基本思路为:维护两个指针i,j,分别用于正序,倒序遍历a,b。外层大循环递增i,对于当前的i判断a[i]+b[j]
与x
的关系,当a[i]+b[j])>x
时,不断递减j直到a[i]+b[j])<=x
,此时判断:若a[i]+b[j])==x
则答案已经找到。若a[i]+b[j])<x
则说明当下的i没有对应的j组成答案,需要对下一个i进行判断。
问题的单调性在于对于之前的i抛弃的j,新递增出来的i也依然用不上这些j。这是升序数组带来的显然结果。因此我们可以保证,i和j只需要单向移动。
1 |
|
另一类问题是用两个指针维护某一个区间,比如经典的求最长不重复子序列的长度问题
基本思路
我们需要维护一个状态数组s[N]
表示当前维护的区间里某个数出现了多少次
维护两个指针i,j分别代表当前区间的左右端点。初始i=0,j=0。令j一次一次的往外扩展,每一次扩展都要判断扩展后区间是否有重复,如果有,则令i向前扩展,直到追上j或者把重复的元素排出了当前区间为止。
重复上述操作,循环内不断更新最大数组长度res
本问题中我们使用到的单调性来源于连续不重复这一约定,这致使i,j有且仅有可能单向向左移动。
1 |
|
离散化(坐标映射)
离散化的思想是映射,把大数映射到小数,无穷映射到有穷,笔者比较喜欢直接使用c++stl
的hash
来做
看一道题
假定有一个无限长的数轴,数轴上每个坐标上的数都是0。 现在,我们首先进行n次操作,每次操作将某一位置x上的数加c。 接下来,进行m次询问,每个询问包含两个整数l和r,你需要求出在区间[l, r]之间的所有数的和
输入格式
第一行包含两个整数n和m。 接下来n行,每行包含两个整数x和c。 再接下来m行,每行包含两个整数l和r。 输出格式 共m行,每行输出一个询问中所求的区间内数字和。
解答:
把所有坐标直接存下来?太大,不现实。我们事实上用到的坐标是什么?其实是每一组r,l,x
因此把所有的r,l,x都存到一个数组里面按升序排列好,取数组的脚标为新的坐标并排列好,原来的r,l处赋值为0,x处赋值+=c,求前缀和即可
有了思路,想想我们要开多少vector或者hashmap来存储需要的数据与关系?
unordered_map<int,int> vtoi
记录大坐标与小坐标的映射关系
1 |
|
1 |
|
区间合并
给n个区间,求n个区间合并之后的区间个数?
基本思路,先cin所有区间,利用sort优先左端点排序。
然后定义ed为我们此时维护的区间的末尾,每次如果ed大于a[i].first,则此ai可以合并,更新ed
否则,此ai不可合并,更新ed并ans++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5+10;
typedef pair<int,int> pii;
pii a[N];
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++) scanf("%d%d",&a[i].first,&a[i].second);
sort(a,a+n);
int res=0;
int l=a[0].first,r=a[0].second;
for(int i=1;i<n;i++)
{
if(a[i].first>r)
{
res++;
l = a[i].first;
r = a[i].second;
}
r=max(r,a[i].second);
}
res++;
cout<<res;
}