「線段樹學習筆記」


前言

寫得確實挺爛的,當時碼風也不怎么樣,不喜勿噴.

線段樹

線段樹的作用&原理

線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間划分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。
使用線段樹可以快速的查找某一個節點在若干條線段中出現的次數,時間復雜度為O(logn)。而未優化的空間復雜度為2n,實際應用時一般還要開4n的數組以免越界,因此有時需要離散化讓空間壓縮。

以上內容抄自百度百科.

在這里插入圖片描述

一般來說線段樹長這樣(畫的有點丑),每一個節點(非葉節點)都是從它的兩個子樹的根節點合並而成,所以線段樹維護的值必須支持合並(具體下面會講到),對於每一次的修改可以直接修改若干顆子樹,可以證明最多修改的子樹不會超過\(\log_2n\)棵,所以每次的修改的時間復雜度為\(\log_2n\),查詢同理.

可以在 \(\mathcal{O}(\log_2n)\) 的復雜度內修改和查詢一定的信息(如區間加一個數,查詢區間和,查詢區間最大值等).

線段樹的實現

define

以下為本文中絕大部分define.

#define LSON now*2;
#define RRSON now*2+1
#define MIDDLE (left+right)/2
#define LEFT LSON,left,MIDDLE
#define RIGHT RRSON,MIDDLE+1,right
#define now now_left,now_right

先從最基礎的開始例題1

合並信息

線段樹最先要寫的部分是合並信息.(個人習慣)

這里要維護的只有一個區間和,那么合並就很簡單了.

void PushUp(int now)
{
	sgt[now].sum=sgt[LSON].sum+sgt[RRSON].sum;
}

建樹

寫完合並,接下來要建樹.(不是所有的線段樹都需要建樹)

建樹時最多也就只會有 \(n*4\) 個節點,所以建樹的復雜度是 \(\mathcal{O}(n)\) 的.

void Build(int now=1,int left=1,int right=n)//建樹的初始值固定
{
	if(left==right)//葉節點時直接賦值
	{
		sgt[now].sum=arr[left];
		return;
	}
	Build(LEFT);//建左子樹
	Build(RIGHT);//建右子樹
	PushUp(now);//建完后需要合並
}

Lazy標記&PushDown

lazy標記可以說是線段樹的精髓所在,可以讓修改只有當查詢到或者再次修改時才會真正去修改值,使得復雜度大大降低.

在這里插入圖片描述

如圖,需要修改藍色區域的值,那么它覆蓋的部分為紅色的兩顆子樹,但是綠色的位置的值也發生了改變,這時就需要在紅色位置打上lazy標記,lazy標記也要支持合並,在以后的修改時需要查詢(修改)到綠色部分時才會將標記下傳.

在這里插入圖片描述

如圖,需要查詢紫色部分的值,那么如果需要查詢藍色部分的那兩顆子樹的值,這時就需要將紅色位置的標記下傳,得到藍色部分的真實的值.

void Down(int now,int left,int right,int lazy_)//修改子樹
{
	sgt[now].sum+=(right-left+1)*lazy_;//子樹表示的值需要加上子樹長度*每個數增加的值
	lazy[now]+=lazy_;//子樹懶標記增加
}
void PushDown(int now,int left,int right)
{
	Down(LEFT,lazy[now]);//修改左子樹
	Down(RIGHT,lazy[now]);//修改右子樹
	lazy[now]=0;//lazy標記必須清空
}

修改

修改部分並沒有什么可以說的,只需要將區間內最上層的所有節點都打上標記就可以了.

void Updata(int now_left,int now_right,int num,int now=1,int left=1,int right=n)
//其中的now,left,right一般不會變,所以就缺省了
{
	if(left>now_right||now_left>right)return;//如果需要查詢的區間與當前區間沒有公共位置則推出
	if(now_left<=left&&right<=now_right)//如果需要查詢的區間包含了當前區間可以直接修改
	{
		sgt[now].sum+=num*(right-left+1);
		lazy[now]+=num;//注意修改懶標記
		return;
	}
	PushDown(now,left,right);//需要推一下懶標記為了下方的修改
	Updata(now,num,LEFT);//修改左子樹
	Updata(now,num,RIGHT);//修改右子樹
	PushUp(now);//合並
}

