/* 可持久化的迹象,我们俯身欣赏! ——《膜你抄》 */
引子
我们在生活中可能会遇到这样的问题,要是某一变化是基于某一个历史版本而来的变化。
这样处理的过程就比较困难。(然而对于暴力这个一点都不困难)
有什么是暴力算法解决不了的呢?
又有什么暴力算法是优化不了的呢?
我们分析暴力算法的复杂度(裸暴力我们就不说了)
考虑有点技术含量的暴力:我开M个数据结构维护每一时刻的版本然后在那一时刻的版本上做操作。
Hmm...复杂度是对了,但是空间呢?大概联赛给你开放整一个内存吧。(可能还不够。。。)
我们分析这样的劣势:就是把以前一样的东西重复记录的M次。
要是我们只改变有修改部分的结构就好了!!!
于是就有了可持久化数据结构:我们只记录原来数据结构中发生改变的副本,其他的留在原数据结构中。
从Trie树开始的可持久化之旅
这个首先得搞懂思想(请确保读懂上面相关知识)
在可持久化Trie树中插入一个元素的步骤一般如下:
- 设当前可持久化Trie树的根为lstrt(lastroot),插入元素以后的根为nowrt(nowroot)
- 建立一个新的节点(根)NowRoot
- 把nowrt下所有元素的指针所指信息置为lstrt中的所有指针信息(就是吧trie[nowrt][s]=trie[lstrt][s],s为字符所有可能,但是要除去当前字符信息)
- 对于当前字符信息重新维护,并新建节点new,trie[nowrt][now_char]=new(当前字符信息重新指)
- 令lstrt=trie[lstrt][now_char],nowrt=trie[nowrt][new_char] (重新准备迭代)
- 重复3-5至所有的new_char处理完毕。
这里是对于四个字符串依次插入可持久化Trie树的图,结合上述思想理解一下:
这四个字符串依次是:“abc","abd","abcd","bcd”.
这里是一个例题:P4735 最大异或和
Solution:显然需要考虑前缀xor和,记为s[i]
对于询问操作 l,r , x 就是询问一个位子p∈[l-1,r-1]使s[p] xor (x xor s[n])
如果只考虑右端点限制,那么就是直接从root[r-1] 访问进去贪心选取就行,
我们再次考虑左端点的限制,那么我们记lastest[x]表示指针所指元素位子为x时,最后面元素插入时经过这个点的最大的序号。
(如果没有元素经过这个点置为最小值-1:我们坚决不访问他)
然后询问时候考虑左端点限制是在处理下一个点去哪的位置是last值必须大于等于l-1才被认为有效点,
然后剩下的贪心(走和当前位相反或者走有元素那边)就行。
Hint:本题卡常#3和#6点建议打上O2优化、快读、还有别打那么多头文件...
# pragma G++ optimize(2) # include <cstdio> using namespace std; const int N=600010; int n,m,tot; int trie[N*24][2],lastest[N*24],root[N],s[N]; inline int max(int x,int y){return (x>y)?x:y;} inline int read() { static char c= getchar(); int a= 0; while(c < '0' || c > '9') c= getchar(); while(c >= '0' && c <= '9') a= a * 10 + c - '0', c= getchar(); return a; } void insert(int i,int dep,int lstrt,int nowrt) { if (dep<0) { lastest[nowrt]=i; return;} int c=(s[i]>>dep) & 1; if (lstrt!=0) trie[nowrt][c^1]=trie[lstrt][c^1]; trie[nowrt][c]=++tot; insert(i,dep-1,trie[lstrt][c],trie[nowrt][c]); lastest[nowrt]=max(lastest[trie[nowrt][0]],lastest[trie[nowrt][1]]); } int ask(int nowrt,int val,int dep,int mark) { if (dep<0) return s[lastest[nowrt]]^val; int c=(val>>dep) & 1; if (lastest[trie[nowrt][c^1]]>=mark) return ask(trie[nowrt][c^1],val,dep-1,mark); else return ask(trie[nowrt][c],val,dep-1,mark); } inline void write(int x) { if (x>9) write(x/10); putchar('0'+x%10); } int main() { n=read();m=read(); int t; lastest[0]=-1; root[0]=++tot; insert(0,23,0,root[0]); for (int i=1;i<=n;i++) { t=read(); s[i]=s[i-1]^t; root[i]=++tot; insert(i,23,root[i-1],root[i]); } char op[3]; for (int i=1;i<=m;i++) { scanf("%s",op); if (op[0]=='A') { int x=read(); root[++n]=++tot; s[n]=s[n-1]^x; insert(n,23,root[n-1],root[n]); } else { int l=read(),r=read(),x=read(); write(ask(root[r-1],x^s[n],23,l-1)); putchar('\n'); } } return 0; }
提高:可持久化 SegmentTree
这个题目其实就是可持久化思想的运用,我这里写了一个维护区间的max(什么都不维护感觉慎得慌)
我把它改了一下不影响题意!!!
/* 对于操作1:输入v,1,pos,val 在历史版本v中把pos位置的数改为val作为当前版本 对于操作2:输入v,2,l,在历史版本v中输出第l位置和第l位置之间的所有数的最大值,并把版本v作为当前版本 */
考虑怎么建立一棵可持久化SegmentTree,还是保留上面可持久化的思想,只维护有更改的那些线段。
其他的不做更改(直接链到对应节点就行)。
如图:
其实这个思想和前面的思想很像,但是此处和常规的线段树不同,他没有树形结构!!所以要数组模拟链表。
我们在每一个线段树的节点记录lc和rc作为左儿子节点编号和右儿子节点的编号,按照开节点的顺序编号就行。
为了节省空间,儿子的编号作为递归参数传递!
//建树 int build(int l,int r) { int p=++tot; if (l==r) { tree[p].val=a[l]; return p;} int mid=(l+r)>>1; tree[p].lc=build(l,mid); tree[p].rc=build(mid+1,r); tree[p].val=max(tree[tree[p].lc].val,tree[tree[p].rc].val); return p; }
建树的过程也不用赘述了吧。
然后是更改insert(now,l,r,x,val)操作表示当前在编号为now节点,当前区间为[l,r],然后吧x位置的值单点修改为val(即a[x]=val)
//更改 int insert(int now,int l,int r,int x,int val) { int p=++tot; tree[p]=tree[now]; if (l==r) { tree[p].val=val; return p;} int mid=(l+r)>>1; if (x<=mid) tree[p].lc=insert(tree[now].lc,l,mid,x,val); else tree[p].rc=insert(tree[now].rc,mid+1,r,x,val); tree[p].val=max(tree[tree[p].lc].val,tree[tree[p].rc].val); return p; }
注意到有个地方容易码错就是在递归insert的时候是从tree[now]出发而不是tree[p](否则不是自己到自己了吗)
但是不知道怎么过的三个点orz
接下来是query函数(这个和普通线段树大同小异)
//询问 int query(int now,int l,int r,int opl,int opr) { if (opl<=l&&r<=opr) return tree[now].val; int mid=(l+r)>>1; int val=-inf; if (opl<=mid) val=max(val,query(tree[now].lc,l,mid,opl,opr)); if (opr>mid) val=max(val,query(tree[now].rc,mid+1,r,opl,opr)); return val; }
鼓掌~~(就这么码完了)
(注意下:main函数调用子函数,and 版本编号初始为0,后面第i个询问都有基于前的新版本)
// luogu-judger-enable-o2 # include <cstdio> # define inf (0x7f7f7f7f) # define int long long using namespace std; const int N=1e6+10; int n,m,tot; int root[N*20],a[N]; struct Sem_Tree{ int lc,rc,val; }tree[N*20]; int max(int x,int y){return (x>y)?x:y;} inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } void write(int x) { if (x<0) putchar('-'),x=-x; if (x>9) write(x/10); putchar('0'+x%10); } int build(int l,int r) { int p=++tot; if (l==r) { tree[p].val=a[l]; return p;} int mid=(l+r)>>1; tree[p].lc=build(l,mid); tree[p].rc=build(mid+1,r); tree[p].val=max(tree[tree[p].lc].val,tree[tree[p].rc].val); return p; } int insert(int now,int l,int r,int x,int val) { int p=++tot; tree[p]=tree[now]; if (l==r) { tree[p].val=val; return p;} int mid=(l+r)>>1; if (x<=mid) tree[p].lc=insert(tree[now].lc,l,mid,x,val); else tree[p].rc=insert(tree[now].rc,mid+1,r,x,val); tree[p].val=max(tree[tree[p].lc].val,tree[tree[p].rc].val); return p; } int query(int now,int l,int r,int opl,int opr) { if (opl<=l&&r<=opr) return tree[now].val; int mid=(l+r)>>1; int val=-inf; if (opl<=mid) val=max(val,query(tree[now].lc,l,mid,opl,opr)); if (opr>mid) val=max(val,query(tree[now].rc,mid+1,r,opl,opr)); return val; } signed main() { n=read();m=read(); for (int i=1;i<=n;i++) a[i]=read(); root[0]=build(1,n); for (int i=1;i<=m;i++){ int id=read(),op=read(); if (op==1) { int pos=read(),v=read(); root[i]=insert(root[id],1,n,pos,v); } else { int pos=read(); int ans=query(root[id],1,n,pos,pos); root[i]=root[id]; write(ans);putchar('\n'); } } return 0; }
后面还有第二个稍微难一点的例题(区间第K小数)