可持久化線段————主席樹(洛谷p3834)

不学算法的小明發表於2024-08-14

洛谷P3834 可持久化線段樹 2

問題描述:

  • 給定n各整數構成的序列,求指定區間[L,R]內的第k小值(求升序排序後從左往右數第k個整數的數值)

輸入:

  • 第一行輸入兩個整數n,m,分別代表序列長度n和對序列的m次查詢;
  • 第二行輸入n個整數,表示序列的n個整數;
  • 之後的m行,每行輸入3個整數L,R,k,表示查詢[L,R]內的第k小值;

輸出:

  • 對於每個查詢,輸出查詢區間內的第k小值;

資料範圍:

  • 1 ≤ n, m ≤ 2×\(10^5\);
  • \(a_i\)\(10^9\);
  • 1 ≤ L≤ R ≤ n;
  • 1 ≤ k ≤ R-L+1;
    //第一次寫mathjax,東找西找. 。·*༙ ✟ 昇天 ✟ *༙· 。.

分析問題:
//以前學了點css總算用上了
題目這麼直白,肯定是不能用暴力搜尋先排序後定位總共m次查詢算下來複雜度為O(mn\(log_2\)n),顯然超時(別問我為什麼,問就是不會,我聽大佬說的!!)

本題考慮使用線段樹求解,但使用線段樹求解問題的時候,需要滿足大區間的解可以由小區間的解合併而來,也就是我們經常說的線段樹葉子節點與根節點的關係,但是區間中第k小的問題似乎並不符合這一特徵,總不能說得到左右兩個孩子節點區間的第k小數(也就是兩個小區間的解,就兩個數),可以得出他們所組成的大區間的第k小數吧

但我們仔細想想,既然不能讓第k小數成為每個區間的解,也就是利用線段樹無法直接得到答案,我們或許可以換個角度入手,讓兩個線段樹相減得到新的線段樹,而新線段樹對應了新區間的解

下面我們來逐步推出可持久化線段樹的解題思路

  1. 既然是線段樹相減,我們首先要搞清楚的是哪來的這麼多線段樹,這些線段樹都想表達什麼意思。先問個問題,該如何利用線段樹求解第k小問題,比如給你一個區間[1,i],如何求第k小元素

  2. 我們回顧下正常方法:先將區間內的所有元素排序,讓後數個數,從左往右數第k個就是答案。這裡可以資料化的有序列元素,元素個數,k,對映到線段樹,編寫線段樹首先要弄清楚線段樹區間解是什麼,葉子節點代表什麼。前面以經分析k無法成為線段樹的區間解,顯然序列元素也不行,那麼試試元素個數,那麼葉子節點代表排好序的序列。以序列{2,1,4,3}為例,如圖:

如需查詢[1,3]內的第k小元素根節點3為區間內元素總數,當k=2時從根節點出發左孩子節點個數為2,由於葉子節點是有序的所以k≤2,則說明區間中第k小的樹在左孩子節點;但如果第k小數在右孩子節點,當k=3時,k>2,此時k需要發生變化,不可能在右孩子節點上查詢第3小的數(總共才一個數),數的左邊已經有兩個數了,查詢必須在左邊的基礎上查,也就是在右邊查詢第k-2=1小的數,根據這個方法向下推直到葉子節點即可找到答案。

  1. 既然明白了求[1,i]區間第k小數問題的方法,那麼如何求區間[L,R]內第k小數呢,有沒有覺得這個問題很熟悉,對!就是字首和思想,我們求[L,R]可以利用[1,L-1]區間的線段樹-[1,R]區間的線段樹,從而得到[L,R]區間的線段樹,這在邏輯上是成立的,應為只是元素個數的相減,得出的答案任然是元素個數,然後利用求解[1,i]區間線段樹第k小數的方法求解。
    例如:求區間[2,4]的線段樹,等於把第四個線段樹與第一個線段樹相減(對應圓圈中的數字相減)

  2. 上點中的方法我們似乎要建立很多棵線段樹,是的,這就是可持續化線段樹,也就是常說的主席樹。但這樣就可以得到答案嗎,前面說到將排序好的序列作為線段樹的葉子節點而已知每個節點的值為當前節點及節點下孩子節點元素個數的總和,那麼葉子節點的值不是0就是1,好像也不能作為答案,那麼就只有區間可以作為答案,在葉子節點中pl=pr,而我們的例子中剛好pl=pr=答案,有同學肯定會說,你這是故意的,這個例子太特殊了。確實如果序列中的數不是連續的,比如{100,200,50,6000000},那麼我每棵線段樹葉子節點是不是要有6000000個,(只有四個葉子節點值為1)意味著總共4×6000000×2(×2的意思是加上了葉子節點上方的根節點,用等比數列算一算,差不多就是這個數)非常浪費空間。很容易想到利用離散化(求序列的第k小值與元素本身的大小其實並沒有關係,只與元素之間的相對大小有關)將分佈廣而稀疏的資料轉化為密集分佈,從而使演算法更快速更省空間地處理。

  3. 但使用離散化也有需要注意的要點————有關重複元素的處理。如序列{1,5,5,6,7},序列中第3小的元素不是6,而是5,說明重複元素也要計數,如何處理,擺在面前的有兩個方法,1.在葉子節點上。2.線上段樹建立個數上。我直接給答案了,第一種方法不行,在葉子節點上反正我是不知道這麼做。序列中總共n個元素,建立n棵線段樹[1,i],i從1到n。編碼時對n個元素離散化,並用unique()函式去重得到size個不同的元素,每棵線段樹中葉子節點的個數為size。其中重複的數字線段樹結構不同,由於線段樹每個節點的值為節點下孩子的個數,後面的5比前面的5路徑上會多1(之後看程式碼理解,這個我不好表達)。

