realme Book 預熱:3:2 比例高素質螢幕
可持久化資料結構對空間有要求,其優點是充分利用了已經“記住”的資訊。
一、 可持久化陣列
P3919 【模板】可持久化線段樹 1(可持久化陣列)中這樣說:
“如題,你需要維護這樣的一個長度為 \(N\) 的陣列,支援如下幾種操作
-
在某個歷史版本上修改某一個位置上的值;
-
訪問某個歷史版本上的某一位置的值.
此外,每進行一次操作(對於操作2,即為生成一個完全一樣的版本,不作任何改動),就會生成一個新的版本。版本編號即為當前操作的編號(從1開始編號,版本0表示初始狀態陣列)。
“一個完全一樣的版本”,首先想到的是——全部記下來,也就是每一次更新,都把陣列做一次 memcpy()
操作。這樣做的代價,不提複製所用的時間,空間複雜度也會增長至 \(O(nm)\)
However, 為什麼要全複製一遍呢?一個顯然的方法是,只把改變的點複製出來,或者說,對於改變的的節點,在運算時動態建立一個新的,保留原來的作為歷史版本。 這樣,空間複雜度(以線段樹為例,每次修改 \(log_n\) 個點)就只有 \(O(n*4+q*log_n)\) 了。
在程式碼實現的時候,我們實際上建立起了 \(q\) 棵線段樹,但是對於沒有修改的兒子,直接把指標指向左邊那棵線段樹對應的位置——這樣建立起來的線段樹,一般來說,只有最開始的那棵是完整的,而右側的都是有獨立的根、但依附連線於其上的附著物。
二、 靜態區間第k小
看到第k小的字樣,一下子想起平衡樹。主席樹有時確實能實現與平衡樹相似的功能,但她們的本質卻是大相徑庭。
P3834 【模板】可持久化線段樹 2(主席樹)裡這樣描述:
“如題,給定 \(n\) 個整數構成的序列 \(a\) ,將對於指定的閉區間 \([l,r]\) 查詢其區間內的第 \(k\) 小值。”
我們建立一棵主席樹,他維護的是“值域”,即所謂權值線段樹。每讀入一個數,就做一次 update()
以加入,留下了 \(n\) 個版本。線段樹 \(i\) 每個點的 \(t[p].val\) 值為 \([1,i]\) 範圍內,\([t[p].l,t[p].r]\) 內有多少個不同的數字。按照字首和方式計算出 \(x\) 與 \(mid\) 相比較,並確定向左還是右遞迴。
下面是程式碼。要特別注意,對於右子樹的 update()
#include<stdio.h>
#include<algorithm>
const int N=2e5+10;
struct ZldTree{int ls,rs,val;}t[N<<5];
int n,q,m,tot,a[N],b[N],root[N];
inline int rd(){
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f^=1;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
return f?x:-x;
}
inline int New(int p){
t[++tot]=t[p];
++t[tot].val;
return tot;
}
int build(int l,int r){
int p=++tot;
if(l==r) return p;
int mid=(l+r)>>1;
t[p].ls=build(l,mid);
t[p].rs=build(mid+1,r);
return p;
}
int update(int p,int l,int r,int x){
p=New(p);
if(l==r) return p;
int mid=(l+r)>>1;
if(x<=mid) t[p].ls=update(t[p].ls,l,mid,x);
else t[p].rs=update(t[p].rs,mid+1,r,x);
return p;
}
int query(int u,int v,int l,int r,int k){
if(l>=r) return l;
int x=t[t[v].ls].val-t[t[u].ls].val;
int mid=(l+r)>>1;
if(x>=k) return query(t[u].ls,t[v].ls,l,mid,k);
else return query(t[u].rs,t[v].rs,mid+1,r,k-x);
}
int main(){
n=rd(),q=rd();
for(int i=1;i<=n;++i) a[i]=b[i]=rd();
std::sort(b+1,b+n+1);
m=std::unique(b+1,b+n+1)-b-1;
root[0]=build(1,m);
for(int i=1;i<=n;++i){
a[i]=std::lower_bound(b+1,b+m+1,a[i])-b;
root[i]=update(root[i-1],1,m,a[i]);
}
while(q--){
int l=rd(),r=rd(),k=rd();
printf("%d\n",b[query(root[l-1],root[r],1,m,k)]);
}
return 0;
}
三、 可持久化並查集
模板題是P3402 可持久化並查集。
世界上根本沒有所謂“可持久化”的並查集,有的只是用主席樹模擬的並查集。這與前面兩部分一脈相承。
題目要求:
“給定 \(n\) 個集合,第 \(i\) 個集合內初始狀態下只有一個數,為 \(i\) 。
有 \(m\) 次操作。操作分為 \(3\) 種:
1 a b
合併 \(a,b\) 所在集合;
2 k
回到第 \(k\) 次操作(執行三種操作中的任意一種都記為一次操作)之後的狀態;
3 a b
詢問 \(a,b\) 是否屬於同一集合,如果是則輸出 \(1\) ,否則輸出 \(0\) 。”
考慮用主席樹維護每個並查集裡每個節點的父親關係。平時寫的一行並查集 inline int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
運用了路徑壓縮演算法,大大減小了時間消耗。但是,路徑壓縮的並查集不利於可持久化。為了方便模擬,可持久化的並查集不路徑壓縮。
在尋找 \(father\) 時,我們要做的就是在主席樹上找到點 \(u=find(a)\) 和 \(v=find(b)\) ,分別向上跑直到 \(root\) ,然後把前者的父親設為後者,像普通版一樣。
不路徑壓縮……似乎還有問題:就像BST的退化,並查集也可能退化為一條長鏈,時間複雜度再次崩潰。為了解決這一問題,我們採取啟發式合併,即把最大深度最小的連通塊往最大深度大的上面合併。證明。
思考:程式碼中為什麼 update(root[i],t[u].fa,t[v].fa)
?
程式碼如下:
#include<stdio.h>
const int N=2e5+10;
struct ZldTree{int l,r,lson,rson,fa,dep;}t[N<<4];
#define ls t[p].lson
#define rs t[p].rson
#define mid ((t[p].l+t[p].r)>>1)
int n,m,tot,root[N];
inline int rd(){
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f^=1;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
return f?x:-x;
}
inline void swap(int &x,int &y){x^=y^=x^=y;}
inline int New(int p){
t[++tot]=t[p];
return tot;
}
int build(int l,int r){
int p=++tot;
t[p].l=l,t[p].r=r;
if(l==r){t[p].fa=l;return p;}
t[p].lson=build(l,mid);
t[p].rson=build(mid+1,r);
return p;
}
int update(int p,int u,int v){
p=New(p);
int l=t[p].l,r=t[p].r;
if(l==r){t[p].fa=v;return p;}
if(u<=mid) t[p].lson=update(ls,u,v);
else t[p].rson=update(rs,u,v);
return p;
}
int query(int p,int x){
int l=t[p].l,r=t[p].r;
if(l==r) return p;
if(x<=mid) return query(ls,x);
else return query(rs,x);
}
void add(int p,int x){
int l=t[p].l,r=t[p].r;
if(l==r){++t[p].dep;return;}
if(x<=mid) add(ls,x);
else add(rs,x);
}
int find(int rt,int x){
int v=query(root[rt],x);
if(t[v].fa==x) return v;
return find(rt,t[v].fa);
}
int main(){
n=rd(),m=rd();
root[0]=build(1,n);
for(int i=1,opt,k,a,b;i<=m;++i){
opt=rd();
root[i]=root[i-1];
if(opt==1){
a=rd(),b=rd();
int u=find(i,a),v=find(i,b);
if(t[u].fa==t[v].fa) continue;
if(t[u].dep>t[v].dep) swap(u,v);
root[i]=update(root[i],t[u].fa,t[v].fa);
if(t[u].dep==t[v].dep) add(root[i],t[v].fa);
}
else if(opt==2){
k=rd();
root[i]=root[k];
}
else{
a=rd(),b=rd();
int u=find(i,a),v=find(i,b);
if(u==v) putchar('1');
else putchar('0');
putchar('\n');
}
}
return 0;
}