数据结构总结

This is a data structure summary.

并查集

一个维护连通性的神器,时间复杂度几乎为常数级别(加了按秩合并后就真的是常数级别了)
(n)个点看成一个森林,用一个数组记录每个点的父亲,初始时均为自己。
要合并两棵树时,也就是合并集合,把一棵个树的父亲指向另一棵树就行了。
要查询两个点是否在同一集合内,只需要看他们的根节点是否是同一个点就行了。

int find(int x){
    return f[x] == x ? x : find(f[x]);
}

可以发现,我们只要判断连通性,并不需要记录具体路径是什么,于是就有了路径压缩

int find(int x){
    return f[x] == x ? x : f[x] = find(f[x]);
}

就是我们直接把路径中的每个点指向其根节点,省去了后续查询中的很多不必要的操作。
合并操作:

void merge(int x, int y){
    f[find(y)] = find(x);
}

按秩合并

记录每个集合的大小,合并时把小的集合指向大的集合。

边带权

即带权并查集,每条边都有个权值,在路径压缩的时候把路径中的所有边权加起来。

//洛谷 P1196 银河英雄传说
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <stack>
#include <list>
#define rep(i,m,n) for(int i=m;i<=n;++i)
#define dop(i,m,n) for(int i=m;i>=n;--i)
#define lowbit(x) (x&(-x))
#define ll long long
#define INF 2147483647
#define re register
#define Open(s) freopen(s".in","r",stdin);freopen(s".out","w",stdout);
#define Close fclose(stdin);fclose(stdout);
#define pause system("pause");
using namespace std;
inline int read(){
    int s = 0, w = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){if(ch == '-')w = -1;ch = getchar();}
    while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0',ch = getchar();
    return s * w;
}
const int MAXN = 30010;
int f[MAXN], down[MAXN], size[MAXN], top[MAXN];
int n;
int find(int x){
    if(f[x] != x){
      int fa = find(f[x]);
      down[x] += down[f[x]];
      f[x] = fa;
    }
    return f[x];
}
void merge(int x,int y){
    int f1 = find(x), f2 = find(y);
    if(f1 != f2){
      down[f1] += size[f2];
      size[f2] += size[f1];
      f[top[f1]] = f2;
      find(x); find(y);
    }
}
void init(){
    for(int i = 1; i <= n; ++i)
       f[i] = top[i] = i, size[i] = 1;
}
int a, b;
int main(){
    cin >> n; init();
    while(233){
       char ch = getchar();
       while(ch != 'M' && ch != 'C' && ch != -1) ch = getchar();
       if(ch == -1) break;
       if(ch == 'M') a = read(), b = read(), merge(a, b);
       else a = read(), b = read(), printf("%d
", find(a) == find(b) ? abs(down[a] - down[b]) - 1 : -1);
    }
    return 0;
}

先进先出

队列

先进后出

链表

就是一条链嘛

倍增

RMQ问题

·问题:给定一个数列,求区间最大(小)值。

ST表

以区间最大值为例。
倍增的思想,用(f[i][j])表示从第(i)个数开始的(2^j)个数里的最大值。
初始时对于每个(iin [1,n])(f[i][0]=a[i])(a[i])即第(i)个数。
然后枚举(j)(f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1])),预处理就完成了。
然后是查询,设要查询的区间为([l,r]),设(p)为使(2^p)不大于((r-l+1))的最大的数,则区间最大值为(max(f[l][p], f[r-(1<<p)+1][p]))
时间复杂度:预处理(O(nlog n)),查询(O(1))

