基础数据结构

基础数据结构

\(\mathcal{Part}\) 1. 链表

大家应该比较熟,直接说特点啦

  • 可以 \(\mathcal{O}(1)\) 查询后继
  • \(\mathcal{O}(n)\) 查询元素
  • \(\mathcal{O}(1)\) 插入和删除元素

至于 STL 的话,感觉不怎么好用,而且手写更方便(又不是什么很难得东西

  • 应用1

链式前向星,应该印在脑子里了吧

struct node{int to, nxt; }edge[MAXM];
int head[MAXN], e_cnt;
inline void add(int from, int to){edge[++ e_cnt].to = to, edge[e_cnt].nxt = head[from], head[from] = e_cnt; } 
  • 应用2

哈希表,我得评价是不如双哈希,就不写了

\(\mathcal{Part}\) 2.二叉堆

我们现在要建立这样一个数据结构,支持

  • 插入一个数 \(x\)
  • 查询 \(\max x\)
  • 删掉 \(\max x\),如果有多个只删除一个

  • 二叉堆

二叉堆是一个完全二叉树,分为大顶堆和小顶堆,大顶堆的根节点比他所有儿子以及孙子等要大,小根堆则同理

那么查询操作就直接输出根节点就好了

插入操作呢?

我们将 \(x\) 插入到这个堆最下面,然后以交换为基本规则,交换到符合规则,时间复杂度 \(\mathcal{O}(\log n)\)

inline void up(int x) {
    while (x > 1 && t[x] > t[x / 2]) 
        swap(t[x/2], t[x]), x /= 2;;
}

删除操作呢?

一样的,我们把根节点和最后一个节点交换,把根节点删掉,再进行下潜操作,时间复杂度 \(\mathcal{O}(\log n)\)

inline void down(int x) {
    while (x <= cnt) {
        if (x * 2 > cnt) break;
        if (x * 2 == cnt) {
            if (t[x] < t[x*2]) swap(t[x], t[x*2]);
            break;
        }
        int s = x * 2 + (t[x*2] < t[x*2+1]);
        if (t[x] < t[s]) swap(t[x], t[s]), x = s;
        else break;
    }
} 

完整代码

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 1e6 + 7;

int n;

struct Tree{
    int cnt, t[MAXN];

    inline void up(int x) {
        while (x > 1 && t[x] > t[x / 2]) 
            swap(t[x/2], t[x]), x /= 2;;
    }
    inline void down(int x) {
        while (x <= cnt) {
            if (x * 2 > cnt) break;
            if (x * 2 == cnt) {
                if (t[x] < t[x*2]) swap(t[x], t[x*2]);
                break;
            }
            int s = x * 2 + (t[x*2] < t[x*2+1]);
            if (t[x] < t[s]) swap(t[x], t[s]), x = s;
            else break;
        }
    } 
    inline void add(int x) {t[++ cnt] = x; up(cnt); }
    inline int top(){return t[1]; }
    inline void pop(){t[1] = t[cnt --]; down(1); }
}t;

int main () {
    ios::sync_with_stdio(false);
    cin.tie(NULL);

    cin >> n;
    for (int i = 1, op, x; i <= n; i ++) {
        cin >> op;
        if (op == 1) cin >> x, t.add(-x);
        else if (op == 2) cout << -t.top() << '\n';
        else if (op == 3) t.pop(); 
    }
    return 0;
}

当然,这东西有STL

  • 大根堆:priority_queue<int> Q1
  • 小根堆:priority_queue<int, vector<int>, greater<int>> Q2;

  • 对顶堆

用来查询第 \(k\) 小的数(如果你会平衡树可以不看的

我们建立两个堆,一个大顶一个小顶堆

大顶堆存的是排名为 \([1,k)\) 的数,小根堆则存另外的数

显然,小根堆的堆顶就是答案

其次,我们要注意维护好这两个堆的大小

\(\mathcal{Part}\) 3. 并查集

  • 模板

过于简单直接上代码

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 1e4 + 7;

int n, m, fa[MAXN];

void init() {
	for (int i = 1; i <= MAXN; i ++) fa[i] = i;
}

inline int find(int x) {
	if (fa[x] == x) return x;
	return fa[x] = find(fa[x]);
}

inline void merge(int x, int y) {
	int fx = find(x), fy = find(y);
	if (fx == fy) return ;
	fa[fx] = fy;
}

inline bool check(int x, int y) {
	return find(x) == find(y);
}
  • 逆序思想

我们现在要维护删边操作,我们并不会,怎么办?

我们可以反向思考,把它变成加边操作,于是就可以解决这个问题

  • 离线思想

给定的边有边权,我们要维护边权大于等于 \(k\) 的联通块数量怎么办?

我们可以先把询问存下来,对他进行排序,同时对边权进行排序

然后一条边一条地加,最后跟询问匹配即可

  • 带权并查集

考虑维护 \(d\) 数组表示这个点到其父亲的距离

每次在 \(find\) 中更新 \(d_x=d_x+d_{f_x}\) 即可,\(merge\) 中维护

最后根据情况输出答案即可

  • 扩展域并查集

一组点可能有多钟关系,这时候,我们吧并查集空间开大几倍

比如有 \(A,B,C\) 三种关系,我们将 \(1,n\)\(A\) 关系,\(n+1,2n\)\(B\) 关系,\(2n+1,3n\)\(C\) 关系

然后就可以维护啦

\(\mathcal{Part}\) 4.树状数组

  • 单修区查

现在,我们要自己设计一个树形数据结构,支持两种操作

  • 单点修改
  • 查找一个 \(x\) 的前缀和

这个数据结构需要有如下性质

  • 修改一个值只影响到少数区间
  • 对于任意一个前缀和的询问,可以用少数区间把询问的前缀拼出来

那树状数组是个什么东西呢?

首先上个经典的图

图

我们记 \(c_x\) 表示 \([x-lowbit(x)+1,x]\) 的区间和

\(lowbit\) 是啥东西呢?

表示为 lowvit(x)=x&(-x),意思是找到最后一个一所对应的权值(注:不是位置!

现在看看原理(lxl坚信一点用都没有)

  • 询问

我们假设要询问 \([1,x]\) 的值,我们先求出 \([x-lowbit(x)+1,x]\) 的和,即 \(c_i\),然后递推下去,知道访问 \([1,0]\) 这个区间停止,时间复杂度为 \(\mathcal{O}(\log n)\) 的(二进制啊

#define lb(x) (x & -x)
int query(int x) {
	int ans = 0;
	for (int i = x; i; i -= lb(i)) ans += a[i];
	return ans;
}
  • 修改

我们看修改 \(x\) 的值会影响到哪些值

这里根据略微复杂的分类讨论可以得到修改 \(x\) 影响到 \(x+lowbit(x)\) 即递推下去的值,同样,时间复杂度为 \(O(\log n)\)

inline void modify(int x, int y) {
	for (int i = x; i <= n; i += lb(i)) a[i] += y;
}

变形:权值线段树

我们维护一个数轴,把添加一个值当做单点修改,查询当做区间查询即可


  • 区修单查

我们采用反演思想

我们把区间修改做个差分,就是查询后缀和

比如 \([x,y]\) 改成 \([x,n]-[y+1,n]\) 即可

现在怎么维护一个后缀修改

我们看查询,我们想要知道修改这个点 \(x\) 影响到了哪些点

显然左端点小于 \(x\) 的点,我们结合刚刚讲到的权值线段树,就有个思路

我们直接区间查询 \([1,x)\) 的修改不就行了?那修改就变成了单点修改,于是我们就会做了

这类问题例题可以去看逆序对

\(\mathcal{Part}\) 5.线段树

一样的,我们现建立一个数据结构,使它满足上面两个性质

首先上个经典的图

其中,根节点表示一整个区间,左右子节点各表示一半区间

线段树可以维护任何负有传递性的东西,但是常数较大,注意下面的函数都是 \(\mathcal{O}(\log n)\)

  • 建树,\(build\)

直接按照上述规则建树即可,注意要在最后上传信息

struct Node{int sum, lg; }t[4 * MAXN];
#define ls(i) (i << 1)
#define rs(i) (i << 1 | 1)
inline void push_up(int x) {t[x].sum = t[ls(x)].sum + t[rs(x)].sum; }

inline void build(int cur, int l, int r) {
    if (l == r) {
        t[cur].sum = a[l];
        return ;
    } 
    int mid = l + r >> 1;
    build(ls(cur), l, mid); build(rs(cur), mid + 1, r);
    push_up(cur);
}
  • 单点修改

直接找到这个点修改即可,同时也要上传信息

inline void modify(int cur, int l, int r, int x, int y) {
    if (l == r) {
        sum[cur] += y;
        return ; 
    }
    int mid = l + r >> 1;
    if (x <= mid) modify(ls(cur), l, mid, x, y);
    else modify(rs(cur), mid + 1, r, x, y);
    push_up(cur);
}
  • 区间查询

判断区间是否相交即可,注意不要用 else,有可能两个区间都有相交

inline int query(int cur, int l, int r, int x, int y) {
    if (x <= l && y >= r) return sum[cur];
    int mid = l + r >> 1, ans = 0;
    if (x <= mid) ans += query(ls(cur), l, mid, x, y);
    if (y > mid) ans += query(rs(cur), mid + 1, r, x, y);
    return ans; 
}
  • 区间修改

递归到每个子节点进行修改?

如果这么做时间复杂度为 \(\mathcal{O}(size)\),size 为整个树的大小

有什么方法?

我们注意到只有查询才会查询这个节点的具体的值,我们于是可以将这个节点加个标记

表示我的所有儿子都要修改,但是因为我懒,我等到你查到我儿子我再把这个标记传给他

所以这叫懒惰标记,每次往下递归都要下传

inline void f(int cur, int l, int r, int k) {
    t[cur].sum += (r - l + 1) * k;
    t[cur].lg += k;
}
inline void push_down(int cur, int l, int r) {
    int mid = l + r >> 1;
    f(ls(cur), l, mid, t[cur].lg);
    f(rs(cur), mid + 1, r, t[cur].lg);
    t[cur].lg = 0;
}

区修区查完整代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int MAXN = 1e5 + 7;

int n, m, a[MAXN];

struct Segt_Tree{
	struct Node{int sum, lg; }t[MAXN * 4];
	
	#define ls(i) (i << 1)
	#define rs(i) (i << 1 | 1)
	inline void push_up(int x) {t[x].sum = t[ls(x)].sum + t[rs(x)].sum; }
	
	inline void build(int cur, int l, int r){
		if (l == r) {
			t[cur].sum = a[l];
			return ;
		}
		int mid = l + r >> 1;
		build(ls(cur), l, mid); build(rs(cur), mid + 1, r);
		push_up(cur);
	}
	
	inline void f(int cur, int l, int r, int k){
		t[cur].sum += (r - l + 1) * k;
		t[cur].lg += k;
	}
	inline void push_down(int cur, int l, int r) {
		int mid = l + r >> 1;
		f(ls(cur), l, mid, t[cur].lg);
		f(rs(cur), mid + 1, r, t[cur].lg);
		t[cur].lg = 0;
	}
	
	inline void modify(int cur, int l, int r, int x, int y, int k) {
		if (x <= l && y >= r) {
			f(cur, l, r, k);
			return ;
		}
		int mid = l + r >> 1;
		push_down(cur, l, r);
		if (x <= mid) modify(ls(cur), l, mid, x, y, k);
		if (y > mid ) modify(rs(cur), mid + 1, r, x, y ,k);
		push_up(cur);
	}
	inline int query(int cur, int l, int r, int x, int y) {
		if (x <= l && y >= r) return t[cur].sum; 
		int mid = l + r >> 1, ans = 0;
		push_down(cur, l, r);
		if (x <= mid) ans += query(ls(cur), l, mid, x, y);
		if (y > mid ) ans += query(rs(cur), mid + 1, r, x, y);
		return ans;
	}
}t;

signed main () {
	cin >> n >> m;
	for (int i = 1; i <= n; i ++) cin >> a[i];
	t.build(1, 1, n);
	for (int i = 1, flag; i <= m; i ++) {
		cin >> flag;
		if (flag == 1) {
			int x, y, k;
			cin >> x >> y >> k;
			t.modify(1, 1, n, x, y, k);
		} else if (flag == 2) {
			int x, y;
			cin >> x >> y;
			cout << t.query(1, 1, n, x, y) << '\n';
		}
	}
	return 0;
}

\(\mathcal{Part}\) 6.后记

数据结构他是灵活的,所以要多加练习,特别是线段树,应用非常之广泛,当然,我并没有学分块,所以只会线段树

posted @ 2023-06-22 17:18  Phrvth  阅读(69)  评论(0)    收藏  举报