查詢

查詢與修改類似.

int Query(int now_left,int now_right,int now=1,int left=1,int right=n)
{
	if(left>now_right||now_left>right)return 0;//不在范圍內
	if(now_left<=left&&right<=now_right)//包含了
	{
		return sgt[now].sum;
	}
	PushDown(now,left,right);//下傳懶標記
	return Query(now,LEFT)+Query(now,RIGHT);//需要將左右子樹的值相加
}

權值線段樹

權值線段樹的的原理&作用

權值線段樹其實很簡單,其實就是下標為值域的一顆線段樹.

在這里插入圖片描述

可以很容易得出整個數列第k大值,只需要在樹上二分就行了.

也可以處理一些區間中出現次數最多的數的個數之類的問題.
例題

權值線段樹的代碼實現

單點修改

void Updata(int num,int now=1,int left=1,int right=n)
{
	if(num>right||num<left)
	{
		return;
	}//按這個數的大小左右二分,最多Logn次就到葉節點,所以每次修改的時間復雜度為O(logn)
	sgt[now]++;//可以直接修改
	if(left==right)
	{
		return;
	}
	Updata(num,LEFT);
	Updata(num,RIGHT);
}

查詢第k大

int Query(int num,int now=1,int left=1,int right=n)
{
	if(left==right)//到葉節點說明找到了
	{
		return left;
	}
	if(sgt[LSON]>=num)
	{
		return Query(num,LEFT);//如果在左子樹就往左子樹找
	}
	return Query(num-sgt[LSON],RIGHT);//不在就往右子樹找
}

對於一些很大數可能需要先離散化.

動態開點

動態開點的作用&原理

動態開點是線段樹中的一個基礎知識,在線段樹合並,分裂,主席樹等地方都是必須用到的.
在一些用到權值線段樹的題目中數據如果數據大於1e6基本就會跑不過了,但是,有了動態開點,可以節省很多用不到的點.

在這里插入圖片描述

如圖,在這樣一顆線段樹中,灰色部分的節點時沒有用的如果在權值線段樹中像這樣灰色的節點的數量可能會很多,這樣極大的浪費了空間,所以需要用一種新的方式來存這棵樹,動態開點就是這樣出現的.

在原本的線段樹中節點 \(x\) 的左兒子為 \(x\times2\) 右兒子為 \(x\times2+1\).而現在,不再通過計算的方式獲得子節點編號,需要直接記錄在當前節點上,每次修改時如果需要修改到一個沒有加入的節點時才會將這個節點放入這顆樹中,這樣可以節省很多的空間.

動態開點的代碼實現

大體與普通線段樹類似,但是在帶修改(下傳標記)的部分需要傳入 \(now\) 的地址.

//以下部分需要更改
#define LSON sgt[now].lson
#define RRSON sgt[now].rson
struct SegmentTree
{
	int lson,rson;//記錄當前節點左右子節點
	long long sum;
}

下傳標記

void Down(int &now/*在Down時會修改部分的值,所以需要傳入地址*/,int left,int right,int add)
{
	if(!now)
	{
		now=++cnt;//如果沒有這個點就加入這個點
	}
	sgt[now].sum+=(right-left+1)*add;
	lazy[now]+=add;
}
void PushDown(int now,int left,int right)
{
	if(lazy[now])//如果不加在根本沒有需要下傳的標記時可能會多開很多的點
	{
		Down(LEFT,lazy[now]);
		Down(RIGHT,lazy[now]);
		lazy[now]=0;
	}
}

修改

