力扣刷题笔记 #03 数据结构

本文最后更新于:2023年3月1日 晚上

本文包含「数据结构」类型题中的:线段树、树状数组、并查集等。持续更新中。

题目描述

方法1

方法2

方法3

坑点

线段树

将区间 \([1,n]\) 逐层二分成子区间(总数不超过 \(4n\)),从而实现对区间 \([L,R]\)\(O(\log n)\) 的单点修改、区间修改、区间和查询、区间最值查询等操作。

懒加载用于多次更新,少量查询时,节约时间。

线段树单点修改,区间求和模板:

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
const int N = 1e5;
struct node { //区间 [l,r] 的和为sum
int l, r, sum;
};
vector<node> tree(N);
vector<int> nums;
/* 递归建树
每次将区间[l,r]从中划分为两段
id的左孩子为2*id指向左半区间
id的右孩子为2*id+1指向右半区间
当l==r时到达叶子节点,返回即可
id:当前节点
l:区间左边界
r:区间右边界
*/
void build(int id, int l, int r){
tree[id].l = l;
tree[id].r = r;
if(l == r){
tree[id].sum = nums[l];
return;
}
int mid = (l + r) >> 1;
build(2 * id, l, mid);
build(2 * id + 1, mid + 1, r);
tree[id].sum = tree[2 * id].sum + tree[2 * id + 1].sum;
return;
}
/* 区间查询:查询给定区间 [l,r] 的和
从根节点开始查询,节点区间与给定区间之间存在四种情况
1.节点区间完全包含于给定区间,此时查询结果加上节点区间和,直接返回
2.节点区间与给定区间完全不相交,此时直接返回0
3.节点区间的左儿子与给定区间相交,此时递归查询左儿子
4.节点区间的右儿子与给定区间相交,此时递归查询右儿子
id:当前节点
l:区间左边界
r:区间右边界
*/
int search(int id, int l, int r){
if(tree[id].l >= l && tree[id].r <= r)
return tree[id].sum;
if(tree[id].r < l || tree[id].l > r)
return 0;
int s = 0;
if(tree[2 * id].r >= l)
s += search(2 * id, l, r);
if(tree[2 * id + 1].l <= r)
s += search(2 * id + 1, l, r);
return s;
}
/* 单点修改
一直搜索到叶子节点,首先修改叶子结点的sum,然后逐层返回,修改对应的sum
id:当前节点
i:要修改的节点
k:修改的值
*/
void change(int id, int i, int k){
if(tree[id].l == i && tree[id].r == i){
tree[id].sum = k;
return;
}
int mid = (tree[id].l + tree[id].r) >> 1;
if(i <= mid)
change(2 * id, i, k);
else
change(2 * id + 1, i, k);
tree[id].sum = tree[2 * id].sum + tree[2 * id + 1].sum;
return;
}

NumArray(vector<int>& num) {
nums = num;
build(1, 0, num.size() - 1);
}
void update(int index, int val) {
change(1, index, val);
}
int sumRange(int left, int right) {
return search(1, left, right);
}

307. 区域和检索 - 数组可修改 (H)

题目描述:给定一个数组,要求实现两个操作:单点更新、区间和查询。

方法1:分块,

方法2:线段树,

方法3:树状数组,

2407. 最长递增子序列 II (H)

题目描述:一个整数数组 nums,找到其中最长严格递增子序列的长度,要求子序列中相邻元素的差值不超过 k

方法1:DP,定义 \(dp[i]\)\(nums[i]\) 结尾的 LIS 长度,每次从 \(dp[j]\) 中找满足条件的最大值转移,复杂度 \(O(n^2)\)\[ dp[i]=\max (dp[j])+1 \text {, 其中 } 0 \leq j<i \text { 且 } num[i]-k\leqslant num[j]<num[i] \] 方法2:DP + 线段树,定义 \(dp[i]\)\(i\) 结尾的 LIS 长度,用线段树表示 \(dp\) 数组,在区间内查询最大值,然后更新目标值、更新区间最大值。复杂度 \(O(n \log n)\)\[ dp[nums[i]]=\max (dp[j])+1 \text {, 其中 } num[i]-k \leq j<num[i] \]

树状数组

线段树更适合区间修改问题,单点修改问题建议用树状数组。使用时要注意,下标从 1 开始。

单点修改,求区间和模板:

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
long long tree[30050];
vector<int> num;
int n;
int lowbit(int x) {
return x & -x;
}
long long query(int x) {
long long ans = 0;
for(int i = x; i > 0; i -= lowbit(i))
ans += tree[i];
return ans;
}
void add(int x, int u) {
for(int i = x; i <= n; i += lowbit(i))
tree[i] += u;
}
//初始化,tree 下标从 1 开始
NumArray(vector<int>& nums) {
num = nums;
memset(tree, 0, sizeof(tree));
n = nums.size();
for(int i = 1; i <= n; i++){
add(i, nums[i - 1]);
}
}
// 单点修改
void update(int index, int val) {
add(index + 1, val - num[index]);
num[index] = val;
}
// 查询 [left,right] 区间和 (left,right 下标从 0 开始)
long long sumRange(int left, int right) {
return query(right + 1) - query(left);
}

