文章目录
一、并查集
1.1 适用问题类型
并查集可用于求图中是否存在环(这个当然也可以用拓扑排序)、图连通相关问题。
1.2 基本api
个人习惯将并查集抽象为一个类使用。
并查集的基本操作与名称来由相关,及并操作和查操作,根据题目需求,可能会出现统计连通子图数目、连通分量等。
class UnionFind{
public:
UnionFind(int n); //初始化图节点数目
int find_op(int x); //查找编号为x的节点
int union_op(int x, int y); //合并两个节点到一个子图,返回值可以表征是否成功
int count(); //计算子图数目
};
1.3 实现代码及原理
并查集的实现十分巧妙,在学习这个之前,我一度以为这个数据结构的实现会很复杂,后来知道真相的我感动得眼泪落下来。
基本原理就是使用一个数组parent,通过哈希表的方式记录数组下标对应的连接目标节点。为了提高性能,使用rank数组记录带合并两个节点所在子图的深度,需要尽可能使最大深度更浅。
class UnionFind{
private:
vector<int> parent;
vector<int> rank;
public:
UnionFind(int n){
parent.assign(n, -1);
rank.assign(n, 0);
}
int find_op(int x){
while(parent[x] != -1)
x = parent[x];
return x;
}
// 根据x、 y所在连通子图根节点的rank大小,rank较大的作为根节点。
int union_op(int x, int y) {
int xp = find_op(x), yp = find_op(y);
if(xp == yp) return 0;
if(rank[xp] > rank[yp]){
parent[yp] = xp;
} else if(rank[xp] < rank[yp]) {
parent[xp] = yp;
} else {
parent[xp] = yp;
rank[xp] += 1;
}
return 1;
}
int count() {
int ans = 0;
for(int v: parent){
if(v == -1) ans++;
}
return ans;
}
};
1.4 练习题目
1.5 推荐学习资料
二、单调栈
2.1 适用问题类型
对整个数组中所有的值,找最近一个比当前值大或小的值。
2.2 实现原理及代码
单调栈指的是栈顶到栈底的元素为单调递增或递减。单调栈的基本操作仍然是push、pop、top。与普通栈的区别在于push数据
v
v
v入栈时,要将栈顶小于
v
v
v的值全部弹出。
注意下面代码的注释:
vector<int> nextGreaterElements(vector<int>& nums) {
//这里根据题目要求,栈中保存的为数据对应的下标而非直接存值
stack<int> loc;
vector<int> ans(nums.size(), -1);
for(int ii = 0; ii < nums.size() * 2; ++ii){
int i = ii % nums.size();
// 关键:栈非空时,弹出栈顶所有小于nums[i]的元素。
while(!loc.empty() && nums[loc.top()] < nums[i] && loc.top() != i){
ans[loc.top()] = nums[i];
loc.pop();
}
if(!loc.empty() && loc.top() == i)
break;
loc.push(i);
}
return ans;
}
2.3 练习题目
三、单调队列
3.1 适用问题类型
求滑动窗口中的最大/最小值。
3.2 基本api
单调队列可用 O ( 1 ) O(1) O(1)的平均复杂度获得当前队列中的最值。
class MonotonicQueue{
public:
void push(int v);
void pop();
int max(); //相应的也可以是min()
int size();
};
3.3 实现代码及原理
封装为一个类,用标准的队列维护正常的队列操作。再使用一个双端队列维护最值。这个双端队列中保存的元素一定是单调的,这也是单调队列名称的由来。
类似单调栈,单调队列的维护也是通过一个while循环不断从队列尾部弹出小于/大于待插入值的元素。注意push和pop两个操作。
class MonotonicQueue{
private:
queue<int> value;
deque<int> most;
public:
void push(int v){
value.push(v);
// keep monotonic
//如果需要维护队列中最大元素,那么单调队列是递减的,反之是递增的。
while (!most.empty() && most.back() < v)
most.pop_back();
most.push_back(v);
}
void pop() {
int pop_v = value.front();
value.pop();
if(most.front() == pop_v)
most.pop_front();
}
int max(){
return most.front();
}
int size() {
return value.size();
}
};
3.4 练习题目
四、线段树(分段树)
4.1 适用问题类型
分段树的作用主要是实现对数组任意区间进行查询,并可以动态的修改数组,只要满足区间可加性的查询都能使用分段树(例如区间和、区间乘积,区间计数等)。分段树有几个典型的用法:
- 前缀和数组升级版:前缀和数组可以实现任意区间的 O ( 1 ) O(1) O(1)查询复杂度,但修改数组的话,需要重新建立前缀和数组,复杂度为 O ( n ) O(n) O(n)。线段树的查询和修改复杂度均为 O ( log n ) O(\log n) O(logn)
- 权重动态变化的加权采样(动态轮盘赌算法):如果权重不改变,那直接使用前缀和+二分查找即可实现采样查询。但权重变化的话,就需要使用线段树。强化学习领域中,一篇基于优先权经验重放(Prioritized experience replay)的论文就是使用的这个方法实现。
4.2 基本api
线段树的基本操作为树的构建、修改和查询三个操作,一旦构建好树,树的容量(即原始数组大小arr_size,而不是保存区间和的tree_size)不能轻易改变
public:
SegTree(vector<int> &arr){
// tree数组空间,一般初始化为原数组最大长度的4倍,并记录原数组的下标范围,作为update以及query根据,分段大小不可改变。
int size = arr.size();
tree.assign(size * 4, 0);
L = 0;
R = size - 1;
_build(arr, 0, 0, arr.size() - 1);
}
void update(int loc, int val){
_update(0, loc, val, L, R);
}
int query(int ql, int qr){
return _query(0, L, R, ql, qr);
}
int tree_size(){
return tree.size();
}
int arr_size(){
return R - L + 1;
}
4.3 完整模版代码
#include<bits/stdc++.h>
using namespace std;
class SegTree{
public:
vector<int> tree;
int L;
int R;
private:
void _build(vector<int> &arr, int root, int left, int right){
//递归初始化
if(left == right){
tree[root] = arr[left]; //段数不可切分,为叶节点,初始化为原数组的值
return;
}
//分段, 递归初始化【分治思想】
int mid = (left + right) / 2, left_child = root * 2 + 1, right_child = root * 2 + 2;
_build(arr, left_child, left, mid);
_build(arr, right_child, mid + 1, right);
tree[root] = tree[left_child] + tree[right_child];
}
void _update(int root, int loc, int val, int left, int right){
if(loc < left || loc > right) return;
if(left == right) {
tree[root] = val;
return;
}
int mid = (left + right) / 2, left_child = root * 2 + 1, right_child = root * 2 + 2;
_update(left_child, loc, val, left, mid);
_update(right_child, loc, val, mid + 1, right);
tree[root] = tree[left_child] + tree[right_child];
}
int _query(int root, int left, int right, int q_left, int q_right){
if(q_right < left || q_left > right) return 0;
if(q_left <= left && q_right >= right) return tree[root];
int ans = 0;
int mid = (left + right) / 2, left_child = root * 2 + 1, right_child = root * 2 + 2;
ans += _query(left_child, left, mid, q_left, q_right);
ans += _query(right_child, mid + 1, right, q_left, q_right);
return ans;
}
public:
SegTree(vector<int> &arr){
// tree数组空间,一般初始化为原数组最大长度的4倍,并记录原数组的下标范围,作为update以及query根据,分段大小不可改变。
int size = arr.size();
tree.assign(size * 4, 0);
L = 0;
R = size - 1;
_build(arr, 0, 0, arr.size() - 1);
}
void update(int loc, int val){
_update(0, loc, val, L, R);
}
int query(int ql, int qr){
return _query(0, L, R, ql, qr);
}
int tree_size(){
return tree.size();
}
int arr_size(){
return R - L + 1;
}
};