1. 程式人生 > >可持久化線段樹 & 主席樹 || 超詳細解釋 + 模板

可持久化線段樹 & 主席樹 || 超詳細解釋 + 模板

心血來潮 把這個基礎演算法結構補了

吶 先了解一下 可持久化線段樹 是什麼

自然是 可持久化 + 線段樹 啦 多用於詢問第m次修改後 某 節點 || 區間 的 值

線段樹自然是很好理解的(這個不知道就去補一下吧)

然而可持久化怎麼弄呢 總不能每次都copy整棵樹吧 不然時空複雜度都打得要死

因此 聰明的靈長類動物——裸猿人類們啊 發現

在修改一個 節點 || 區間 時啊 改變的只有他的祖先們

因此 我們只需要將該 節點 || 區間涉及的點 和 他們的祖先 複製一遍 賦上修改後該 節點 || 區間 的值 即可

首先是 單點修改(區間修改全靠自覺啦) 具體細節見程式碼 題目仍舊是洛谷的模板

Tip1:單點 修改 && 查詢 建樹只需將序列二分下去 區間記錄 左兒子 右兒子 節點上記錄 點權值

Tip2:儲存 節點 和 區間 的 陣列 大概開 樹上節點數的20倍(雖然本題我看有人開10倍 但我爆了 超凶的 =-=)

下面是程式碼加註釋 真的超詳細的=w=

#include <iostream>
#include <cstdio>
using namespace std;
const int MAX = 20000010;
struct Persistable_Segment_tree
{
	int ls,rs,v;//分別是該位置代表的節點或區間的左兒子右兒子和權值
} tr[MAX];
int edit[1 << 20] = {1},w[1 << 20],tot;//edit儲存節點版本 注意第一個(下標0)賦值為1表第一個版本 w儲存點初始權值
int build(int l,int r)
{//此處不能直接用++tot代替pos 因為跳轉到子程式中繼續搜尋下去 tot值會增加 而此處值代表當前節點編號 應不變
	int pos = ++tot;//tot是當前陣列末尾的位置 ++tot則是在末尾處新建儲存節點或區間的相關資訊 充分利用空間OwO真是太厲害了
	if (l == r)//區間左右相等 即只包括一個點 則只存點權
	{
		tr[pos].v = w[l];//記錄初始點權
		return pos;//pos是當前節點的編號 需返回 用以讓其父親節點記錄他
	}
	int mid = (l + r) >> 1;//二分存中點
	tr[pos].ls = build(l,mid);//記錄當前節點的左兒子編號
	tr[pos].rs = build(++mid,r);//記錄當前節點的右兒子編號
	return pos;//返回當前節點編號 需返回 用以讓其父親節點記錄他
}
int update(int ed,int l,int r,int p,int k)//在ed版本的基礎上 修改p點權值為k 記錄當前區間最左&&最右端的點l&&r 
{//此處不能直接++tot代替pos 因為跳轉到子程式中繼續搜尋下去 tot值會增加 而此處值代表當前節點編號 應不變
	int pos = ++tot;//記錄當前節點編號 充分利用空間OwO真是太奇妙了
	if (l == r)//當搜尋到單個節點了
	{
		tr[pos].v = k;//記錄修改後節點權值
		return pos;//返回當前節點編號 讓當前版本的父親記錄他
	}
	tr[pos].ls = tr[ed].ls;//將之前的該節點左兒子複製 (引用-->「把子節點指向前驅節點以備複用」)
	tr[pos].rs = tr[ed].rs;//將之前的該節點右兒子複製 因為之後只會改變兩兒子之一的值 這樣子可以確定該節點位置
	int mid = (l + r) >> 1;//二分存中點
	if (p <= mid) tr[pos].ls = update(tr[ed].ls,l,mid,p,k);//向下尋找 逼近p點 更改pos點的左兒子
	else tr[pos].rs = update(tr[ed].rs,++mid,r,p,k);//向下尋找 逼近p點 更改pos點的右兒子 用tr[ed]的原因是此時tr[pos]只有1深度的孩子的值
	return pos;//返回pos pos作為該點父親的某個兒子的位置 用以記錄
}
int found(int ed,int l,int r,int p)
{//ed是 某版本 儲存區間1~n的值 的位置
	if (l == r) return tr[ed].v;//找到該點 此時ed已經變為 記錄當前版本的p點的位置了 其v則是當前版本的p點的權值 返回
	int mid = (l + r) >> 1;
	if (p <= mid) return found(tr[ed].ls,l,mid,p);//向下尋找 逼近p點 ed變為ed的左兒子
	else return found(tr[ed].rs,++mid,r,p);//向下尋找 逼近p點 ed變為ed的右兒子
}
int main()
{
	int n,m,edition,mode,node,weight;//恪盡職守的變數定義
	scanf("%d%d",&n,&m);//發人深省的範圍輸入
	for (int a = 1 ; a <= n ; a ++) scanf("%d",&w[a]);//循規蹈矩的節點輸入
	build(1,n);//建樹 從區間 1 ~ n 開始遞迴 找左右兒子
	for (int a = 1 ; a <= m ; a ++)//循序漸進的命令處理
	{
		scanf("%d%d%d",&edition,&mode,&node);//五花八門的命令輸入
		if (mode % 2)//巧妙絕倫的判斷
		{
			scanf("%d",&weight);//撲朔迷離的補充輸入
			edit[a] = update(edit[edition],1,n,node,weight);//update解釋見子程式
		}//以update此時求出tr陣列的末尾 edit[a]意為在第a個版本時修改的點為edit[a-1]到edit[a]的點(上面那行程式讓本人想了很久很久)
		else//機智無比的轉折
		{
			edit[a] = edit[edition];//因為複製沒有建立新節點 因此當前版本的所有點等於當前版本(不是第a-1的版本)之前的所有點
			printf("%d\n",found(edit[edition],1,n,node));//輸出查詢某edition的某node的值
		}
	}
	return 0;//逢考必備的結尾
}

