C++演算法 線段樹

lichangjian發表於2020-08-28

線段樹這個演算法,看起來非常高階,而且很有用處,所以還是講一下下吧。

溫馨提示:寫線段樹前請做好寫碼5分鐘,除錯一輩子的準備^-^

啊直接步入正題……

首先我們考慮一個題目:有一個序列,要做到單點修改單點查詢,該怎麼做呢?

同學們先不要著急關掉……我們細細分析,像這種題,明顯大家都知道……直接暴力就過了嘛……,所以不做分析……

然後我們考慮第二個題目:有一個序列,要做到單點修改區間求和,該怎麼做呢?

像傳統的for(int i=1;i<=n;i++)ans+=a[i]當然是很香的,但如果遇到一些非常奇怪的題目(數 據 大 到 你 自 閉),這個就沒什麼用了……

 

啊這,簡單的暴力不能用,就要用複雜的暴力——線段樹,當然是簡單億點點的~因為單點修改可以O(1),不存在浪費時間的事情,所以我們用線段樹小小的處理一下就好了。

接下來畫一個圖幫助斯烤:

 

 

在這個圖上呢……一共有8個點,就是底下那些最小的線段,而我們把他合成了許多部分,每個部分的值就是f[wz*2]+f[wz*2+1](f是每個的值,wz是每個的編號)。

然後就可以把他一直向下,一直向下,直到發現這個區間是被我們要求的區間所包含的,然後這個區間內的數就全部加上,就這麼一直向下找找找……,最後就可以算出一個區間的總和。

下面是演示程式碼~

 

void qh(long long wz)//wz是現在所處的小塊編號 
{
	if(tree[wz].l>=a&&tree[wz].r<=b)//a和b是我們要求的區間的左右端點 
	{
		zshu+=tree[wz].shu;//全部被包含,直接加上 
		return;
	} 
	long long mid=(tree[wz].l+tree[wz].r)/2;//不能被包含?分裂一下試試吧
	if(b>mid)//和諧小細節~ 
	{
		qh(wz*2+1);//如果有被包含,就去右邊的小塊看看 
	}
	if(a<=mid)//和諧小細節*2~(至於為啥有和諧小細節,以及為什麼要這樣寫,請同學們自己斯烤(這麼簡單還是可以的8)) 
	{
		qh(wz*2);//如果有被包含,也要去左邊的小塊看看
	}
}

 

嘿嘿簡單吧^-^,啊我忘記講建樹了QAQ。沒事馬上就講。

建樹嘛,直接遞迴就好了,每次把這個區間分成兩份,一直分到l和r一樣(就是說這是一個點的值,要輸入了),然後獲取兩個點的值之後就可以向上遞迴~嗯嗯對,一直遞迴就建好了,相信大家都有這個寫遞迴的能力,但要實在不會就看看下面的程式碼吧(溫馨提示:線段樹用來建樹的陣列要開到節點數的4倍大,至於為什麼我也忘了……):

struct hehe
{
	long long l,r,shu,f;
}tree[400005];//tree結構體~ 
long long mid;
void js(long long ll,long long rr,long long wz)//js,建樹的意思。ll和rr分別是左右邊界,wz就是他的編號 
{
	tree[wz].l=ll;//左邊界是ll 
	tree[wz].r=rr;//右邊界是rr 
	if(ll==rr)//左右邊界一致,這是一個點 
	{
		scanf("%lld",&tree[wz].shu);
		return;
	}
	long long mid=(ll+rr)/2;//如果這不是一個點,肯定能分成兩部分 
	js(ll,mid,wz*2);//左邊 
	js(mid+1,rr,wz*2+1);//右邊 
	tree[wz].shu=tree[wz*2].shu+tree[wz*2+1].shu;//加起來 
}

看,是不是非常簡單,接下來為了學的更深一點點,要把題目加難了(是的還要加難,但我相信你們一定可以學會的)

有一個序列,要做到區間增加區間求和,該怎麼做呢?

啊這,這個是線段樹裡最難的一部分(起碼我覺得最難),直接下放顯然不太現實……,會浪費掉很多不必要的時間。正所謂科技發展在於懶人~,其實我們也可以在不影響結果的情況下偷個懶是吧QWQ。

就像加一樣~我知道這兩個序列的和,我就沒必要去求所有單個序列是多少,加法和這個差不多,我們可以看看有那個序列是全都要加的,然後直接算出它加完之後的值。這樣求這個值的時候肯定是不影響計算的(偷懶成功!)。但這時就會有一些同學吐槽:"啊你這個不嚴謹啊,如果下一次求和是求這個序列的一部分怎麼辦呢?"其實呢,我剛才也寫了是吧……

其實我們也可以在不影響結果的情況下偷個懶是吧

我剛才說的是不影響,但這個操作明顯影響了,在哪裡影響了呢?就是隻加了一個總序列,沒有讓他的一部分加上(偷懶失敗QWQ)。但好像根本沒有必要調整他的一部分啊,因為目前根本用不到,沒說讓做的事情我們還要去做不是浪費時間嗎?但又不能不加,怎麼辦呢?我們可以設定一個懶標記,表示它之前被加了多少,這樣呢,每次要求和的時候只要下放這個懶標記,並且加上該加的數,就能達到不說不做,最大程度優化時間,還保證正確hhhhh(偷懶成功)

至於程式碼的實現也是非常的簡單:

void xf(long long wz)
{
	tree[wz*2].shu+=(tree[wz*2].r-tree[wz*2].l+1)*tree[wz].f;//每個長度單位都加上tree[wz].f,總值就增加了(tree[wz*2].r-tree[wz*2].l+1)*tree[wz].f
	tree[wz*2].f+=tree[wz].f;//之前每一個長度單位要加tree[wz*2].f,現在發現它還要加上tree[wz].f,就把他們兩個加起來就好了嘛,很容易的。 
	tree[wz*2+1].shu+=(tree[wz*2+1].r-tree[wz*2+1].l+1)*tree[wz].f;
	tree[wz*2+1].f+=tree[wz].f;//同理
	tree[wz].f=0;//這裡已經加過了,要清零的,就像轉賬一樣,你給另一個人轉了錢,你錢就沒了。 
}

上方是下放懶標記的程式碼,下方是區間修改程式碼:

void xg(long long wz)
{
	if(tree[wz].l>=a&&tree[wz].r<=b)
	{
		tree[wz].shu+=(tree[wz].r-tree[wz].l+1)*c;//更改值 
		tree[wz].f+=c;//增加懶標記 
		return;
	}//啊後面的都說過,不打了… 
	tree[wz].shu+=(min(tree[wz].r,b)-max(tree[wz].l,a)+1)*c;
	long long mid=(tree[wz].l+tree[wz].r)/2;
	if(b>mid)
	{
		xg(wz*2+1);
	}
	if(a<=mid)
	{
		xg(wz*2);
	}
}

至於加了懶標記以後別的程式碼也是要動的。

比如在原來的求和程式碼裡多了一個xf(wz);

至於加在哪裡同學們自己斯烤吧(我太仁慈了)

啊我再放一道線段樹模板題,大家可以秒掉來吊打我:模板題傳送們

好了我覺得我講完了,如果有什麼不好或漏掉的地方大家可以及時評論,我會更改的。

相關文章