zoukankan      html  css  js  c++  java
  • 数据结构总结

    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

    待补。

  • 相关阅读:
    AtCoder ABC 129F Takahashi's Basics in Education and Learning
    AtCoder ABC 129E Sum Equals Xor
    UVA 511 Do You Know the Way to San Jose?
    UVA 12504 Updating a Dictionary
    [Poi2000] 病毒
    [loj10061] 最短母串
    [Poi2010] Antisymmetry
    校内集训20181003
    校内集训20181001
    校内集训20180925
  • 原文地址:https://www.cnblogs.com/Qihoo360/p/9614969.html
Copyright © 2011-2022 走看看