zoukankan      html  css  js  c++  java
  • 浅谈简单可持久化数据结构及其应用

    参考资料

    《浅谈可追溯化数据结构》————孔朝哲 2019中国国家候选队论文
    《可持久化数据结构研究》————陈立杰
    《算法竞赛进阶指南》———— 李煜东

    感谢他们的文字。


    前言

    一个数据结构通过修改操作改变自身结构(也可能改变数据),就称 这个数据结构的版本得到了更新。
    将一个数据结构可持久化, 就是利用共用一部分结构的思想, 在空间上高效地保存这个数据结构的 所有历史版本


    Trie 的可持久化及其应用

    对 Trie 的插入可持久化, 首要的问题就是不能对上一个版本进行丝毫改变, 再就是确实地保存此版本的正确结构, 最后就是尽量与上个版本共用空间。
    这里介绍一个实现可持久化 Trie 的算法。

    算法流程
    设要插入的字符串为 s, 下标从零开始。
    1.设之前最新版本 Trie 的根为 root, 设 p = root , i = 0。
    2.建立一个新节点 root' 作为更新版本的根, 设 q = root'
    3.对于所有字符集里的字符 c, ch[q]->c = ch[p]->c
    4.新建节点 h, ch[q]->c = h
    5.p = ch[p]->s[i], q = ch[q]->s[i], i += 1;
    6.重复 3~5 直到 i = len(s) 时终止算法。

    正确性:
    首先算法中没有对之前版本的 Trie 上的任何指针进行更改, 所以不会改变上一个版本的结构。

    至于能不能确实地保存当前版本的 Trie, 我描述不出来, 证明待补。
    但我觉得证明这个是有价值的, 或许还可以打开新世界的大门, 所以我一定会回来补证明的。

    复杂度:
    复杂度就显然了, 时间与空间复杂度都是 (O(插入串的总长))

    最大异或和
    将后缀异或和转化为两个前缀异或和的异或和。
    设 s[i] 表示直到 a[i] (包括 a[i])的前缀异或和。
    每次查询就转化为:给定 l,r 找一个最大的 p ((l-1 le p le r-1)), 使得 s[p] xor s[n] xor x 最大。
    如果 p 的范围只有 (le r-1) 的限制, 就可以直接可持久化 0/1 Trie 做了。

    考虑给 Trie 的节点增加额外的信息, 使得不至于在查询的过程中走到 (< l-1) 的节点 : 在可持久化 Trie 中插入数的时候, 给新建的节点染色,这样, 如果一个节点的颜色是位置 (l-1<) 的数的颜色, 这说明以这个节点为根的子树内只有位置 (< l-1) 的数的终止节点, 在 Trie 中游走的时候避免走这类点, 就可以在满足 (le r-1) 限制的同时满足 (ge l-1) 的限制。

    #include<bits/stdc++.h>
    using namespace std;
    const int N = 600003;
    
    int n,m,las;
    int tot, root[N], ch[N*24][2], col[N*24];
    
    void insert(int id, int tmp) {
      int p = root[id-1], q = root[id] = ++tot;
      col[q] = id;
      for(int i=23;i>=0;--i) {
        int v = (tmp>>i)&1;
        col[ch[q][v] = ++tot] = id;
        ch[q][v^1] = ch[p][v^1];
        q = ch[q][v];
        p = ch[p][v];
      }
    }
    
    int ques(int id, int underlim, int tmp) {
      int res = 0;
      int p = root[id];
      for(int i=23;i>=0;--i) {
        int v = (tmp>>i)&1;
        if(ch[p][v^1] && col[ch[p][v^1]] >= underlim) res += (1<<i), p = ch[p][v^1];
        else p = ch[p][v];
      }
      return res;
    }
    
    int main() {
      
      scanf("%d%d", &n,&m);
      for(int i=1, a; i<=n; ++i) {
        scanf("%d", &a);
        las = las ^ a;
        insert(i, las);
      }
      char s[3];
      int l,r,x;
      while(m--)
      {
        scanf("%s", s);
        if(s[0] == 'A')
        {
          scanf("%d", &x);
          las = las ^ x;
          insert(++n, las);
        }
        else
        {
          scanf("%d%d%d", &l, &r, &x);
          if(r==1) {
            cout << (las ^ x) << '
    ';
            continue;
          }
          cout << ques(r-1, l-1, x ^ las) << '
    ';
        }
      }
      return 0;
    }
    

    Fotile模拟赛L

    把连续异或和拆成两个前缀异或和的异或和, 问题就变成了区间内选两个点, 使得异或和尽量大。
    考虑分块, 预处理两端点都在一段连续块之间的答案, 这样, 一个询问只要做两遍 最大异或和 里的做法就行了。
    预处理的时候要用区间 DP, 预处理的时候也要用到 最大异或和 里的做法。

    常数有 (30), 挺吓人的, 直到我看了数据范围之后。

    #include<bits/stdc++.h>
    using namespace std;
    const int N = 12003;
    const int M = 6003;
    const int Mb = 111;
    
    int tot, ch[N*41][2], col[N*41], root[N];
    void insert(int id, int tmp) {
      int p=root[id-1], q=root[id]=++tot;
      col[q] = id;
      for(int i=30;i>=0;--i) {
        int v = (tmp>>i) & 1;
        ch[q][v^1] = ch[p][v^1];
        col[ch[q][v]=++tot] = id;
        p=ch[p][v], q=ch[q][v];
      }
    }
    
    int ask(int id, int underlim, int tmp) {
      int p=root[id], res=0;
      for(int i=30;i>=0;--i) {
        int v = ((tmp>>i)&1) ^ 1;
        if(col[ch[p][v]] and col[ch[p][v]] >= underlim) res |= (1<<i);
        else v^=1;
        p = ch[p][v];
      }
      return res;
    }
    
    int n,m,a[N];
    int B, mxpos, pos[N], L[Mb], R[Mb], f[Mb][Mb];
    
    void init() {
      B = sqrt(n*1.0);
      for(int i=1;i<=n;++i) pos[i] = (i-1)/B + 1;
      mxpos = pos[n];
      for(int i=1;i<=mxpos;++i) L[i]=(i-1)*B+1, R[i]=i*B;
      R[mxpos] = min(R[mxpos], n);
      for(int i=1;i<=mxpos;++i)
        for(int r=L[i]; r<=R[i]; ++r)
          for(int l=L[i]-1;l<r;++l)
            f[i][i] = max(f[i][i], a[l]^a[r]);
      for(int len=2;len<=mxpos;++len)
        for(int l=1;l+len-1<=mxpos;++l) {
          int r = l+len-1;
          f[l][r] = f[l][r-1];
          for(int i=L[r];i<=R[r];++i) f[l][r] = max(f[l][r], ask(i-1, L[l]-1, a[i]));
        }
    }
    
    int main() {
      
      scanf("%d%d", &n,&m);
      for(int i=1;i<=n;++i) {
        scanf("%d", &a[i]); a[i] ^= a[i-1]; insert(i,a[i]);
      }
      
      init();
      
      int lastans = 0;
      while(m--) {
        int x,y,l,r;
        scanf("%d%d", &x,&y);
        l = ((long long)x+lastans)%n + 1;
        r = ((long long)y+lastans)%n + 1;
        if(l>r) swap(l,r);
        
        lastans = 0;
        if(pos[l]==pos[r]) {
          for(int i=l;i<=r;++i)
            for(int j=l-1;j<i;++j)
              lastans = max(lastans, a[i]^a[j]);
        } else {
          lastans = f[pos[l]+1][pos[r]-1];
          for(int i=L[pos[r]];i<=r;++i) lastans = max(lastans, ask(i-1,l-1,a[i]));
          for(int i=l;i<=R[pos[l]];++i) lastans = max(lastans, ask(r,i,a[i-1]));
        }
        
        cout << lastans << '
    ';
      }
      
      return 0;
    }
    

    单点修改可持久化线段树及其应用

    一般不考虑支持区间修改的可持久化线段树, 因为标记下传很麻烦, 如果用标记永久化, 局限性又很大。
    实现可持久化线段树的算法和实现可持久化 Trie 的算法一模一样。

    可持久化线段树的单次插入和查询时间复杂度都是 (O(log n)), 单次插入的空间复杂度是 (O(log n))

    静态区间第k大
    在值域线段树上二分可以求值域的第 (k) 大, 把值域线段树可持久化,把序列从前往后依次插入可持久化值域线段树(其实就是把值域做了前缀和), 一段区间的值域线段树就变成了两个可持久化线段树的差。
    另外, 将值域离散化虽然对时间和空间都只有常数级别的优化, 但优化也是很明显的。

    #include<bits/stdc++.h>
    using namespace std;
    const int N = 100003;
    const int M = 10003;
    
    struct sgt{
    	int tot, ch[2000003][2], cnt[2000003], root[N];
    	void insert(int p, int &q, int l, int r, int x) {
    	  q = ++tot;
    	  ch[q][0]=ch[p][0], ch[q][1]=ch[p][1];
    	  if(l==r) {cnt[q]=cnt[p]+1; return;}
    	  int mid = (l+r)>>1;
    	  if(x<=mid) insert(ch[p][0], ch[q][0], l, mid, x);
    	  else insert(ch[p][1], ch[q][1], mid+1, r, x);
    	  cnt[q] = cnt[ch[q][0]] + cnt[ch[q][1]];
    	}
    	
    	int ask(int p, int q, int l, int r, int k) {
    	  if(l==r) return l;
    	  int mid = (l+r)>>1;
    	  int lcnt = cnt[ch[q][0]] - cnt[ch[p][0]];
    	  if(k<=lcnt) return ask(ch[p][0], ch[q][0], l, mid, k);
    	  else return ask(ch[p][1], ch[q][1], mid+1, r, k-lcnt);
    	}
    	
    } T;
    
    int n,m,a[N], b[N], row[N];
    
    int main() {
    	scanf("%d%d", &n,&m);
    	for(int i=1;i<=n;++i) scanf("%d", &a[i]), b[i]=a[i];
    	sort(b+1,b+1+n);
    	for(int i=1;i<=n;++i) {
    		int to = lower_bound(b+1,b+1+n,a[i]) - b;
    		row[to] = a[i];
    		a[i] = to;
        T.insert(T.root[i-1], T.root[i], 1, n, a[i]);
    	}
    	
    	while(m--) {
    	  int l,r,k;
    	  scanf("%d%d%d", &l,&r,&k);
    	  cout << row[T.ask(T.root[l-1], T.root[r], 1, n, k)] << '
    ';
    	}
    	
    	return 0;
    }
    

    可持久化并查集加强版
    acwing 的题面怎么这么神必啊, 建议看 luogu 的题面。
    这题就是用可持久化数组实现可持久化并查集, 用可持久化线段树实现可持久化数组。
    这题不能用路径压缩, 因为路径压缩的复杂度是均摊的,可以构造数据不断回到 对于某个操作需要高复杂度的版本,然后执行操作, 这样就可以把复杂度卡到爆炸。
    要用复杂度稳定的启发式合并来做, 查询稳定 (O(log^2 n)), 修改稳定增加 (O(log n)) 空间。

    
    
  • 相关阅读:
    Vue Highcharts 动态添加点
    JS Data(日期)对象
    VUE axios使用
    VUE 创建新项目
    Arduino ESP32 WiFi功能
    Arduino ESP8266 ESP8266mDNS.h
    Arduino ESP8266 ESP8266WebServer.h
    博客园定制自己的皮肤
    Arduino ESP32 BLE
    hadoop的伪分布安装(低版本)
  • 原文地址:https://www.cnblogs.com/tztqwq/p/13546458.html
Copyright © 2011-2022 走看看