【數據結構】線段樹

這幾天上網課學了不少東西,如今整理一下html

線段樹

有一位老師說線段樹和樹狀數組沒有什麼區別,讓咱們只學一個就行
我仍是都學了
如今來整理一下線段樹數組

進入正題

在我這個線段樹萌新的眼中,它就是用來求前綴和和統計的ㄟ( ▔, ▔ )ㄏmarkdown

爲何叫作線段樹:

大概就是它長得很像樹,而後它也是個樹吧~~
至少它都有滿二叉樹的特色:優化

  • 每一個節點有一個序號 kui

  • 除了葉節點其餘節點都有左孩子和右孩子code

  • k 節點左孩子的序號爲 k * 2htm

  • k 節點右孩子的序號爲 k * 2 + 1blog

但線段樹也有不同凡響的特色:遞歸

  • 存儲的是一個區間的信息(如區間和)get

  • 大部分操做都是用二分的思想

便於記憶,能夠直接把線段樹看做是一個存儲着區間信息的滿二叉樹

一些記憶:

之前沒有學過線段樹到時候,我常常把一個數組分紅兩部分,而後把每一段再分,一直到分紅元素不能再分爲止,我還常常想爲何不能這樣求前綴和,否則太不方便了,結果學了線段樹後發現被打臉了……

或許我早生個幾十年……≖‿≖✧

線段樹就是這樣的:

把一個線性數組平均分紅兩半,而後把每一段繼續分紅兩段,一直到只剩一個元素不能再分爲止

畫個圖:

線段樹

而後把它們當作樹給編個號:
線段樹1

簡化一下:
線段樹2
如今應該就能看出來了:線段樹本質實際上是一個滿二叉樹
除了葉節點,每一個節點存儲着一個區間的信息
而葉節點存儲單個元素的信息

程序實現:

思路:

  1. 用遞歸分別存儲一個節點的左孩子和右孩子

  2. 在存儲的同時求和

建樹代碼:(我是從一位dalao的博客裏學的)

void build_tree(int l, int r, int k) {  // l 存儲這個區間的左端點,r 存儲這個區間的右端點,k 存儲這個區間的序號
	e[k].l = l;
	e[k].r = r;                     // 用結構體存儲一個節點的信息
	if (l == r) {                   // 若是遞歸到一個葉節點
	      e[k].sum = cun[l];        // 存下這個節點的和
		return;                 // dalao說過這句別忘了加,否則會死循環
	}
	int mid = (l + r) / 2;          // 二分思想
	build_tree(l, mid, k*2);        // 遞歸左孩子
	build_tree(mid + 1, r, k*2 + 1);// 遞歸右孩子
	e[k].sum = e[k*2].sum + e[k*2 + 1].sum; // 求和
}

一些線段樹的基本操做:

首先,先引入一個頗有用的東西:延遲標記(也叫懶標記)

先舉個栗子 (。・ω・)ノ゙:

有一組數:存爲 a 數組,如今我要把 a[3]a[7] 中間的全部數都加上 4(包括 a[3]a[7]),最後輸出 a[3]a[7] 的區間和。

用線段樹的作法怎麼作?(光想思路,不用代碼實現)

暴力作法:

用線段樹將 a[3]a[7] 的每個點都加上 4,而後再從新求有 a[3]a[7] 的全部區間的和。

有這種思路是正常的,特別是我這種蒟蒻,但這種方法實在是太暴力了,若是作題容易 TLE,由於數據一大這種方法就跟用線段樹枚舉沒有什麼區別,甚至可能比枚舉的耗時還要長……

這個時候就是 超級飛俠 延遲標記發揮做用的時候了

延遲標記優化作法:

在存儲樹節點的結構體裏再開一個變量 book
而後遞歸找到(下面會說作法) a[3]a[7] 的區間並直接加上 4,並將 4 存儲到那個區間的 book 裏並把這個區間的 book 清零,下一步就是:

無論了

沒錯,就是無論了,反正我要求輸出的是 a[3]a[7] 的區間和,又沒要求輸出 a[3] 的值或者什麼的。

那可能會有人想:
若是又要求輸出 a[3] 之類的東西呢?

