acwing算法笔记-数据结构:哈希表,并查集,KMP

哈希表

这里我们先介绍哈希函数hash()
哈希函数的作用是将一个大值域集合 映射成 小值域集合
举个例子:
原来的集合存的是x x在-10^9 - 10^9之间,我们通过hash函数,hash(x) = k
将x映射成k, k在0-10^5之间,通过哈希函数我们将空间进行了压缩。

而哈希表是一个键值对集合,就是存储了上述的映射关系
其中key就是哈希值k,value就是元素x

然后我们一般用数组h[]模拟哈希表,下标就是key(哈希值k), h[k]是value(元素x)

哈希表里,我们很容易发现,对于相同的key,可能存在多个元素,这就是哈希冲突
怎么解决哈希冲突呢,这里常用的有两种方法:

  1. 开放寻址法:发现当前槽里有元素,就去找下一个空的槽
  2. 拉链法:在当前槽里放一个邻接表,然后插入表头

840. 模拟散列表


基本思路

拉链法或者是开放寻址法都是可以的,一般开放寻址法要好写一些

注意:
表示空节点(节点值为空),一般有两种方法

  1. 通过节点地址,常见在链表中,如果是结构体模拟,那就指针指向空,指针 = nullptr; 如果是数组模拟,那就是下标 = -1.
  2. 通过节点值,一般把节点值设为正无穷或者负无穷 常见的是0x3f3f3f3f,为什么不用INT_MAX或者INT_MIN,因为后者可能会在做一些运算后溢出

参考代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//开放寻址法
#include <cstring>
#include <iostream>

using namespace std;

const int N = 200003, null = 0x3f3f3f3f;

int h[N];

int find(int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0;
}
return t;
}

int main()
{
memset(h, 0x3f, sizeof h);

int n;
scanf("%d", &n);

while (n -- )
{
char op[2];
int x;
scanf("%s%d", op, &x);
if (*op == 'I') h[find(x)] = x;
else
{
if (h[find(x)] == null) puts("No");
else puts("Yes");
}
}

return 0;
}

//拉链法
#include <cstring>
#include <iostream>

using namespace std;

const int N = 100003;

int h[N], e[N], ne[N], idx;

void insert(int x)
{
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}

bool 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;
}

int main()
{
int n;
scanf("%d", &n);

memset(h, -1, sizeof h);

while (n -- )
{
char op[2];
int x;
scanf("%s%d", op, &x);

if (*op == 'I') insert(x);
else
{
if (find(x)) puts("Yes");
else puts("No");
}
}

return 0;
}

Reference

[1]. yxc
[2]. 理解两种哈希表的实现方法
[3]. 开放寻址法细节
[4]. STL


841. 字符串哈希


基本思路

给定一个字符串的任意两段区间,判断两个子串是否相等

如果暴力做法,m次询问,每次都需要同时扫描两个子串,时间复杂度$O(n*m)$,显然会超时。
所以这里我们利用前缀和的思想可以提前预处理这段字符串,那么对于字符串没有所谓的前缀和呢,所以我们实际处理的是前缀哈希值
这就是我们所说的字符串哈希算法,然后对于每一次询问,就可以把查询时间缩减到$O(1)$,时间复杂度就可以优化成$O(n + m)$

所以总结就是以下几个步骤

  1. 求字符串的前缀哈希值数组
  2. 根据前缀和哈希值数组得到两个子串的哈希值
  3. 比较两个子串的哈希值,如果相等,那就代表两个字符串相等

参考代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <algorithm>

using namespace std;

typedef unsigned long long ULL;

const int N = 100010, P = 131; // P进制

int n, m;
char str[N];
ULL h[N], p[N]; //h[]前缀哈希值数组 p[]存储p的幂

ULL get(int l, int r) //得到区间为[l, r]的字符串的哈希值
{
return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
scanf("%d%d", &n, &m);
scanf("%s", str + 1);

p[0] = 1; // P的0次幂为1
for (int i = 1; i <= n; i ++ ) //字符串从1开始编号
{
h[i] = h[i - 1] * P + str[i];
p[i] = p[i - 1] * P;
}

while (m -- )
{
int l1, r1, l2, r2;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);

if (get(l1, r1) == get(l2, r2)) puts("Yes");
else puts("No");
}

return 0;
}

Reference

[1]. yxc
[2]. Sean今天AC了吗
[3]. 小张同学
[4]. _不知道
[5]. 三个不好理解的地方逐一击破 + 完整注释!
[6]. 字符串哈希的简单分析与证明


并查集

并查集是一种数据结构,它可以动态维护若干个不重叠的结合,并且支持合并与查询

对于每一个集合我们可以用一棵树表示,其中root节点就代表这棵树,然后根节点的父节点是它自己。

在图论之前,数组经常是用来存一个序列,但是学习图论之后,数组经常用来存一些节点的属性,比如说e[]存节点值,ne[]存节点的next指针
p[]存节点的父节点 son[]存节点的子节点 还有很多
这里我们用p[]存储每个节点的父节点。

那么对于并查集的两个操作

  1. 合并:实际上就是把其中一个树的根节点,作为另一颗树根节点的子节点,p[x] = y
  2. 查询:就是查该节点的根节点,然后通过根节点判断属于哪个集合

836. 合并集合


基本思路

并查集模板题,对于查询操作,可以用递归查询,然后回溯的过程里可以做路径压缩

参考代码

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
28
29
30
31
32
33
34
#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int p[N];

int find(int x){ //返回x的祖先节点 + 路径压缩
//祖先节点的父节点是自己本身
if(p[x] != x){
//将x的父亲置为x父亲的父亲,实现路径的压缩
p[x] = find(p[x]);
}
return p[x];
}

int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) p[i] = i; //初始化,让数x的父节点指向自己
while(m --){
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);

if(op[0] == 'M') p[find(a)] = find(b); //将a的祖先点的父节点置为b的祖先节点
else{
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}

Reference

[1]. yxc
[2]. 图解_并查集
[3]. 理解并查集中的find函数
[4]. Bug-Free


837. 连通块中点的数量


基本思路

该题是并查集的应用,乍看像图论,其实是并查集
每一个连通块其实都是一个集合。

  1. 对于连边操作,其实就是集合间的合并。
  2. 对于查询是否在同一连通块,也就是集合的询问操作。
  3. 对于查询连通块中点的数量,也就是查询集合的大小。

因此,我们这题直接用并查集模板就可以完成了。
只是要附加一个size,在合并的时候加上即可。

参考代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int p[N], cnt[N];

int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}

int main()
{
cin >> n >> m;

for (int i = 1; i <= n; i ++ )
{
p[i] = i;
cnt[i] = 1;
}

while (m -- )
{
string op;
int a, b;
cin >> op;

if (op == "C")
{
cin >> a >> b;
a = find(a), b = find(b);
if (a != b)
{
p[a] = b;
cnt[b] += cnt[a];
}
}
else if (op == "Q1")
{
cin >> a >> b;
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
else
{
cin >> a;
cout << cnt[find(a)] << endl;
}
}

return 0;
}

Reference

[1]. yxc
[2]. 并查集_连通块中点的数量
[3]. 封禁用户



acwing算法笔记-数据结构:哈希表,并查集,KMP
https://vendestine.com/hash
Author
Wenzhe Li
Posted on
April 10, 2022
Licensed under