其實這行數似乎比線段樹的程式碼還少=-=這世道究竟......

好了接下來是 主席樹 不會講的太詳細哈 =-=

2018.9.8 經過一個多月偶爾的攻關 Frocean 還是決定向 STL 低下了頭 QAQ

2018.10.20 經過 Frocean 暗度陳倉的努力 最終還是趕跑了 大惡魔 STL

主席樹是用到了字首和的思想 =-=

然而一開始看網上各位 dalao 的解釋完全不懂 然後通過各種神奇的方式弄清了——

首先套用一下 “ 對於原序列的每一個字首 [ 1 ... i ] 建立出一棵線段樹維護值域上每個數出現的次數,則其樹是可減的 ”

什麼鬼東西 原序列哪裡有字首 不是輸入一個個權值嘛 而且每個字首建一棵樹 這是要爆炸

(輕蔑) 事實上這個的意思是......我還是舉個例子吧

首先我們要把所有數的邊界弄出來做線段樹的邊界 (什麼線段樹啊 都沒說清楚) 別急 說好了舉例子的

例如我們一個數列 5 個數 互不相同 比如 233 19260817 6666 19491001 和 1000000007 五個數

最小最大是什麼? 好找到了我們拿他們做線段樹的邊界......等等這會死人的啊

於是離散一下 =-= 變成 1 3 2 4 和 5 到時候求答案轉換回去即可

好了線段樹怎麼造呢 看我強迫症畫了十五分鐘的圖 (畫圖太慢所以基本不畫OvO)

差不多了吧 =-= 意思很清楚

每次讀入一個數 然後包含他離散後數值的樹的區間 size 都加一 相當於單點修改

如果求區間 l 到 r 的 最小值 就用 第 r 次更新後樹的形態 減去 第 l - 1 (記住是 l - 1) 次更新的樹的形態