void Updata(int now_left,int now_right,int add,int &now/*這里也需要傳入一個地址*/,int left=1,int right=n)
{
	if(now_left>right||left>now_right)
	{
		return;
	}
	if(!now)
	{
		now=++cnt;//如果沒有這個點就加入這個點
	}
	if(now_left<=left&&right<=now_right)
	{
		sgt[now].sum+=(right-left+1)*add;
		lazy[now]+=add;
		return;
	}
	PushDown(now,left,right);
	Updata(now,add,LEFT);
	Updata(now,add,RIGHT);
	PushUp(now);
}

線段樹合並

線段樹合並的原理&作用

線段樹合並基於動態開點,所以需要先學習有關動態開點的內容后再看一下這部分內容
在一些題目中需要將兩顆線段樹和在一起,組成一棵更大的線段樹,這時就需要用到線段樹合並.

在這里插入圖片描述

在如圖所示的兩顆線段樹中(線段樹為動態開點,灰色部分為沒有值),如果把這兩顆線段樹合並后為:

在這里插入圖片描述

在有值的位置將兩顆線段樹的值相加,如果有一棵線段樹這一個位置沒有值則為另一棵線段樹的值.

可以將兩顆線段樹和為一棵線段樹,看起來可能沒什么用,可以先看看這道例題,需要查詢第 \(k\) 大值,這就很容易想到在權值線段樹二分了,但是,對於將島嶼之間兩邊的操作時,就需要用到線段樹合並了,對於每個島開一棵權值線段樹,維護每種數出現的次數,在不同的島相連時就將所屬的線段樹合並(至於合並時兩座島嶼所屬線段樹的 \(root\) 可以用並查集輕松維護),這樣就可以輕松切掉這題了.

線段樹合並的代碼實現

合並

以下代碼為將 \(sgt2\) 合並到 \(sgt1\) 上.

void Merge(int &sgt1/*因為sgt1是要修改的,所以需要傳入地址*/,int sgt2,int left=1,int right=n)
{
	if(!sgt1||!sgt2)//如果在兩顆合並的子樹中有一顆為空樹,那么就直接是可能非空(有可能是兩顆空樹)的那棵
	{
		sgt1+=sgt2;
		return;
	}
	if(left==right)//當合並到葉節點時就直接將值合並即可
	{
		sgt[sgt1].sum=sgt[sgt1].sum+sgt[sgt2].sum;
		return;
	}
	//遞歸合並子樹
	Merge(sgt[sgt1].lson,sgt[sgt2].lson,left,MIDDLE);
	Merge(sgt[sgt1].rson,sgt[sgt2].rson,MIDDLE+1,right);
	PushUp(sgt1);//將合並后的線段樹上的值合並
}

線段樹合並的復雜度

可以發現線段樹合並時如果兩顆樹都有的部分需要全部掃一遍,有一棵樹沒有的部分可以直接返回,所以它的復雜度為兩樹在同一位置都有節點的節點的個數,而一顆樹中只有\(n*4\)個節點,所以線段樹合並的時間復雜度為\(O(n)\).

以上內容純屬一個不會線段樹的菜雞扯淡,不要聽這個菜雞瞎bb.

關於線段樹合並的復雜度證明,可以發現在線段樹合並中每一次都會刪除一個節點,所以說最多只會刪掉產生的節點數,所以線段樹合並的復雜度的上限為在其他操作時最多能產生的節點數,所以這個復雜度是均攤的,而並非是嚴格的 \(n\log_2n\),需要按其他會產生新節點的操作計算復雜度.

線段樹空間回收

空間回收的作用&原理

可以發現在線段樹合並中會浪費掉很多的空間(每次合並節點時就會有一個節點被浪費掉),在比賽中每一點的空間都是極為珍貴,於是就出現了線段樹的空間回收.

可以將一些不再會有用的點先存放起來,在未來需要開出一個新的節點時可以先在這些被"扔掉"的點中取,在線段樹合並(可能有一些其他操作)時可以節省大量的空間.

空間回收的代碼實現

刪除節點

void Delete(int &now/*傳的是地址*/)
{
	sgt[now].lson=sgt[now].rson=0;
	sgt[now]./*這個節點中的內容都要刪除*/;
	rubbish[++tot]=now;//可以將這個rubbish數組理解為一個棧,tot為棧頂
    now=0;//順便刪掉這個位置
}

