1. 程式人生 > 資訊 >realme Book 預熱:3:2 比例高素質螢幕

realme Book 預熱:3:2 比例高素質螢幕

可持久化線段樹的經典用途是靜態區間第k大。它充分利用重複資訊,用相對較少的空間實現了可持久化。

可持久化資料結構對空間有要求,其優點是充分利用了已經“記住”的資訊

一、 可持久化陣列

P3919 【模板】可持久化線段樹 1(可持久化陣列)中這樣說:

“如題,你需要維護這樣的一個長度為 \(N\) 的陣列,支援如下幾種操作

  1. 在某個歷史版本上修改某一個位置上的值;

  2. 訪問某個歷史版本上的某一位置的值.

此外,每進行一次操作(對於操作2,即為生成一個完全一樣的版本,不作任何改動),就會生成一個新的版本。版本編號即為當前操作的編號(從1開始編號,版本0表示初始狀態陣列)。

“一個完全一樣的版本”,首先想到的是——全部記下來,也就是每一次更新,都把陣列做一次 memcpy() 操作。這樣做的代價,不提複製所用的時間,空間複雜度也會增長至 \(O(nm)\)

量級,陷入MLE的深淵裡無法自拔

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()

操作,第二個引數傳的是 \(k-x\) 。這與 \(Splay\) 中求 \(kth\) 的方法十分相近。

#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;
}

THE END