浅谈简单可持久化数据结构及其应用

参考资料

《浅谈可追溯化数据结构》————孔朝哲 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) << '\n';
        continue;
      }
      cout << ques(r-1, l-1, x ^ las) << '\n';
    }
  }
  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 << '\n';
  }
  
  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)] << '\n';
	}
	
	return 0;
}

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


posted @ 2020-08-23 15:32  xwmwr  阅读(374)  评论(7编辑  收藏  举报