学习心得:
- 理解算法原理 --> 背住模板
- 默写相关模板题
- 提高熟练度:默写完删掉再写,重复3-5遍
第一章:基础算法
1. 1 快速排序(O(nlogn)
):快速排序
算法思想:分治
- 确定分界点 x = q[l + r >> 1]
- 调整区间:让第一个区间里的数小于等于x, 让第二个区间的数大于等于x
- 递归处理左右两边
代码实现:
public static void quick_sort(int[] q, int i, int j) {
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) {
int tmp = q[i];
q[i] = q[j];
q[j] = tmp;
}
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
1.2 归并排序(O(nlogn)
):归并排序
算法思想:分治
- 确定分界点 mid = r + l >> 1
- 递归处理左右两边
- 归并,将两个区间合二为一(从递归后的最顶层开始合并,每次回溯后达到排序的效果)(
O(n)
)
代码实现:
public static 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 ++]; //tmp为辅助数组,用于临时存储归并数据,也是归并排序空间复杂度主要来源
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.3 整数二分:数的范围
算法思想:
-
时时刻刻保证答案在区间内部
-
check( )
函数为满足条件部分,例如:1、2、3、3、4、5
中,求3
的起始位置,则所求位置在所有3
范围的最左侧,于是需要保证每次mid
需要在最左边一个3
或其右侧,故check( )
函数为if (q[mid] >= 3)
;若求3
的最后位置,同理check( )
函数为if (q[mid] <= 3)
。每次判断后需要调整区间端点,例如在求3
的左端点时,如果满足q[mid] >= 3
,则说明所求值在mid
左侧或就是mid
,则将区间右端点更新为mid
,即r = mid
。 -
每次二分之前需将
l、r
初始化,注意: 更新时,若r
被更新为mid - 1
,则mid
应初始化为mid = l + r + 1 >> 1
,避免更新边界后范围不变,出现死循环。
代码实现:
public static boolean check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
public static 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.4 浮点数二分:数的三次方根
算法思想:
算法原理与整数二分相似,在区间划分的时候没有整数二分的各种边界情况,一般用左右端点的差值是否小于某个值来判定是否需要继续循环。
代码实现:
public static boolean check(double x) {/* ... */} // 检查x是否满足某种性质
public static double bsearch_3(double l, double r)
{
final static double eps = 1e-8; // eps 表示精度,取决于题目对精度的要求,一般比题目保留小数位数大2
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
高精度略……
1.5 一维前缀和:前缀和
算法思想:
在输入时,顺便记录前i
项的和,i
从1 ~ n
,记为:s[i]
,当需要求第i
个数到第j
个数的和时,直接使用s[j] - s[i - 1]
,时间复杂度为O(1)
,相比于重新遍历一更快。用于求解区间和更为方便和快速。
代码实现:
s[i] = a[1] + a[2] + ... a[i] //输入时顺便求解
// 输入数组时顺带求出前缀和
for (int i = 1; i <= n; i ++){
a[i] = sc.nextInt();
if(i == 1) sum[i] = a[i];
else sum[i] = sum[i - 1] + a[i];
}
// 只需要前缀和
for (int i = 1; i <= n; i ++) s[i] = sc.nextInt();
for (int i = 1; i <= n; i ++) s[i] += s[i - 1]; //s[0] = 0
a[l] + ... + a[r] = s[r] - s[l - 1]
1.6 二维前缀和:二维前缀和
算法思想:
二维前缀和思想类似于一维前缀和,用s[i][j]
表示以(i,j)
为右下角,(0,0)
为左上角的子矩阵中所有数之和。可用于求解以(x1, y1)
为左上角,(x2, y2)
为右下角的子矩阵的和为。(该算法有容斥原理的思想)
代码实现:
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]; //求前缀和
// 求解以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和
int res = s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] +s[x1 - 1][y1 - 1];
1.7 一维差分:差分
算法思想:
实际上,差分与前缀和互为逆运算,例如,如果s[i]
为a[i]
的前缀和,那么a[i]
则称为s[i]
的差分数组,即a[i] = s[i] - s[i - 1]
,差分数组的作用在于,如果需要对一个数组中的l ~ r
区间的k
个数字均加上c
,则时间复杂度与k
相关,而对其差分数组进行操作,则只需在其差分数组的第l
项加c
,再对第r + 1
项减去c
,时间复杂度降为O(1)
。并且,差分数组的求解过程正好与对差分数组的"+ c"
、"- c"
操作相似,具体操作见代码实现。
代码实现:
// 插入操作,用于对差分数组的修改,a数组初始化为0
public static void insert(int l, int r, int c){
a[l] += c;
a[r + 1] -= c;
}
for (int i = 1; i <= n; i ++) s[i] = sc.nextInt(); //读入需要操作的原数组
/*
利用插入操作,直接求解s数组的差分数组a。
原理是在a[i]位置插入s[i]后,a[i + 1]会先减去s[i],
等到i = i + 1时,a[i + 1]的位置会加上s[i + 1],则最终a[i + 1]位置上的数
正好为s[i + 1] - s[i],由定义可知,a正好构成s的差分数组
*/
for (int i = 1; i <= n; i ++) insert(i, i, s[i]);
//该操作可能会有很多组,直接对s数组操作时间复杂度较高,则对其差分数组操作,最后统一求一次前缀和即可得到新的s数组
int l = sc.nextInt(), r = sc.nextInt(), c = sc.nextInt();
insert(l, r, c);
for (int i = i; i <= n; i ++) b[i] += b[i - 1]; //求一遍前缀和
//如果有需要可将a数组再拷回s数组,一般会直接输出a数组即为答案
1.8 二维差分:差分矩阵
算法思想:
类似于一维差分,二维差分与二维前缀和互为逆运算,例如,s[i][j]
是b[i][j]
的前缀和,那么b[i][j]
就称为s[i][j]
差分数组。二维差分数组的作用在于,如果需要对二维矩阵中以(x1, y1)
为左上角,(x2, y2)
为右下角的区域内每个数加上c
,那么可以对差分矩阵中b[x1][y1]
加上c
,对b[x2 + 1][y1]
与b[x1][y2 + 1]
减去c
,再对b[x2 + 1][y2 + 1]
减去c
。因为对b[x1][y1]
,将会导致求前缀和后,s[x1][y1]
到右下角这部分区域每个数都加上c
,因此,根据容斥原理,需对其他部分进行如上处理。同时,与一维差分类似,二维差分也不用去想如何构造差分数组,也可以利用插入操作完成。
代码实现:
// 插入操作,用于对差分数组b进行修改,同时可以用来构造差分数组b
public static 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;
}
//读入需要操作的二维数组
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
s[i][j] = sc.nextInt();
// 循环完后,b数组变为s的差分数组
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
insert(i, j, i, j, s[i][j]);
// 此操作可能有多组
int x1 = sc.nextInt(), y1 = sc.nextInt(), x2 = sc.nextInt(), y2 = sc.nextInt(), c = sc.nextInt();
insert(x1, y1, x2, y2, c);
// 对b数组求一遍前缀和
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
b[i][j] += b[i][j - 1] + b[i - 1][j] - b[i - 1][j - 1];
1.9 双指针: 最长连续不重复子序列
算法思想:
双指针算法是优化枚举最常用的算法,其核心思想在于通过找到单调性,将O(n²)
的暴力枚举转变成O(n)
的双指针算法。该算法有两类,第一类为类似于归并排序中,在两个区间上归并的操作。第二类则在一个区间上动态维护一个小区间。
代码实现:
// 第一类见归并排序
//第二类,具体见题解
for (int i = 0, j = 0; i < n; i ++){
while (j < i && check(i, j)) j ++; // j < i 也可以根据具体逻辑适当调整,check()为检验是否满足某一性质
// 具体问题的逻辑
}
最长连续不重复子序列题解:
题目描述:
给定一个长度为n
的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式:
第一行包含整数n
。
第二行包含n
个整数(均在0~10⁵
范围内),表示整数序列。
输出格式:
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。
数据范围:
1
<= n
<=10⁵
输入样例:
5
1 2 2 3 5
输出样例:
3
双指针算法O(n)
:
定义两个指针i
、j
,数组a
,i
从第一个元素开始往后走,每经过一个元素,用s
数组记录当前数字出现的次数,当走到某个元素a[i]
时,若s[a[i]] > 1
,则说明该数字前面出现过一次,此时让j
指针向后走,j
指针每经过一个元素,就让该元素出现次数就减一,当不满足s[a[i]] > 1
时,说明j
刚好经过与a[i]
重复的那个数值,则此时j
不再向后走,记录j ~ i
之间元素个数,然后i
继续向后走,重复此过程,直到走完整个数组。
import java.util.Scanner;
public class Main {
static final int N = 100010;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[n];
int[] s = new int[N]; //用于记录每位数出现的次数
for (int i = 0; i < n; i ++)
a[i] = sc.nextInt();
int res = 0;
for (int i = 0, j = 0; i < n; i ++){
s[a[i]] ++; //每次i往后走,相应的数字出现的次数就自增
while (j < i && s[a[i]] > 1){
s[a[j]] --; //让j经过的数值出现的次数减一
j ++; //j 往后走
}
res = Math.max(res, i - j + 1);
}
System.out.println(res);
}
}
1.10 位运算:二进制中1的个数
算法思想:
该部分不存在什么思想,更多的是语法,会用就行。
用法一: 求n
的二进制表示中第k
位(从右往左看,最右边为第0位)数字。原理:n >> k & 1
。
用法二:lowbit
操作,返回x
的最后一位1
,例如x = (1010)
2,则lowbit(x) = 10
;若x = (101000)
2,则lowbit(x) = 1000
,具体见代码实现。lowbit
操作是树状数组的基础。原理: 因为x & -x = x & (~x + 1)
,而x & (~x + 1)
能返回x
的最后一位1
(可以自行模拟),所以一般用x & -x
求解x
的最后一位1
。
代码实现:
// 用法一:
int res = n >> k & 1;
// 用法二:
public static int lowbit(int x){
return x & -x;
}
// 求解一个二进制数中含有多少个1的问题时,可以每次减去最后一位1(用lowbit实现),减了多少次就代表有多少个1
1.11 离散化:离散化
算法思想:
离散化的思想是将数值范围很大,但是数据量不大的一系列数映射到从0
开始的有序递增的区间,从而降低算法的时间和空间复杂度。离散化不改变数据间的相对大小,压缩数据间无用的距离。例如,1,3,200,48,67349,6546646
这一系列数的范围为1 ~ 6546646
,中间有许多无用的距离,我们将其压缩到0 ~ 5
这几个位置,此时就产生了映射关系0 -> 1, 1 -> 3, 2 -> 48, 3 -> 200, 4 -> 67349, 5 -> 6546646
。这与直接开一个数组将他们存进去再排序是有本质区别的,例如我们想对200
这个值加上50
,如果直接开数组,想要找到200
这个值,需要遍历一遍,而如果是通过离散化映射,我们可以直接利用映射关系找到在什么位置,然后直接进行操作,该步骤时间复杂度直接降低到O(1)
。在映射之前,我们需要对数据进行排序,便于后面用整数二分找到每个数对应的映射值(下标)。同时需要对数据进行去重,因为即使有两个"200"
,我们每次对200
操作时,也是在同一个位置上进行操作的,因此重复的那个200
不但没有存在的意义,反而影响在二分时寻找其映射值(下标)。
代码实现:
// 1.存储所有待离散化的值,java中可以手写PII
class PII implements Comparable<PII> {
private int x, y;
public int first() {
return x;
}
public int second() {
return y;
}
public PII(int x, int y){
this.x = x;
this.y = y;
}
public int compareTo(PII p){
return x - p.x;
}
}
// 2.将所有值排序
Arrays.sort(alls);
// 3.去除重复元素(在C ++中可使用erase + unique操作,而java中需要手动实现,实现方法利用简单的双指针算法即可)
// 4.每道题的具体逻辑
// 二分找到每个数离散化后的值
public static int find(int x) {
int l = 0, r = alls.size() - 1; //此时的alls已经排序去重
while (l < r) {
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 将离散化后的值映射到 1 ~ n,便于可能需要求前缀和的情况。
}
1.12 区间合并:区间和并
算法思想:
区间合并是将数轴上所有有交集的区间进行合并,得到没有交集的区间。例如将区间[0, 2], [3, 7], [4, 5], [7, 10], [13, 15]
合并后的区间为[0, 2], [3, 10], [13, 15]
。其思想类似于贪心,先将所有区间按照左端点进行排序,每次维护一个区间,如果枚举的区间与当前区间无交集,则将维护的区间放入答案中去,再将维护的区间更新为枚举的区间,否则将维护的区间的右端点更新为维护区间与枚举区间右端点的最大值。
代码实现:
// C ++中每个区间可以用pair存,而java中可以手写pair
class PII implements Comparable<PII> {
private int x, y;
public int first() {
return x;
}
public int second() {
return y;
}
public PII(int x, int y){
this.x = x;
this.y = y;
}
public int compareTo(PII p){
return x - p.x;
}
}
// 将所有区间合并
public static ArrayList<PII> merge(ArrayList<PII> segs) {
ArrayList<PII> res = new ArrayList();
Collections.sort(segs);
int l = -2000000000, r = -2000000000;
for (var seg : segs)
if (seg.first() > r){
if (l != -2000000000)
res.add(new PII(l, r));
l = seg.first();
r = seg.second();
}
else r = Math.max(r, seg.second());
if (l != -2000000000) res.add(new PII(l, r));
return res;
}
待续……