時間,空間的壓榨
講清楚思路之後,思考下如何編碼才可以最大限度地壓榨時間和空間
兩棵樹相減真的需要所有節點都相減嗎,仔細觀察[2,4]線段樹得出的過程,會發現只需要對查詢路徑上的節點及左右區間做減法即可,簡單說就是隻需計算查詢過程中使用節點即可,因為查詢得到答案的過程也不是所有節點都會用上,一邊更新一邊查詢(之後看程式碼你會理解)

真的需要建立n棵完整的線段樹嗎,和上面道理一樣,既然查詢過程中都用不上的節點為什麼還要建立,但需要注意的是:查詢區間不同,需要計算的節點也會不同。所以線段樹我們改建還得建,但我們只建立一一棵完整的線段樹,仔細觀察上面建立的四棵線段樹,會發現相鄰線段樹長得非常像,他們對應區間只相差一位數,其實線段樹相差的那部分與這位數是有關係的,如果該位數從根節點出發到葉子節點會產生一臺路徑,會發現這條路徑上的所有節點都與另一棵樹上對應的節點相差1,所以我們完全可以只建立這條路徑上的節點,剩下的節點與另一棵樹共用,保證其邏輯上的完整性即可

程式碼

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 20010;
int cnt = 0;  //對根節點計數
int a[N], b[N], root[N];  //a儲存原陣列,b複製陣列,root儲存根節點
struct {
	int L, R, sum;  //sum記錄該子樹根節點下有幾個元素
}tree[N<<5]; //為什麼是N<<5,這個我是真不會
int build(int pl,int pr){
	//該函式為建立初始線段樹(可建可不建) 因為再建立有元素線段樹的同時,之後的線段樹結構會趨於完整,前面的線段樹即使不完整也不影響其功能,因為不完整的部分不會被使用
	int rt = cnt++;  //每新增一個根節點,則需要增加計數,相當於為該線段樹申請空間
	tree[rt].sum = 0; //還未新增任何元素,所以所有根節點下元素個數都為零
	//類似於dfs的演算法
	int mid = (pl + pr) >> 1;
	if (pl < pr) {
		//遞迴的同時開闢左右子樹
		tree[rt].L = build(pl, mid);
		tree[rt].R = build(mid + 1, pr);
	}
	return rt;  //返回下一位根節點的索引
}

int update(int pre,int pl,int pr,int x) {
	//更新的同時也在建立各元素的線段樹
	int rt = ++cnt;
	tree[rt].L = tree[pre].L; //將其與另一棵樹的其他節點相連保證其邏輯上的完整性
	tree[rt].R = tree[pre].R;
	tree[rt].sum = tree[rt].sum + 1;  //新增沿路節點
	//有點像二分查詢
	int mid = (pl + pr) >> 1;
	if (pl < pr) {
		if (x < mid) {
			update(tree[pre].L, pl, mid, x);
		}
		else {
			update(tree[pre].R, mid + 1, pr, x);
		}
	}
	return rt; //返回建立的該線段樹的根節點
}

int query(int u, int v, int pl, int pr, int k) {
	//返回搜尋結果的索引,注意這裡是b陣列的索引
	//這裡沒有使用cnt變數,實際上並沒有建立[L,R]區間的線段上,我們的目的只是為了找到一條路徑指向第k小數即可
	if (pl == pr) return pl;
	int x = tree[tree[v].L].sum - tree[tree[u].L].sum;
	int mid = (pl + pr) >> 1;
	if (x >=k){
		return query(tree[u].L, tree[v].L, pl, mid, k);
	}
	else {
		//注意這裡是k-x
		return query(tree[u].R, tree[v].L, mid + 1, pr, k-x);
	}
}

int main() {
	int n, m; cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		b[i] = a[i];
	}
	sort(b + 1, b + 1 + n);
	//離散化標準操作
	int size = unique(b + 1, b + 1 + n) - b - 1;
	root[0] = build(1, size);  //一棵樹的尺寸是size,陣列中不相同數字的個數
	for (int i = 1; i <= n; i++) {
		//建立n棵樹
		//注意這裡是-b
		int x = lower_bound(b + 1, b + 1 + n,a[i]) - b;

		root[i] = update(root[i - 1], 1, size, x);
	}
	while (m--) {
		int x, y, k; cin >> x >> y >> k;

		int t = query(root[x - 1], root[y], 1, size, k);
		//別忘了把索引的值列印出來
		cout << b[t] << endl;
	}
	return 0;
}

第一篇部落格,標記一下***

相關文章