因此我們不能在空間只有 n 的 線段樹上做這事......之前不是有可持久化線段樹嘛

那我們對於每次更新到的點 像可持久化線段樹一樣 把加入第 i 個點當成第 i 個版本 然後同樣多加一堆節點就好了 然後就沒有然後了哈

話說網上那些圖都不畫單數個節點的真的是煩躁啊......

Tip1 : 初始離散化 + 去重 這裡用手打的但是沒去重 (手打去重我過不了QAQ)

Tip2 : 主席樹建樹先確定邊界 然後只用記每個點的左兒子和右兒子哈 權值到時候搜到第 k 大的時候返回點位置即可 (多個相同也不怕 看判斷方法)

Tip3 : 主席樹插入大概和上面的那樹差不多 於是關於 edit 和 ed 的 自覺上翻

Tip4 : 主席樹查詢很重要 細看哦 沒有細講主要是懶得畫圖......

洛谷的模板戳這裡 還有註釋'可能'在程式碼裡面哦 我對我的碼風莫名自信~

#include <algorithm>
#include <cstdio>
using namespace std;
const int MAXN = 200010;
int v[MAXN],bot[MAXN],newv[MAXN],edit[MAXN],tot;
//v存原點權 bot存點id (此處接sort處註釋) 
struct node {
    int l,r,siz; //l r 找位置 然後 siz 就是當前區間內的數的數量
} tr[MAXN << 5];
inline short cmp(int x,int y) {return v[x] < v[y];}
//通過比較v陣列大小讓bot當作排好序 
inline int r()
{
	char q = getchar(); int x = 0;
	while (q < '0' || q > '9') q = getchar();
	while ('0' <= q && q <= '9') x = (x << 3) + (x << 1) + q - (3 << 4),q = getchar();
	return x;
}
void insert(int ed,int l,int r,int i)
{ //同 可持久化線段樹 但是有點修改 不過原理一樣的
	++tr[ed].siz;
	if (l == r) return;
	int mid = (l + r) >> 1;
	if (i <= mid)
	{
		tr[++tot] = tr[tr[ed].l]; // 這種是l r siz一起更新的 省位置
		tr[ed].l = tot;
		insert(tr[ed].l,l,mid,i);
		return;
	}
	tr[++tot] = tr[tr[ed].r];
	tr[ed].r = tot;
	insert(tr[ed].r,++mid,r,i);
}
int out(int l,int r,int i,int j,int k)
{ // 查到點了返回點權 其實可以放在最外層..但我太懶了
	if (l == r) return v[bot[l]];
	int mid = (l + r) >> 1;
	int ex = tr[tr[j].l].siz - tr[tr[i].l].siz;
	//important ex是當前區間左兒子區間的size值 這裡用到了二叉查詢樹的思想
	if (ex >= k) return out(l,mid,tr[i].l,tr[j].l,k);
	return out(++mid,r,tr[i].r,tr[j].r,k - ex);
} //減去後是剩下要查的區間裡第k大的點數 之前是之前(相對的)全部區間裡第k大的點數
int main()
{
	int n = r(),m = r();
	for (int a = 1 ; a <= n ; ++ a) v[a] = r(),bot[a] = a;
	sort(bot + 1,bot + n + 1,cmp);
	for (int a = 1 ; a <= n ; ++ a) newv[bot[a]] = a;
	for (int a = 1 ; a <= n ; ++ a)
	{
		edit[a] = ++tot; //當前版本樹開頭接上上個節點的編號(+1) 
		tr[tot] = tr[edit[a - 1]]; //這個如果接版本 0 完全沒問題 
		insert(edit[a],1,n,newv[a]); //更新 確定子樹有範圍不怕 
	}
	while (m--)
	{
		int i = r(),j = r(),k = r();
		printf("%d\n",out(1,n,edit[i - 1],edit[j],k)); //處理詢問 
	}
	return 0;
}