新建節點

int New()
{
	if(tot)//如果"垃圾桶"內有元素,那么就從"垃圾桶"里拿
    {
        return rubbish[tot--];
    }
	return ++cnt;//否則就拿一個新節點
}

空間回收只能用在一些特殊的線段樹中(主要是線段樹合並).

線段樹分裂

線段樹分裂的原理

找了很久也沒有什么特別好的題和博客,於是自己yy出了一道簡單模板題.

在這里插入圖片描述

對於圖中這樣的一棵線段樹,如果需要分裂出其中橙色的部分,就需要新建幾個祖先節點(綠色部分),需要把原來的線段樹
中與這顆子樹有關的邊斷開(紅色線段斷開的邊).

線段樹分裂的代碼實現

分裂

void Split(int &sgt1,int &sgt2,int now_left,int now_right,int left=1,int right=n)
//在sgt1這棵權值線段樹中把left~right的部分分裂到sgt2中
{
	if(right<now_left||now_right<left)//不在需要分裂的范圍內
    {
        return;
    }
	if(!sgt1)//如果在sgt1不存在這顆子樹那么自然就沒有了
    {
        return;
    }
	if(now_left<=left&&right<=now_right)//如果在范圍內就直接斷開sgt1中與父親的邊,並且連到sgt2的下面
	{
		sgt2=sgt1;//直接連到sgt2中
		sgt1=0;//把這條邊斷開
		return;
	}
	if(!sgt2)
    {
        sgt2=New();//如果不被修改范圍的區間包含的部分需要新建一個節點(綠色部分)
    }
	//繼續向下分裂
	Split(sgt[sgt1].lson,sgt[sgt2].lson,now,left,MIDDLE);
	Split(sgt[sgt1].rson,sgt[sgt2].rson,now,MIDDLE+1,right);
	PushUp(sgt1);//最后需要更新維護的信息
	PushUp(sgt2);
}

線段樹分裂的時間復雜度

這個東西和線段樹的區間修改極其相似,所以可以用同樣的方法來證明復雜度,可以發現這個東西是 \(\mathcal{O}(n\log_2n)\) 的,且最多會產生 \(n\log_2n\) 個新節點.

標記永久化

標記永久化的作用&原理

普通的線段樹需要 PushUp()PushDown(),標記永久化就可以不需要這兩個東西了.

標記永久化就是對於每次修改就在最上層的節點上打上標記,在查詢時就只需要一路向下查詢將標記加上就可以得到答案了.

標記永久化的代碼實現

節點

struct SegmentTree
{
	long long sum;
	long long tag;//需要加上一個標記用的量,但是不用懶標記
}sgt[maxn*4];

下傳標記

它 SPFA 了

修改

void Updata(int now_left,int now_right,int add,int now=1,int left=1,int right=n)
{
	if(now_right<left||right<now_left)
    {
        return;
    }
	sgt[now].sum+=1ll*(min(now_right,right)-max(now_left,left)+1)*add;
	//修改的區間對於當前區間的貢獻
	if(now_left<=left&&right<=now_right)
	{
		sgt[now].tag+=add;//被完全包含了就直接修改tag
		return;
	}
	Updata(now,add,LEFT);
	Updata(now,add,RIGHT);
	//PushUp也SPFA了
}

查詢

long long Query(int now_left,int now_right,int now=1,int left=1,int right=n)
{
	if(now_right<left||right<now_left)
    {
        return 0;
    }
	if(now_left<=left&&right<=now_right)
    {
        return sgt[now].sum;//包含就直接返回線段樹上的值
    }
	return 1ll*(min(now_right,right)-max(now_left,left)+1)*sgt[now].tag//當前節點的tag對於這次查詢的貢獻
	+Query(now,LEFT)+Query(now,RIGHT);//繼續查詢左右區間內的值
}