单点修改,求最值模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
long long tree[100050];
long long dp[100050]; //dp[i] 表示以 i 结尾的最长序列长度
int n;
int lowbit(int x) {
return x & -x;
}
long long query(int x, int y){
long long ans = 0;
while(y >= x){
ans = max(dp[y], ans);
y--;
for(; y - lowbit(y) >= x; y -= lowbit(y))
ans = max(tree[y], ans);
}
return ans;
}
void update(int x, long long u) {
for(int i = x; i <= n; i += lowbit(i))
tree[i] = max(u, tree[i]);
}

并查集

并查集主要用于处理不相交集合的合并问题,常用于快速判断两个点是否在同一个集合。在使用路径压缩优化后,合并和查找操作都近似 \(O(1)\) 时间复杂度。

并查集极简模板(在函数内定义即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int fa[n], size[n], setCnt = n;
iota(fa, fa + n, 0);
fill(size, size + n, 1);
function<int(int)> find = [&](int x){
return fa[x] == x ? x : fa[x] = find(fa[x]); // 路径压缩
};
auto merge = [&](int from, int to){
from = find(from);
to = find(to);
if (from != to) {
fa[from] = to;
size[to] += size[from]; // size 只有在 fa[x] == x 处才有意义
setCnt--;
}
};

并查集完整类模板:

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
class UnionFind {
public:
vector<int> parent;
vector<int> size;
int n;
int setCount;
public:
UnionFind(int _n): n(_n), setCount(_n), parent(_n), size(_n, 1) {
iota(parent.begin(), parent.end(), 0);
}
int find(int x) {
return parent[x] == x ? x : parent[x] = find(parent[x]);
}
bool merge(int x, int y) {
x = find(x);
y = find(y);
if (x == y) return false;
if (size[x] < size[y]) swap(x, y);
parent[y] = x;
size[x] += size[y];
--setCount;
return true;
}
bool connected(int x, int y) {
x = find(x);
y = find(y);
return x == y;
}
};

相关题目:

1971. 寻找图中是否存在路径 (E)

题目描述:给定一张具有 \(n\) 个顶点的双向图,判断是否存在从顶点 A 到顶点 B 的有效路径。

方法1:BFS,建图后,将顶点 A 先放入队列,每次将其邻居入队,直到队列为空或访问到顶点 B 时结束。时空复杂度均为 \(O(n+m)\)

方法2:DFS,建图后,从顶点 A 开始递归搜索,用 \(vis\) 记忆每个访问过的顶点防止陷入循环,遍历完所有邻居后如果还没访问到 B,则返回 false。时空复杂度均为 \(O(n+m)\)

方法3:并查集,将图中每个连通分量视为一个集合,用并查集维护,判断 A 和 B 是否属于一个集合即可。空间复杂度 \(O(n)\),时间复杂度 \(O(n+m)\)

547. 省份数量 (M)

题目描述:有 \(n\) 个城市,其中一些彼此相连,用一个邻接矩阵存储。省份是一组直接或间接相连的城市,求省份数量。

方法1:DFS,使用 \(vis\) 数组记录访问过的城市,以每个未访问的城市为起点开始搜索,并标记访问。搜索起点的个数就是省份数量。时间复杂度 \(O(n)\)

方法2:并查集,维护集合数 \(cntSet\),合并每个相连的城市,最后的 \(cntSet\) 就是答案。时间复杂度 \(O(n)\)

684. 冗余连接 (M)

题目描述:给定往一棵 \(n\) 个节点的树中添加一条边后的图,从中找出一条可删去的边,删除后可使得剩余部分恢复成树,如果有多个答案,返回数组 edges最后出现的边

方法1:并查集,最后出现的边就是第一个导致环的边,起初每个点属于不同的连通分量,遍历每条边将节点相连。如果某一条边的两端顶点已经属于同一个集合,则说明它就是冗余边。时间复杂度 \(O(n)\)

399. 除法求值 (H)

题目描述:给定一个字符串分式数组和分式的浮点数值,再给出一组查询分式数组,要求根据已知条件计算出值,如果没有答案则返回 -1.0

变量之间的倍数关系具有传递性,因此可以看作图论问题,分子和分母各为图中的节点,分式的值作为两点之间的权重。要查询到分式首先要判断分子与分母是否有联系,有联系才可能计算出结果。

由于分子、分母本身是字符串,如果要建图,就需要用哈希表将每个出现过的字符串映射成数字索引,用连续的数字表示节点编号。

方法1:BFS,建图后,对于每个查询,从起点出发,通过 BFS 的方式,不断更新起点与当前点之间的路径长度,直到搜索到终点为止。

方法2:Floyd 算法,对于查询数量很多的情形,如果为每次查询都独立搜索一次,则效率会变低。可以使用 Floyd 算法,预先计算出任意两点之间的距离

方法3带权并查集,不需要建图,用哈希表维护并查集。只需将出现过的分子、分母放到同一个集合,即可快速判断两个字符串是否有联系。并查集同时维护每个字符串到其父节点的倍数,路径压缩、合并时也要更新倍数。每次查询时只需判断是否在同一个集合,再将其通分成同一个祖先的倍数相除即可。


力扣刷题笔记 #03 数据结构
https://hwcoder.top/LeetCode-Data-Structure
作者
Wei He
发布于
2022年10月2日
许可协议