本博文主要是记录acwing算法基础课第一章基础算法和其模板的相关内容,记录下来供以后复习。
文章目录
要求:
- 把算法的思想搞懂。
- 课后把代码模板背过,能够达到快速默写出来、调试通过就好了。
- 每一个模板重复写几遍。
1、排序
1.1、 快速排序——分治
思路:
- 确定分界点:q[i]、q[(i+r)/2]、q[r]
- 调整区间:分界点左边是小于x的,分界点右边是大于x的。
- 递归处理左右两段。
如何优雅的将数组一分为2
- 左右指针分别交换来替换。
- 分割点x选第一个数
注意点:
- 注意边界问题。
- 这个模板背就完事了
题目:
import java.util.Scanner;
import java.io.BufferedInputStream;
public class Main{
public static void main(String[] args){
Scanner in = new Scanner(new BufferedInputStream(System.in));
int n = in.nextInt();
int[] arr = new int[n];
for(int i = 0; i < n; i++){
arr[i] = in.nextInt();
}
quickSort(arr, 0, n - 1);
for(int i = 0; i < n; i++){
System.out.print(arr[i] + " ");
}
}
public static void quickSort(int[] arr, int l, int r){
if(l>=r) return;
int i = l - 1; //这里很容易出错,因为是先加和先减的,所以要在左右边边界分别加减1.
int j = r + 1;
int x = arr[l];
while(i < j){
do i++; while(arr[i]<x);
do j--; while(arr[j]>x);
if(i < j){
int a = arr[i];
arr[i] = arr[j];
arr[j] = a;
}
}
quickSort(arr, l, j);
quickSort(arr, j + 1, r);
}
}
1.2、 归并排序——分治
快排是以一个数来分,归并排序是以中间的一个数来分。
思想:
-
找分界点,mid = (l+r)/2
-
递归排序
-
归并——合二为一(重点)
两个排好序的,顺序取最小值,合并为一个数组。
题目:
解法:
import java.util.Scanner;
import java.io.BufferedInputStream;
public class Main{
public static void main(String[] args) {
Scanner in = new Scanner(new BufferedInputStream(System.in));
int n = in.nextInt();
int[] arr = new int[n];
for(int i = 0; i < n; i++){
arr[i] = in.nextInt();
}
mergeSort(arr, 0, n-1);
for(int i = 0; i < n; i++){
System.out.print(arr[i] + " ");
}
}
public static void mergeSort(int[] arr, int l, int r){
if(l >= r) return;
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
int[] tmp = new int[r - l + 1];
int k = 0, i = l, j = mid + 1;
while( i <= mid && j <= r){
if(arr[i] <= arr[j])
tmp[k++] = arr[i++];
else
tmp[k++] = arr[j++];
}
while(i <= mid) tmp[k++] = arr[i++];
while(j <= r) tmp[k++] = arr[j++];
for(i = l, j = 0; i <= r; i++, j++)
arr[i] = tmp[j];
}
}
2、二分
2.2、 整数二分
模板:
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;
}
作者:yxc
链接:https://www.acwing.com/blog/content/277/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
有单调性一定可以二分,没有单调性也可能能够二分。二分的本质是:边界中一分为二,一个半边是满足这个性质,一个半边不满足。
为了更好的处理边界问题我们引入了模板,但是该如何选择用哪个模板呢?可以用下面这个图来理解。
显然,如果查找数字11,可能出现11的地方是一个区间,如果我们需要查找这个区间的左端点,如橙色箭头所示,当我们选取到Array[mid]的时候,11是会落在左区间的的,因此选用第一个模板区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用
。当我们要查找区间的右端点的时候,如蓝色箭头所示,就会用到第二个模板。这里需要自己体会一下。
模板1与模板2的区别在于:模板2取mid的时候加上了1.
题目:
import java.util.Scanner;
import java.io.BufferedInputStream;
public class Main{
public static void main(String[] args){
Scanner in = new Scanner(new BufferedInputStream(System.in));
int n = in.nextInt();
int m = in.nextInt();
int[] arr = new int[n];
for(int i = 0; i < n; i++){
arr[i] = in.nextInt();
}
while(m-->0){
int x = in.nextInt();
int l = 0, r = n - 1;
while(l < r){
int mid = l + ((r - l) >> 1);
if(arr[mid]>=x) r = mid;//想想往左边区间找还是往右边区间找这个数
else l = mid + 1;
}
if(arr[l] != x){
System.out.println("-1 -1");
}else{
String ans = l + " ";
l = 0;
r = n - 1;
while(l < r){
int mid = l + ((r - l + 1) >> 1);
if(arr[mid]<=x)l = mid;
else r = mid - 1;
}
System.out.println(ans + l);
}
}
}
}
2.3、 小数二分(浮点数二分)
求根号x
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
double n = in.nextDouble();
double l = 0, r = Math.abs(n); // 考虑 n为负数的情况
while (r - l > 1e-8) { // // 精度比所求精度高 2位
double mid = (l + r) / 2;
if (mid * mid * mid >= Math.abs(n)) // 不需要考虑边界问题
r = mid;
else
l = mid;
}
if (n >= 0)
System.out.println(String.format("%.6f", l)); // 保留 6位小数
else
System.out.println("-" + String.format("%.6f", l));
}
}
3、高精度
java和python同学没有必要学这个。先不学。
- 两个大的整数相加
- 两个大整数相减
- 大整数*小整数
- 大整数除整数
4、前缀和与差分
前缀和的作用是:能快速的算出数组的每一部分的连续和。
让S0=0
有两个优点:
- 生成前缀和很方便
- 计算i+~-j很方便。
Si-Sj-1
模板:
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
其实是一个公式和一个思想,算法比较简单。
5、双指针算法
归并排序的时候需要将两个子序列合并的时候用的是双指针算法。
核心思想是:
- 暴力写法是
O(n^2)
的。但是双指针优化到O(n)
,运用了某些性质
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
一个没有重复的连续子序列内部都是没有重复元素的,所以j指向的只能右移,所以只要枚举i就可以了。
数据很大的时候可以用hashmap来做。不开数组来判重的方法就是hashmap
import java.util.Scanner;
import java.util.*;
//为啥能够用maps来判断重复呢?i走过了一遍,
//知道重复了多少次,j是从左边走过来,直到新加进来的不重复就是
//原来的本身就不重复,但新进来的是不是重复呢?就看j的移动是不是能够让它降到1
//此时说明j走到了一个合适的位置。
public class Main{
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] arr = new int[n];
for(int i = 0; i < n; i++){
arr[i] = in.nextInt();
}
int ans = 0;
Map<Integer, Integer> maps = new HashMap<Integer, Integer>();
for(int i = 0,j = 0; i < n; i++){
maps.put(arr[i], maps.getOrDefault(arr[i], 0) + 1);
while(maps.get(arr[i])>1){
maps.put(arr[j], maps.getOrDefault(arr[j], 1) - 1);
j++;
}
ans = Math.max(ans, i - j + 1);
}
System.out.println(ans);
}
}
import java.util.Scanner;
public class Main{
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int[] a = new int[n];
int[] b = new int[m];
for(int i = 0 ; i < n; i++){
a[i] = in.nextInt();
}
for(int j = 0 ; j < m; j++){
b[j] = in.nextInt();
}
String ans = "No";
for(int i = 0, j = 0; j < m; j++){
int tmp = b[j];
if(tmp == a[i] && i < n){
i++;
}
if(i == n){
ans = "Yes";
break;
}
}
System.out.println(ans);
}
}
import java.util.Scanner;
public class Main{
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int x = in.nextInt();
//System.out.println(n + " " + m);
int[] a = new int[n];
int[] b = new int[m];
for(int i = 0; i < n; i++){
a[i] = in.nextInt();
}
for(int j = 0; j < m; j++){
b[j] = in.nextInt();
}
int ii = 0, jj = 0;
for(int i = 0, j = m - 1; i < n; i++){
int sum = a[i] + b[j];
//System.out.println(sum + " ");
while(sum >= x){
j--;
sum = a[i] + b[j];
if(sum == x){
ii = i;
jj = j;
break;
}
System.out.println(sum + " ");
}
}
System.out.println(ii + " " + jj);
}
}
6、位运算
几种常用的位运算的操作:
求n的第k位数字: n >> k & 1
返回n的最后一位1:lowbit(n) = n & -n
主要是为了解决两类问题:
-
n的2进制表示中第k位是几?
基本思路:先把第k位,移到个位(右移);再看一下个位是几
-
lowbit:返回x的最后一位1。举个例子?
1010 -> 10 101000 -> 1000
表达形式是
x&-x
为啥可以这么做呢?-x的二进制表示与x取反加1的表示是一样的,取反之后,1变为0,加完1之后,那个0之前的就不会进位l,0变为1了,之后的0取反之后变为1,加1之后变为0,取&之后就只留下了第一个1
import java.util.Scanner;
public class Main{
public static void main(String[] args){
var in = new Scanner(System.in);
int n = in.nextInt();
for(int i = 0 ; i < n; i++){
int x = in.nextInt();
int count = 0;
while(x>0){
x = x - lowbit(x);
count++;
}
System.out.print(count + " ");
}
}
private static int lowbit(int x){
return x & -x;
}
}
7、离散化
整数有序的离散化:
一个映射的概念。需要注意两个问题:
- 去重(存在重复元素的时候咋办)(:
- 快速的映射(如何算出a[i]离散化之后的值)(二分):
值域很大,但是很稀疏,可以用离散化来做。
用到过的数映射为从1开始的自然数就可以了。再用前缀和来解题。java使用ArrayList来实现,但是unit函数需要自己实现。java可以用集合去重?
排序之后去重用的是双指针算法
import java.util.*;
class Pair{
int first;
int second;
public Pair(int first, int second){
this.first = first;
this.second = second;
}
}
public class Main{
public static void main(String[] args){
var in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
List<Integer> alls = new ArrayList<Integer>();
List<Pair> add = new ArrayList<>();
List<Pair> query = new ArrayList<>();
int[] ori = new int[300010];
int[] sum = new int[300010];
//读取数据
for(int i = 0; i < n; i++){
int a = in.nextInt();
int b = in.nextInt();
add.add(new Pair(a, b));
alls.add(a);
}
for(int j = 0; j < m; j++){
int l = in.nextInt();
int r = in.nextInt();
query.add(new Pair(l, r));
alls.add(l);
alls.add(r);
}
//排序出重。为啥这么做呢?是需要做前缀和。
Collections.sort(alls);
int unique = unique(alls);
alls = alls.subList(0, unique);
for(Pair item : add){
int tag = find(item.first, alls);
ori[tag] += item.second;
}
//前缀和
for(int i = 1; i <= alls.size(); i++) sum[i] = sum[i - 1] + ori[i];
for(Pair item : query){
int l = find(item.first, alls);
int r = find(item.second, alls);
System.out.println(sum[r] - sum[l - 1]);
}
}
public static int unique(List<Integer> list){
int j = 0;
for(int i = 0; i < list.size(); i++){
if(i == 0 || list.get(i) != list.get(i - 1)){
list.set(j, list.get(i));
j++;
}
}
return j;
}
public static int find(int x, List<Integer> list){
int l = 0;
int r = list.size() - 1;
while(l < r){
int mid = l + r >> 1;
if(list.get(mid) >= x){
r = mid;
}else{
l = mid + 1;
}
}
return l + 1;
}
}
8、区间合并
用的情况没有那么多,但是也会用到:
- 区间左端点排序
- 扫描整个区间,维护一个当前的区间进行合并
import java.util.*;
class Pair implements Comparable<Pair>{
int first;
int second;
public Pair(int a, int b){
this.first = a;
this.second = b;
}
@Override
public int compareTo(Pair other){
if(this.first < other.first){
return -1;
}else{
return 1;
}
}
}
public class Main{
public static void main(String[] args){
var in = new Scanner(System.in);
int n = in.nextInt();
List<Pair> a = new ArrayList<>();
for(int i = 0; i < n; i++){
a.add(new Pair(in.nextInt(), in.nextInt()));
}
Collections.sort(a);
/*
for(Pair item : a){
System.out.println(item.first + "," + item.second);
}
*/
int count = 0;
int lmax = Integer.MIN_VALUE;
for(int i = 0; i < a.size(); i++){
Pair pair = a.get(i);
if(pair.first > lmax){
count++;
}
lmax = Math.max(lmax, pair.second);
}
System.out.println(count);
}
}
总结
写的比较匆忙,推荐acwing这个平台吧。记录是为了更好的学习融会贯通。