文章目录
堆 / 优先队列
如何手写一个堆?(使用一维数组存储)
堆的操作:
- 插入一个数
- 求集合当中的最小值
- 删除最小值
- 删除任意一个元素
- 修改任意一个元素
(
后面两个 4 和 5 是 stl 里的堆无法实现的
但是 Java 的 API 可以实现,有 remove() 方法。
)
堆 是一棵 完全二叉树 。
最后一层节点是从左到右依次排布的。
小根堆:每一个节点都是小于等于左右儿子的。
Q:如何存储堆?
A:使用一个一维数组就可以。
堆的两个基本操作 up 和 down
down(x)
和 up(x)
e.g. down(6)
在 6,3,4 里找到最小值 3,将 3 和 6 交换位置。
在 6,3,5 里找到最小值 3,将 3 和 6 交换位置。
最后变成了
e.g. up(2)
每次往上走的时候只需要和根节点比较就好了。
最后变成了
用两个基本操作实现堆⭐
堆的操作:
- 插入一个数
- 求集合当中的最小值
- 删除最小值
- 删除任意一个元素
- 修改任意一个元素
例题列表
838. 堆排序(手写堆)⭐⭐⭐ 模板(数组存储 + down操作)
https://www.acwing.com/activity/content/problem/content/888/
import java.util.Scanner;
public class Main {
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(), m = scanner.nextInt(), a, size = 0;
int[] heap = new int[n + 1];
while (n-- != 0) {
a = scanner.nextInt();
heap[size++] = a;
}
for (int i = size / 2; i >= 0; --i) down(i, heap, size); // 只需要从 size / 2 开始就可以了
while (m-- != 0) {
System.out.print(heap[0] + " ");
heap[0] = heap[--size]; // 每次将最后一个元素放在堆顶,然后down操作
down(0, heap, size);
}
}
static void down(int x, int[] heap, int size) {
// 将 x 与两个子节点进行比较
int t = x;
if (2 * x + 1 < size && heap[2 * x + 1] < heap[t]) t = 2 * x + 1;
if (2 * x + 2 < size && heap[2 * x + 2] < heap[t]) t = 2 * x + 2;
if (x != t) {
swap(heap, x, t);
down(t, heap, size);
}
}
static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
Q:为什么在初始化时从 size / 2 开始 down 就可以了?
A:
是
O
(
n
)
O(n)
O(n) 的。(用高中数列知识可以算出来是
O
(
n
)
O(n)
O(n) 的)
839. 模拟堆(手写堆 plus)
https://www.acwing.com/problem/content/841/
TODO
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int h[N], siz = 0;
int ph[N], hp[N]; // k/idx与x(堆数组的下标)的双向对应,ph是k->x,hp是x->k的。
void heap_swap(int x, int y) {
// 这三个顺序应该是无所谓的,因为反正都是交换2对2的
swap(hp[x], hp[y]);
swap(ph[hp[x]], ph[hp[y]]);
swap(h[x], h[y]);
}
void up(int x) {
while (x / 2 && h[x / 2] > h[x]) {
heap_swap(x / 2, x);
x >>= 1;
}
}
void down(int x) {
int t = x;
if (2 * x <= siz && h[2 * x] < h[t]) t = 2 * x;
if (2 * x + 1 <= siz && h[2 * x + 1] < h[t]) t = 2 * x + 1;
if (x != t) {
heap_swap(x, t);
down(t);
}
}
int main()
{
scanf("%d", &n);
string opt;
int k, x, idx = 0;
while (n--) {
cin >> opt;
if (opt == "I") {
scanf("%d", &x);
siz++;
idx++;
ph[idx] = siz;
hp[siz] = idx;
h[siz] = x;
up(siz);
} else if (opt == "PM") {
printf("%d\n", h[1]);
} else if (opt == "DM") {
heap_swap(1, siz);
siz--;
down(1);
} else if (opt == "D") {
scanf("%d", &k);
k = ph[k];
heap_swap(k, siz);
siz--;
up(k);
down(k);
} else {
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
down(k);
up(k);
}
}
return 0;
}
哈希表
https://oi-wiki.org/ds/hash/ (写的很好)
例题列表
840. 模拟散列表
https://www.acwing.com/problem/content/842/
开放寻址法 和 拉链法 都很不错。
解法1——开放寻址法
只需要开一个一维数组,不需要再开链表。(经验上,数组要开到数据范围的2~3倍)
import java.util.Arrays;
import java.util.Scanner;
// 开放寻址法
public class Main {
final static int N = 200003, inf = 0x3f3f3f3f;
static int[] h = new int[N];
static {
Arrays.fill(h, inf); // 设置inf表示这个位置没有元素
}
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(), N = 100001;
while (n-- != 0) {
char op = scanner.next().charAt(0);
int x = scanner.nextInt();
if (op == 'I') h[find(x)] = x;
else System.out.println(h[find(x)] != inf? "Yes": "No");
}
}
// 找到 x 在的位置
static int find(int x) {
int t = (x % N + N) % N; // 哈希函数映射
while (h[t] != inf && h[t] != x) t = (t + 1) % N; // 这个位置有元素而且不是 x
return t;
}
}
开放寻址法的核心是 find(x)
方法。即找到元素 x 所在的位置。
解法2——拉链法
下面的写法相当于,用数组模拟哈希表。
对于哈希表数组的每个位置,使用数组模拟了一个单链表。
在插入时使用头插法。
import java.util.Arrays;
import java.util.Scanner;
// 拉链法
public class Main {
final static int N = 100003; // 大于100000的第一个质数(尽量取和2的幂次比较远的质数)
// h存储当前位置的链表头,e存储各个idx对应的元素x,ne存储各个idx的指针指向的idx
static int[] h = new int[N], e = new int[N], ne = new int[N];
static int idx;
static {
Arrays.fill(h, -1);
}
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(), N = 100001;
while (n-- != 0) {
char op = scanner.next().charAt(0);
int x = scanner.nextInt();
if (op == 'I') insert(x);
else System.out.println(find(x)? "Yes": "No");
}
}
static void insert(int x) {
int k = (x % N + N) % N; // x 可能是负数,所以 + N
e[idx] = x; // 把数据 x 存储 e 数组中
ne[idx] = h[k]; // 把 idx 的 next 指针指向当前的插槽 h[k]对应的idx(头插法)
h[k] = idx++; // 把插槽 h[k] 的头节点换成 idx
}
static boolean find(int x) {
int k = (x % N + N) % N; // 找到插槽位置
for (int i = h[k]; i != -1; i = ne[i]) { // 枚举这个插槽上的链
if (e[i] == x) return true;
}
return false;
}
}
在 insert(x)
中使用头插法。
在 find(x)
中枚举当前插槽上的链表中的所有元素,看是否有元素 x。
841. 字符串哈希(字符串前缀哈希法)(P进制表示字符串)
https://www.acwing.com/problem/content/843/
解法思路
预处理出所有前缀的哈希
利用前缀哈希可以求得所有子串的哈希。
注意点
-
不能把某个数字映射成 0。(比如 a 映射成 0, 那 aa 还是 0)。
-
当 P = 131 或 13331 ,Q 取 2^64 时,一般情况下(99.9%的情况下)可以假定不会发生冲突。(P 是进制,Q 是取模数)
代码
import java.util.Scanner;
public class Main {
final static int N = 100010, P = 131; // 把字符串当成P进制的数字
static long[] h = new long[N], p = new long[N]; // 溢出就相当于取模了(ULL)
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(), m = scanner.nextInt();
String s = scanner.next();
// 左边是高位,右边是低位
p[0] = 1;
for (int i = 1; i <= n; ++i) {
h[i] = h[i - 1] * P + s.charAt(i - 1);
p[i] = p[i - 1] * P;
}
while (m-- != 0) {
int l1 = scanner.nextInt(), r1 = scanner.nextInt(), l2 = scanner.nextInt(), r2 = scanner.nextInt();
System.out.println(get(l1, r1) == get(l2, r2)? "Yes": "No");
}
}
// 计算出从 l 到 r 的哈希值
static long get(int l, int r) {
return h[r] - h[l - 1] * p[r - l + 1]; // 公式
}
}
关于h[r] - h[l - 1] * p[r - l + 1]
h
[
l
]
h[l]
h[l] 中的 0 ~ l 相比于
h
[
r
]
h[r]
h[r] 中的 0 ~ l 少乘了
p
r
−
l
+
1
p^{r - l + 1}
pr−l+1 ,所以相乘之后再减去就相当于去掉了 从 0 ~ r 中的 0 ~ l 这一段。