高级数据结构:并查集、单调栈、单调队列

一、并查集

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 适用问题类型

分段树的作用主要是实现对数组任意区间进行查询,并可以动态的修改数组,只要满足区间可加性的查询都能使用分段树(例如区间和、区间乘积,区间计数等)。分段树有几个典型的用法:

  1. 前缀和数组升级版:前缀和数组可以实现任意区间的 O ( 1 ) O(1) O(1)查询复杂度,但修改数组的话,需要重新建立前缀和数组,复杂度为 O ( n ) O(n) O(n)。线段树的查询和修改复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)
  2. 权重动态变化的加权采样(动态轮盘赌算法):如果权重不改变,那直接使用前缀和+二分查找即可实现采样查询。但权重变化的话,就需要使用线段树。强化学习领域中,一篇基于优先权经验重放(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;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值