//洛谷 P3865 【模板】ST表
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
inline int read(){
    int s=0, w=1;
    char ch = getchar();
    while(ch <= '0' || ch > '9'){ if(ch == '-') w = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
    return s * w;
}
const int maxn = 100010;
int Log[maxn], f[maxn][20], n, m, a, b;
int Query(int l, int r){
    int k = Log[r - l + 1];
    return max(f[l][k], f[r - (1 << k) + 1][k]);
}
int main(){
    n = read(); m = read();
    Log[0]=-1;
    for(int i = 1; i <= n; ++i) f[i][0] = read();
    for(int i = 1; i <= n; ++i) Log[i] = Log[i / 2]+1;
    for(int i = 1; i <= 20; ++i)
       for(int j = 1; j + (1 << i) - 1 <= n; j++)
          f[j][i] = max(f[j][i - 1], f[j + (1 << (i - 1))][i - 1]);
    while(m--){
      a = read(); b = read();
      printf("%d
", Query(a, b));
    }
    return 0;
}

树状数组

·基本用途:维护数列前缀和,支持单点修改
·原理:把(n)写成二进制形式,维护每个(1)出现的位置的值。。。我也说不清。
·实现:

#define lowbit(x) (x & (-x))

单点修改:

void add(int s,int k){    //在s位置加上k
    while(s<=n){
       sum[s]+=k;
       s+=lowbit(s);
     }
}

查询前缀和:

int query(int k){   //查询前k个数的前缀和
	int ans=0;
	while(k){
      ans+=sum[k];
      k-=lowbit(k);
    }
    return ans;
}

·树状数组的优势:常数小,代码短
·树状数组的劣势:适用范围小,不易扩展
·树状数组扩展:区间修改,单点查询
插入时把每个数的值当作以该数为最后一个数的前缀和,那个插入的时候只需要在这个位置插入(a_i-a_{i-1})就行了,在这里执行前缀和得到的就是这个数的值,其实就是差分的思想。修改时,设修改的区间为([l,r]),修改的值为(p),那么我们对(l)的位置加上(p),对(r+1)的位置再加上(-p)就行了。

//洛谷 P3368 【模板】树状数组2
#include <iostream>
#include <cstdio>
#include <cstring>
#define lowbit(x) (x&(-x))
using namespace std;
const int MAXN = 500010; 
int n,m;
int tree[MAXN], a[MAXN];
void add(int k, int num){
    while(k <= n){
      tree[k] += num;
      k += lowbit(k);
    }
}
int sum(int k){
    int s = 0;
    while(k){
      s += tree[k];
      k ^= lowbit(k);
    }
    return s;
}
int main(){
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++){
        scanf("%d", &a[i]);
        add(i, a[i] - a[i-1]);
    }
    for(int i = 1; i <= m; i++){
        int c, x, y, z;
        scanf("%d", &c);
        if(c == 1) scanf("%d%d%d", &x, &y, &z), add(x, z), add(y + 1, -z);
        else scanf("%d", &x), printf("%d
", sum(x));
    }
    return 0;
}

线段树

·用途:维护区间(最大值,和,等等等等,甚至最大子序列)
·思想:一分为二,分别维护。比如要维护一个(n)个数的数列的区间和。建一棵树,根节点维护([1,n]),左儿子维护([1,n/2]),右儿子维护([n/2+1,n]),以此类推,递归处理,当维护的区间(l=r)时停止递归,每个节点都用自己的左儿子和右儿子的值来更新自己。
·Lazy标记:如果修改或查询时直接到底的话,那么复杂度仍是(O(n))级别的,因为我们要修改/查询到底,直到把只维护一个数的节点也修改/查询了,这样线段树的作用就失去了。Lazy标记应运而生。当当前的区间在要修改/查询的区间之内时,不再往下递归修改,只修改/查询当前节点,然后在当前节点打一个Lazy标记。下次访问到该节点的时候根据标记修改子树,并下传标记。

//洛谷 P3372 【模板】线段树1
#include <iostream>
#include <cstdio>
#define left now << 1
#define right (now << 1) | 1
#define ll long long
using namespace std;
inline int read(){
    int s = 0, w = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){if(ch == '-')w = -1;ch = getchar();}
    while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0',ch = getchar();
    return s * w;
}
const int MAXN = 100010;
const int MAXM = 200010;
ll sum[MAXN << 2], a[MAXN], lazy[MAXN << 2];
int dep[MAXN], dfsx[MAXN], n, head[MAXN], ID, num, m, son[MAXN], s[MAXN], f[MAXN], t[MAXN], pos[MAXN];
void Build(int now, int l, int r){
    if(l == r) sum[now] = a[l];
    else{
      int mid = (l + r) / 2;
      Build(left, l, mid);
      Build(right, mid + 1, r);
      sum[now] = sum[left] + sum[right];
    }
}
void Pushdown(int len, int now){
    if(lazy[now]){
      lazy[left] += lazy[now];
      lazy[right] += lazy[now];
      sum[left] += lazy[now] * (len - (len >> 1));
      sum[right] += lazy[now] * (len >> 1);
      lazy[now] = 0;
    }
    return;
}
ll Query(int now, int l, int r, int wl, int wr){
    if(wl <= l && wr >= r) return sum[now];
    if(wl > r || wr < l) return 0;
    Pushdown(r - l + 1, now);
    int mid = (l + r) / 2;
    return Query(left, l, mid, wl, wr) + Query(right, mid + 1, r, wl, wr);
}
void Change(int now, int l, int r, int wl, int wr, int add){
    if(wl <= l && wr >= r){ sum[now] += add * (r - l + 1), lazy[now] += add; return; }
    if(wl > r || wr < l) return ;
    Pushdown(r - l + 1, now);
    int mid = (l + r) / 2;
    Change(left, l, mid, wl, wr, add);
    Change(right, mid + 1, r, wl, wr, add);
    sum[now] = sum[left] + sum[right];
}
int A, B, C;
int main(){
    n = read();m = read();
    for(int i = 1; i <= n; ++i) a[i] = read();
    Build(1, 1, n);
    for(int i = 1; i <= m; ++i){
       A = read();
       if(A == 1){
         A = read(); B = read(); C = read();
         Change(1, 1, n, A, B, C);
       }
       else {
         A = read(); B = read();
         printf("%lld
",Query(1, 1, n, A, B));
       }
    }
    return 0;
}

(zkw)线段树

待补。

可持久化线段树

·用途:一般的数据结构都是维护最新状态,可持久化数据结构就是能恢复“第(k)次操作之前的状态”。
·思想:线段树每次修改需要修改的节点数是((log n))级别的,于是每次修改后对修改了的节点建立一个副本,之后通过副本恢复状态就行了,空间复杂度(O(nlog n))
详细:点我

主席树

点我

扫描线

·基本用途:求矩形面积并。
待补。

分块

·用途:维护区间,能实现某些线段树难以实现的,也难以实现某些线段树能实现的。
·优势:代码短,实用性强。
·劣势:时间复杂度较高。
·思想:大段维护,小段朴(bao)素(li)。
·实现:把一个数列分成(sqrt n)块,每块大小(sqrt n),最后不足一块的算一块。以维护区间和为例,先预处理出每块的和,时间复杂度(O(n)),查询时把要查询的区间([l,r])分成三部分:
1、左边不足一块的部分。
2、中间很多块的部分。
3、右边不足一块的部分。
对1、3部分朴(bao)素(li)求解,第2部分我们是维护了的,直接加上中间每一块的和就行了。
修改时,
单点修改:把存对应值数组直接修改,然后再对这个点所在的块进行修改。
区间修改:把区间分成三个部分后,对左右部分朴(bao)素(li)修改,然后对中间的每一块使用Lazy数组标记一下修改了多少,这里的Lazy数组和线段树里的类似,但又有不同,这里的Lazy数组显然不能下传标记,因为当一个块里只有一部分点需要查询时,把Lazy数组下放的话,那么这个块里其他的点的值就不会修改,所以,这里引入标记永久化,查询时不对保存值的数组进行修改,也不下放标记,只是单纯的把计数器加上这块的Lazy值乘以这一块中要查询的数的个数。区间修改完成。

一般的时间复杂度(O(nsqrt n)),块的大小用(sqrt n)的原因是为了平均朴(bao)素(li)和维护的复杂度,使得左右两边和中间大块统计/修改时的时间复杂度都是(O(sqrt n))。块的大小也因具体问题而异。
·分块的进阶应用:区间众数

莫队

待补。

树链剖分

·用途:维护和查询树上的链
·思想:把一棵树剖成一些链,为每个点编个号,使得每一条链上的点的编号是连续的,这样我们就可以用数据结构(比如线段树)来维护每一条链了。

重链剖分

明确几个概念:
·重儿子:子树节点数最多的儿子
·重边:到重儿子的边
·重链:全由重边组成的链
显然可以用(dfs)求出每个点的子树大小,重儿子也得以求出,然后再一遍(dfs),对于每个点,如果有重儿子,都优先遍历其重儿子,这样就能使得这条重链上的编号是连续的,本次(dfs)(dfs)序就是每个点的编号。然后用数据结构维护即可。

长链剖分

重儿子就是深度最深的儿子。然后和重链剖分一样处理。
用途:线性时间内处理一些和深度有关的(DP)
(O(1))继承重儿子的信息,然后暴力转移其它儿子的信息。
例如这题

平衡树

Treap

待学。

Splay

待补。

原文地址:https://www.cnblogs.com/Qihoo360/p/9614969.html