不是所有的修改都可以標記永久化,而且線段樹可持久化線段樹中用懶標記的復雜度也是正確的(知識需要更大的空間),所以這個東西更加沒啥用了.

二維線段樹

二維線段樹的作用&原理

二維線段樹有兩種寫法,四分樹樹套樹,根據dalao的說法,四分樹單詞修改的復雜度是會被成 \(\mathcal{O}(n)\) 的,所以以下內容主要講樹套樹的寫法.

顧名思義,二維線段樹是用來維護二維平面上的東西,這里的樹套樹是在線段樹的每個節點再開一棵線段樹,這樣每次修改和查詢的時間復雜度就可以做到 \(\mathcal{O}(n\log^2_2n)\).

二維線段樹的代碼實現

拿出一道模板題.

大致題意就是查詢矩陣最大值+矩陣覆蓋,對於線段樹其實沒什么變化,就是要用上標記永久化,據說二維線段樹不支持打標記(也可能是我太菜了).

內層的線段樹

struct SegmentSegmentTreeX//用兩個結構體表示外傳的線段樹和內層的線段樹,這樣方便一點
{
	int sgt[maxn<<2],tag[maxn<<2]/*用於記錄標記*/;
	void Updata(int now_left,int now_right,int cover,int now=1,int left=1,int right=M)//這里就是最普通的線段樹了
	{
		if(now_right<left||right<now_left)return;
		sgt[now]=max(sgt[now],cover);//修改當前子樹
		if(now_left<=left&&right<=now_right)
		{
			tag[now]=max(tag[now],cover);///修改標記
			return;
		}
		Updata(now,cover,LEFT);
		Updata(now,cover,RIGHT);
	}
	int Query(int now_left,int now_right,int now=1,int left=1,int right=M)//查詢,實在沒什么好說的
	{
		if(now_right<left||right<now_left)return -InF;
		if(now_left<=left&&right<=now_right)
		return max(sgt[now],tag[now]);
		return max(tag[now],//要和標記取max
			   max(Query(now,LEFT),Query(now,RIGHT)));
	}
};

外層的線段樹

struct SegmentSegmentTreeY//外層線段樹
{
	SegmentSegmentTreeX sgt[maxn<<2],tag[maxn<<2];//這里的每個點和標記也是一顆線段樹
	void Updata(int nowlx,int nowly,int now_left,int now_right,int cover,int now=1,int left=1,int right=n)//其余基本相同,就是在原先的標記修改和子樹修改需要改成修改內層的線段樹
	{
		if(now_right<left||right<now_left)return;
		sgt[now].Updata(nowlx,nowly,cover);
		if(now_left<=left&&right<=now_right)
		{
			tag[now].Updata(nowlx,nowly,cover);
			return;
		}
		Updata(nowX,cover,LEFT);
		Updata(nowX,cover,RIGHT);
	}
	int Query(int nowlx,int nowly,int now_left,int now_right,int now=1,int left=1,int right=n)//查詢也差不多
	{
		if(now_right<left||right<now_left)return -InF;
		if(now_left<=left&&right<=now_right)
		return sgt[now].Query(nowlx,nowly);
		return max(tag[now].Query(nowlx,nowly),
			   max(Query(nowX,LEFT),Query(nowX,RIGHT)));
	}
}SegmentSegmentTree;

掃描線

掃描線的作用&原理

放一道模板題.

給出n個矩形,求最終所覆蓋的面積(矩陣面積並),看起來很像一道二維線段樹,但是數據范圍很大,是開不了的,所以,掃描線就誕生了.

在這里插入圖片描述

圖中的藍色線段為掃面線.

先掃到最左邊的矩形的左邊的那條邊:

在這里插入圖片描述

可以發現在到下一條平行於掃描線的邊之間是一個長方形,可以直接計算出來.

在這里插入圖片描述

同樣的方法可以將這個圖形分成若干不相交的長方形.

在這里插入圖片描述

