資料結構 線段樹詳解

2022-05-02 22:54:11 字數 4207 閱讀 1493

線段樹是一種高效的資料結構,可以在\(o(nlog_n)\)的時間內查詢區間最值或區間和,解決動態的rmq問題,並且可以為一些演算法進行優化,如dijkstra最短路、掃瞄線等。

線段樹是一種基於分治演算法的二叉樹。每個結點維護乙個區間,以及在該區間內的資料資訊。在當前結點$x$,它的區間是$[left,right]$,則它的兩個子結點的區間分別為$[left,mid],[mid+1,right]$,$mid=\frac$。由於採用了分治的思想,在進行操作時每一層至多訪問兩個結點,極大優化了效率。根據定義,我們可以得出線段樹結點的結構:

template class segmenttree 

tree[lim];

int size;

// ...

};

這裡使用了類體和模板,可以適用於更多型別。

node結構體就是線段樹的結點,其中的sum是該結點維護的區間和。

線段樹的基本操作就是單點修改。根據分治思想,我們訪問結點時,如果已經到了要修改的結點(區間長度為1且已經到達目標位置),就將其值修改,否則,向它的子結點遞迴,在回溯時隨便更新自己結點的值。

首先看乙個pushup函式,用於更新當前結點的值。

void pushup(int root)
這個函式把子結點的值累加到當前結點上。由於使用了陣列,子結點可以不用進行特判是否存在,直接累加即可。

遞迴過程中,區間會不斷縮小,最後長度會變為\(1\),即為乙個點,如果這個點和要修改的點重合,就可以修改了。時間複雜度為\(o(log_n)\)。

void modify(int root, int left, int right, int pos, t key)

int mid = left + right >> 1;

modify(root << 1, left, mid, pos, key);

modify(root << 1 | 1, mid + 1, right, pos, key);

pushup(root); // 更新區間和

}

現在有了單點修改,但對於乙個初始好的序列,單點修改顯然不太方便,並且時間複雜度會過大(然而有時還是可以過)。所以我們也有一種方法快速構建線段樹,並用乙個序列初始化。

void build(int root, int left, int right, t arr) 

int mid = left + right >> 1;

build(root << 1, left, mid, arr);

build(root << 1 | 1, mid + 1, right, arr);

pushup(root); // 更新區間和

}

如果區間長度為$1$,則直接初始化,否則進行遞迴,回溯時`pushup`。時間複雜度為$o(n)$。只要在遞迴是進行區間的判斷,如果查詢的位置\(pos \leq mid\),則向左子樹遞迴,否則向右子樹遞迴。

t query(int root, int left, int right, int pos) 

int mid = left + right >> 1;

if(pos <= mid) return query(root << 1, left, mid, pos); // 在左邊

else return query(root << 1 | 1, mid + 1, right, pos); // 在右邊

}

如果使用單點修改來實現區間修改,時間複雜度就會達到\(o((right-left+1)nlog_n)\)。所以,我們需要引入懶標記。在結點上加上懶標記就可以快速修改區間。

舉個例子:

我們有乙個維護了區間為$[1,8]$的線段樹,我們要給這個區間加上數$v$,有兩種方法:

很顯然,第二種效率更高。這就是懶標記

懶標記可以是加的標記,也可以是乘的標記,只要滿足分配律,就可以使用懶標記。但是如果是開根號等等的操作就不可以,因為\(\sqrt \neq \sqrt + \sqrt\)。

對於目前的線段樹,我們需要修改結點的結構體:

template class segmenttree 

tree[lim];

int size;

// ...

};

修改可以分為這幾個情況:

然後更新當前結點。

但這裡有乙個問題,如果我們修改時經過乙個已經打過懶標記的結點,就無法正確更新區間和。這時候就需要標記下傳了。在修改區間之前,首先標記下傳,使子結點的區間和也更新,這樣就能得到正確答案了。

void update(int root, int left, int right, t key) 

void pushdown(int root, int left, int right)

再根據上面講解,我們可以得出以下區間修改**。

void modify(int root, int left, int right, int mleft, int mright, t key) 

int mid = left + right >> 1;

pushdown(root, left, right);

if (mleft <= mid) // 如果修改區間被左邊包含,則往左邊區間遞迴

modify(root << 1, left, mid, mleft, mright, key);

if (mid < mright) // 如果修改區間被右邊包含,則往右邊區間遞迴

modify(root << 1 | 1, mid + 1, right, mleft, mright, key);

pushup(root); // 更新

}

與區間查詢類似,也需要進行標記下傳。

查詢可以也可以分為這三個情況:

最後累加左右子樹區間和。

t query(int root, int left, int right, int qleft, int qright)

```cpp

template class segmenttree

void build(t arr) 

void modify(int left, int right, t key)

t query(int left, int right)

private:

struct node

tree[lim];

int size;

void pushup(int root) 

void update(int root, int left, int right, t key)

void pushdown(int root, int left, int right)

void build(int root, int left, int right, t arr)

int mid = left + right >> 1;

build(root << 1, left, mid, arr);

build(root << 1 | 1, mid + 1, right, arr);

pushup(root);

}void modify(int root, int left, int right, int mleft, int mright, t key)

int mid = left + right >> 1;

pushdown(root, left, right);

if (mleft <= mid)

modify(root << 1, left, mid, mleft, mright, key);

if (mid > mright)

modify(root << 1 | 1, mid + 1, right, mleft, mright, key);

pushup(root);

}t query(int root, int left, int right, int qleft, int qright)

資料結構 線段樹

啦啦啦啦啦啦線段樹是個好東西 好吧並沒有什麼好的 但貌似還是很好啊 線段樹就是一棵樹!顧名思義 又是這個詞 就是求關於一段的某些什麼什麼東西。比如區間最大值啊什麼的。引用百科知識 線段樹是一種二叉搜尋樹,與區間樹相似,它將乙個區間劃分成一些單元區間,每個單元區間對應線段樹中的乙個葉結點。對於線段樹中...

資料結構 線段樹

一 目標 1.如何快速的查詢出下列陣列arr 2,5 的和 2。以及更新arr 4 為6。用普通的方法查詢的複雜度為o n 更新的複雜度為o 1 這時候我們可以用線段樹來快速完成這些操作,複雜度為logn。二 內容 如何建立,查詢,更新線段樹。public class qurqpd int tree...

資料結構 線段樹

線段樹是一顆平衡的二叉搜尋樹,他以空間換區時間,讓線性查詢加速log級別的查詢,用到的演算法主要是二分搜尋和遞迴。例如 有陣列data 我有乙個需求,我需要頻繁的查詢區間i j的sum和。這裡先給出兩個解決方案 如果使用最普通的演算法遍歷,那麼查詢和更新的複雜度為o n 當然你還可以使用動態規劃,定...