那這個時候就是延遲標記名字中「延遲」的由來:

若是又要求輸出 a[3],那麼能夠再一次遞歸找 a[3],而後等找到 a[3]a[7] 的左孩子或者右孩子的時候,因爲 book 不爲零,那麼將這段區間的 sum 加上 book 中的值並把 book 繼續下傳而後清零。

畫個流程:

最後到達目標區間後記得 return 結束遞歸哦~~

流程大概就是這樣,如今來解釋一下:

能夠這樣理解:假如你的假期做業還沒作(好比我),其中有要求交的,也有沒讓交的(老師不檢查),那你會選擇都作完仍是隻作要求教的?

反正我選二 (☆゚∀゚)

可是你如今不知道哪些做業要交,哪些做業不用交,只有課表明開始收做業的時候你才知道,那你是否是會趕忙補那個須要交的做業(爭分奪秒)?

反正我會這樣 (☆゚∀゚)

延遲標記就能夠這樣理解:若是須要這個節點的信息,那麼延遲標記就趕忙把這個節點的信息給更新了,而後 光(開)榮(心)退(離)休(場)~~

就像瘋狂補完該交的做業並交上去而後鬆了一口氣的我同樣(。・`ω´・)

代碼實現:

void sign(int k){
	e[k*2].book += e[k].book;           // 將父節點的延遲標記累加到左孩子中
	e[k*2 + 1].book += e[k].book;       // 將父節點的延遲標記累加到右孩子中
	e[k*2].sum += e[k].book * (e[k*2].r - e[k*2].l + 1); // 更新左孩子總和
	e[k*2 + 1].sum += e[k].book * (e[k*2 + 1].r - e[k*2 + 1].l + 1); // 更新右孩子總和
	e[k].book = 0;                      // 延遲標記清零
}

大約就是這個亞子

而後各操做代碼

  • 單點查詢:
void ask_point(int k) {
    if (e[k].l == e[k].r) { // 若是查詢到目標節點
        ans = e[k].sum;     // 記錄下該節點的值
        return ;
    }
    if (e[k].book) sign(k); // 若是這個節點的 book 值不爲零,動用延遲標記
    int mid = (e[k].l + e[k].r) / 2; // 二分思想
    if (x <= mid) ask_point(k*2); // 若是目標節點在左區間,則遞歸左孩子
    else ask_point(k*2 + 1);// 不然遞歸右孩子
}
  • 單點修改:
void change_point(int k) {
    if (e[k].l == e[k].r) {  // 若是遞歸到目標節點
        e[k].sum += y;      // 改變該節點的值
        return;
    }
    if (e[k].book) sign(k); // 若是這個節點的 book 值不爲零,動用延遲標記
    int mid = (e[k].l + e[k].r) / 2; // 二分思想
    if (x <= mid) change_point(k*2); // 若是目標節點在左區間,則遞歸左孩子
    else change_point(k*2 + 1); // 不然遞歸右孩子
    e[k].sum = e[k*2].sum + e[k*2 + 1].sum; // 從新求和
}
  • 區間查詢:
void ask_line(int k) {
    if (e[k].l >= a && e[k].r <= b) {   // 若是遞歸的該區間爲目標區間的一部分
        ans += e[k].sum;                // 累加區間和
        return;
    }
    if (e[k].book) sign(k);
    int mid = (e[k].l + e[k].r) / 2;
    if (a <= mid) ask_line(k*2);
    if (b > mid) ask_line(k*2 + 1);
}
  • 區間修改:
void change_line(int k) {
    if (e[k].l >= a && e[k].r <= b) {
        e[k].sum += y * (e[k].r - e[k].l + 1);
        e[k].book += y;
        return;
    }
    if (e[k].book) sign(k);
    int mid = (e[k].l + e[k].r) / 2;
    if (a <= mid) change_line(k*2);
    if (b > mid) change_line(k*2 + 1);
    e[k].sum = e[k*2].sum + e[k*2 + 1].sum;
}

最後再次膜拜dalao

markdown真好玩
我去我居然寫了一天

相關文章
相關標籤/搜索