每一塊長方形的寬就是相鄰的兩條平行與掃面線的邊之間的距離,長就是掃描線的長了,所以,問題就變成了如何計算掃面線的長,一個長方形有兩條與掃描線平行的邊,遇到左邊的一條時就加上1,遇到右邊時就減去1,這樣掃面線的長度就是不為0的位置的個數,區間加減1很容易就想到線段樹了.

掃描線的實現

#include<bits/stdc++.h>
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int maxn=2e5+7;
int n,M;
map<long long,long long>Hash;
long long place[maxn*2];
long long sor[maxn*2];
struct Line
{
	int val;
	long long x,fy,ly;
	void into(int X,int F,int L,int V)
	{
		x=X;
		fy=F;
		ly=L;
		val=V;
	}
}line[maxn*2];
bool cmp(Line a,Line b)
{
	return a.x<b.x;
}
struct SegmentTree
{
	int cover;
	int len;
}sgt[maxn*4];
#define LSON (now<<1)
#define RRSON (now<<1|1)
#define MIDDLE ((left+right)>>1)
#define LEFT LSON,left,MIDDLE
#define RIGHT RRSON,MIDDLE+1,right
#define NOW now_left,now_right
void PushUp(int now,int left,int right)
{
	if(sgt[now].cover)//如果被完全覆蓋,那么長度就是可以直接計算
	{
		sgt[now].len=place[right+1]-place[left];
	}
	else//沒有被完全覆蓋就合並兒子節點的信息
	{
		sgt[now].len=sgt[LSON].len+sgt[RRSON].len;
	}
}
void Build(int now=1,int left=1,int right=n)//建樹,沒什么用
{
	if(left==right)
	{
		sgt[now].cover=sgt[now].len=0;
		return;
	}
	Build(LEFT);
	Build(RIGHT);
	PushUp(now,left,right);
}
void Updata(int now_left,int now_right,int add,int now=1,int left=1,int right=n)
{
	if(now_right<=left||right+1/*注意需要加一,且用小於等於*/<=now_left)
	{
		return;
	}
	if(now_left<=left&&right+1/*注意加一*/<=now_right)
	{
		sgt[now].cover+=add;
		PushUp(now,left,right);
		return;
	}
	Updata(NOW,add,LEFT);
	Updata(NOW,add,RIGHT);
	PushUp(now,left,right);
}
int main()
{
	scanf("%d",&M);
	long long x1,y1,x2,y2;
	REP(i,1,M)
	{
		scanf("%lld%lld%lld%lld",&x1,&y1,&x2,&y2);
		sor[i*2-1]=y1;//數據太大,需要對於縱坐標離散
		sor[i*2]=y2;
		line[i*2-1].into(x1,y1,y2,1);
		line[i*2].into(x2,y1,y2,-1);
	}
	sort(sor+1,sor+1+M*2);
	sort(line+1,line+1+M*2,cmp);
	sor[0]=114514233;
	REP(i,1,M*2)
	{
		if(sor[i]!=sor[i-1])
		{
			Hash[sor[i]]=++n;
			place[n]=sor[i];
		}
	}
	Build();
	long long answer=0;
	REP(i,1,M*2-1)
	{
		Updata(Hash[line[i].fy],Hash[line[i].ly],line[i].val);//修改操作
		answer+=sgt[1].len/*掃描線長*/*(line[i+1].x-line[i].x)/*兩邊之間的距離*/;
	}
	printf("%lld",answer);
}

主席樹(可持久化權值線段樹)

為什么叫主席樹

主席樹由一位名叫黃嘉泰的神仙最先用在靜態區間 \(k\) 大中,然后可以驚奇的發現黃嘉泰的拼音首字母是hjt,然后...所以就叫主席樹了.

主席樹的作用&原理

在了解主席樹之前先了解一下什么是持久化,類似有關訪問歷史版本的題目往往和持久化有關,如果需要一個可持久化的線段樹需要怎么辦呢,一個非常暴力的方法就是將所有歷史版本的線段樹都保存下來,但這樣想必會MLE,且每次復制一個版本也需要 \(\mathcal{O}(n)\) 的復雜度並不可以.於是就出現了主席樹這樣一個神奇的數據結構.

在這里插入圖片描述

如這樣的一顆權值線段樹,需要加如一個 \(3\).

在這里插入圖片描述

可以發現只有紅色部分的點的值改變了, 於是就發現每次的修改后有大量的點與之前相同,直接復制就浪費了大量空間,於是可以將新的產生的節點與沒有改變的節點相連,這樣就可以重復利用這些在歷史版本中沒有改變的值,節省大量空間,如這樣的一個單點修改只會對一條鏈上的值產生影響,每一顆樹就只要產生 \(\log_2n\) 個節點.

在這里插入圖片描述

最終大概就是這個樣子(完全不像樹).

主席樹的代碼實現

單點修改

宏定義

//碼風略微改變
#define LSON sgt[now].lson
#define RRSON sgt[now].rson
#define MIDDLE ((left+right)>>1)
#define LEFT LSON,left,MIDDLE
#define RIGHT RRSON,MIDDLE+1,right
#define NOW now_left,now_right
//新增加以下兩句
#define NEW_LSON sgt[new_sgt].lson
#define NEW_RSON sgt[new_sgt].rson

修改

void Updata(int &new_sgt/*產生的一個新版本*/,int num/*num這個數在線段樹中+1*/,int now,int left=1,int right=n)
{
	if(num<left||right<num)//不會被修改到就直接連會原來的版本
	{
		new_sgt=now;
		return;
	}
	new_sgt=++cnt;//一個新的節點
	if(left==right)//葉節點就單點修改
	{
		sgt[new_sgt].sum=sgt[now].sum+1;//在原版本中+1得到新版本
		return;
	}
	Updata(NEW_LSON,num,LEFT);//對於左右子樹繼續修改
	Updata(NEW_RSON,num,RIGHT);
	PushUp(new_sgt);//合並信息
}

對於主席樹僅僅是針對可持久化權值線段樹,所以主席樹 \(\in\) 可持久化線段樹.

區間修改可持久化線段樹

區間修改主席樹的作用&原理

例如在線段樹1加上一個可持久化.可以發現如果像原來的可持久化線段樹一樣的方法去寫每次修改最大就會產生一顆完整的線段樹,肯定是會 MLE 的,所以就要拿出之前說過的標記永久化(其實不用標記永久化,直接打懶標記也是可以的).

在這里插入圖片描述

對於一個修改的位置本來它的子孫都應該是被修改了,但是,加上了標記永久化以后就只需要修改自己,不用修改子孫了,所以對於完全被覆蓋的點的左右兒子都還是原來的版本的值,注意在產生新版本的時候需要將原來的標記也賦值到新版本中.

區間修改主席樹的實現

修改

void Updata(int now_left,int now_right,int add,int &new_sgt,int now,int left=1,int right=n)
{
	if(now_left>right||left>now_right)
	{
		new_sgt=now;
		return;
	}
	new_sgt=++cnt_point;
	sgt[new_sgt].sum=sgt[now].sum+
	1ll*add*(min(now_right,right)-max(now_left,left)+1);//當前修改對於當前區間的貢獻
	sgt[new_sgt].tag=sgt[now].tag;//賦值原來的標記
	if(now_left<=left&&right<=now_right)//被完全覆蓋
	{
		sgt[new_sgt].tag+=add;
		NEW_LSON=LSON;//左右兒子不變
		NEW_RSON=RRSON;
		return;
	}
	Updata(NOW,add,NEW_LSON,LEFT);
	Updata(NOW,add,NEW_RSON,RIGHT);
}

查詢

long long Query(int now_left,int now_right,int now,int left=1,int right=n)
{
	//與普通標記永久化線段樹差不多
	if(!now)
	{
		return 0;
	}
	if(now_left>right||left>now_right)
	{
		return 0;
	}
	if(now_left<=left&&right<=now_right)
	{
		return sgt[now].sum;
	}
	return Query(NOW,LEFT)+Query(NOW,RIGHT)+
	1ll*sgt[now].tag*(min(now_right,right)-max(now_left,left)+1);//當前范圍中的標記對於需要查詢范圍的貢獻
}

帶修主席樹(其實就是樹套樹)

帶修主席樹的原理

先扔一道例題.

靜態區間 \(k\) 大可以維護一個前綴每個數出現的次數,這樣就可以用通過類似前綴和求區間和將這一個區間中每個數出現的次數計算出來,對於每個點開一顆線段樹,因為有大量的相同點,所以可以用可持久化線段樹.

再來看這道題,加上了一個單點修改的操作.可以再仔細想想,前綴和會想到什么呢....那就是樹狀數組啦,所以只要在樹狀數組上套一個線段樹就好了.

代碼實現

先建一下這個森林(一堆樹)

修改

void Updata(int num,int val,int &new_sgt,int now,int left=1,int right=len)
//這里寫主席樹並沒有什么用
{
	if(num<left||right<num)
	{
		new_sgt=now;
		return;
	}
	if(!new_sgt)
	{
		new_sgt=++point_cnt;//一個新節點
	}
	if(left==right)
	{
		sgt[new_sgt].sum=sgt[now].sum+val;
		return;
	}
	Updata(num,val,nEW_LEFT,LEFT);
	Updata(num,val,nEW_RIGHT,RIGHT);
	PushUp(new_sgt);
}
void UpdataAdd(int top,int num,int val/*需要將原來的數減去,加上新的一個數*/)
{
	for(int now=top;now<=n;now+=lowbit(now))//在樹狀數組上修改
	{
		Updata(num,val,root[now],root[now]);//在當前的樹上修改
	}
}

Build

void Build()
{
	REP(i,1,n)
	{
		for(int now=i;now<=n;now+=lowbit(now))//一個點一個點地放入樹狀數組,記錄前綴每個數數顯次數
		{
			Updata(Hash[arr[i]],1,root[now],root[now]);
		}
	}
}

Query

int add_sgt[maxn];//記錄在樹狀數組中需要加上的位置的樹的當前根節點
int cut_sgt[maxn];//記錄在樹狀數組中需要減去的位置的樹的當前根節點
int Query(int k,int num_add,int num_cut,int left=1,int right=len)
{
	if(left==right)return left;
	int sum=0;
	REP(i,1,num_add){sum+=sgt[sgt[add_sgt[i]].lson].sum;}//計算出所有左子樹中的樹的個數
	REP(i,1,num_cut){sum-=sgt[sgt[cut_sgt[i]].lson].sum;}
	if(sum>=k)
	{
		REP(i,1,num_add){add_sgt[i]=sgt[add_sgt[i]].lson;}//將根節點賦值為左兒子
		REP(i,1,num_cut){cut_sgt[i]=sgt[cut_sgt[i]].lson;}
		return Query(k,num_add,num_cut,left,MIDDLE);
	}
	REP(i,1,num_add){add_sgt[i]=sgt[add_sgt[i]].rson;}//同理賦值為右兒子
	REP(i,1,num_cut){cut_sgt[i]=sgt[cut_sgt[i]].rson;}
	return Query(k-sum,num_add,num_cut,MIDDLE+1,right);
}
int QueryKth(int left,int right,int k)
{
	int num_add=0,num_cut=0;
	for(int now=right;now;now-=lowbit(now))//將樹狀數組中需要加上的部分放入一個數組
	{
		add_sgt[++num_add]=root[now];
	}
	for(int now=left-1;now;now-=lowbit(now))//同理,將需要減去的部分放入一個數組
	{
		cut_sgt[++num_cut]=root[now];
	}
	return Query(k,num_add,num_cut);//查詢kth
}

后記

文章中還有若干寫的不清楚的地方,也有一些懶得寫的地方,以后有空時可能會把鍋補上.(發現問題可以私信我)

有關平衡樹,多項式等內容,在若干年內也有可能會出類似學習筆記.

to be continued


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM