線段樹 - 多組圖帶你從頭至尾完全理解線段樹

線段樹是算法競賽中經常使用的用來維護 區間信息 的數據結構。php

相關閱讀:樹狀數組html

線段樹能夠在 \(O(\log N)\) 的時間複雜度內實現單點修改、區間修改、區間查詢(區間求和,求區間最大值,求區間最小值)等操做。ios

線段樹維護的信息,須要知足可加性,即能以能夠接受的速度合併信息和修改信息,包括在使用懶惰標記時,標記也要知足可加性(例如取模就不知足可加性,對 \(4\) 取模而後對 \(3\) 取模,兩個操做就不能合併在一塊兒作)。c++

線段樹

線段樹的基本結構與建樹

線段樹將每一個長度不爲 \(1\) 的區間劃分紅左右兩個區間遞歸求解,把整個線段劃分爲一個樹形結構,經過合併左右兩區間信息來求得該區間的信息。這種數據結構能夠方便的進行大部分的區間操做。git

有個大小爲 \(5\) 的數組 \(a=\{10,11,12,13,14\}\) ,要將其轉化爲線段樹,有如下作法:設線段樹的根節點編號爲 \(1\) ,用數組 \(d\) 來保存咱們的線段樹, \(d_i\) 用來保存線段樹上編號爲 \(i\) 的節點的值(這裏每一個節點所維護的值就是這個節點所表示的區間總和),如圖所示:github

圖中 \(d_1\) 表示根節點,紫色方框是數組 \(a\) ,紅色方框是數組 \(d\) ,紅色方框中的括號中的黃色數字表示它所在的那個紅色方框表示的線段樹節點所表示的區間,如 \(d_1\) 所表示的區間就是 \([1,5]\)\(a_1,a_2, \cdots ,a_5\) ),即 \(d_1\) 所保存的值是 \(a_1+a_2+ \cdots +a_5\)\(d_1=60\) 表示的是 \(a_1+a_2+ \cdots +a_5=60\)算法

經過觀察不難發現, \(d_i\) 的左兒子節點就是 \(d_{2\times i}\)\(d_i\) 的右兒子節點就是 \(d_{2\times i+1}\) 。若是 \(d_i\) 表示的是區間 \([s,t]\) (即 \(d_i=a_s+a_{s+1}+ \cdots +a_t\) ) 的話,那麼 \(d_i\) 的左兒子節點表示的是區間 \([ s, \frac{s+t}{2} ]\)\(d_i\) 的右兒子表示的是區間 \([ \frac{s+t}{2} +1,t ]\)數組

具體要怎麼用代碼實現呢?數據結構

咱們繼續觀察,有沒有發現若是 \(d_i\) 表示的區間大小等於 \(1\) 的話(區間大小指的是區間包含的元素的個數,即 \(a\) 的個數。設 \(d_j\) 表示區間 \([s,t]\) ,它的區間大小就是 \(t-s+1\) ),那麼 \(d_i\) 所表示的區間 \([s,t]\) 中確定有 \(s=t\) ,且 \(d_i=a_s=a_t\) 。這就是線段樹的遞歸邊界。函數

思路以下:

此處給出 C++ 的代碼實現,可參考註釋理解:

void build(int s, int t, int p) {
  // 對 [s,t] 區間創建線段樹,當前根的編號爲 p
  if (s == t) {
    d[p] = a[s];
    return;
  }
  int m = (s + t) / 2;
  build(s, m, p * 2), build(m + 1, t, p * 2 + 1);//遞歸到左右子樹
  // 遞歸對左右區間建樹
  d[p] = d[p * 2] + d[(p * 2) + 1];
}

關於線段樹的空間:若是採用堆式存儲( \(2p\)\(p\) 的左兒子, \(2p+1\)\(p\) 的右兒子),如有 \(n\) 個葉子結點,則 d 數組的範圍最大爲 \(2^{\left\lceil\log{n}\right\rceil+1}\)

分析:容易知道線段樹的深度是 \(\left\lceil\log{n}\right\rceil\) 的,則在堆式儲存狀況下葉子節點(包括無用的葉子節點)數量爲 \(2^{\left\lceil\log{n}\right\rceil}\) 個,又因爲其爲一棵徹底二叉樹,則其總節點個數 \(2^{\left\lceil\log{n}\right\rceil+1}-1\) 。固然若是你懶得計算的話能夠直接把數組長度設爲 \(4n\) ,由於 \(\frac{2^{\left\lceil\log{n}\right\rceil+1}-1}{n}\) 的最大值在 \(n=2^{x}+1(x\in N_{+})\) 時取到,此時節點數爲 \(2^{\left\lceil\log{n}\right\rceil+1}-1=2^{x+2}-1=4n-5\)

線段樹的區間查詢

區間查詢,好比求區間 \([l,r]\) 的總和(即 \(a_l+a_{l+1}+ \cdots +a_r\) )、求區間最大值/最小值等操做。

以上面這張圖爲例,若是要查詢區間 \([1,5]\) 的和,那直接獲取 \(d_1\) 的值( \(60\) )便可。

若是要查詢的區間爲 \([3,5]\) ,此時就不能直接獲取區間的值,可是 \([3,5]\) 能夠拆成 \([3,3]\)\([4,5]\) ,能夠經過合併這兩個區間的答案來求得這個區間的答案。

通常地,若是要查詢的區間是 \([l,r]\) ,則能夠將其拆成最多爲 \(O(\log n)\)極大 的區間,合併這些區間便可求出 \([l,r]\) 的答案。

此處給出 C++ 的代碼實現,可參考註釋理解:

int getsum(int l, int r, int s, int t, int p) {
  // [l,r] 爲查詢區間,[s,t] 爲當前節點包含的區間,p 爲當前節點的編號
  if (l <= s && t <= r)
    return d[p];  // 當前區間爲詢問區間的子集時直接返回當前區間的和
  int m = (s + t) / 2, sum = 0;
  if (l <= m) sum += getsum(l, r, s, m, p * 2);
  // 若是左兒子表明的區間 [l,m] 與詢問區間有交集,則遞歸查詢左兒子
  if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
  // 若是右兒子表明的區間 [m+1,r] 與詢問區間有交集,則遞歸查詢右兒子
  return sum;
}

線段樹的區間修改與懶惰標記

若是要求修改區間 \([l,r]\) ,把全部包含在區間 \([l,r]\) 中的節點都遍歷一次、修改一次,時間複雜度沒法承受。咱們這裏要引入一個叫作 「懶惰標記」 的東西。

咱們設一個數組 \(b\)\(b_i\) 表示編號爲 \(i\) 的節點的懶惰標記值。爲了增強對懶惰標記的理解,此處舉個例子:

A 有兩個兒子,一個是 B,一個是 C。

有一天 A 要建一個新房子,沒錢。恰好過年嘛,有人要給 B 和 C 紅包,兩個紅包的錢數相同都是 \(1\) 元,然而由於 A 是父親因此紅包確定是先塞給 A 咯~

理論上來說 A 應該把兩個紅包分別給 B 和 C,可是……缺錢嘛,A 就把紅包偷偷收到本身口袋裏了。

A 高興地說:「我如今有 \(2\) 份紅包了!我又多了 \(2\times 1=2\) 元了!哈哈哈~」

可是 A 知道,若是他不把紅包給 B 和 C,那 B 和 C 確定會不爽而後致使家庭矛盾最後崩潰,因此 A 對兒子 B 和 C 說:「我欠大家每人 \(1\)\(1\) 元的紅包,下次有新紅包給過來的時候再給大家!這裏我先作下記錄……嗯……我欠大家各 \(1\) 元……」

兒子 B、C 有點惱怒:「但是若是有同窗問起咱們咱們收到了多少紅包咋辦?你把咱們的紅包都收了,咱們還怎麼裝?」

父親 A 趕緊說:「有同窗問起來我就會給大家的!我欠條都寫好了不會不算話的!」

這樣 B、C 才放了心。

在這個故事中咱們不難看出,A 就是父親節點,B 和 C 是 A 的兒子節點,並且 B 和 C 是葉子節點,分別對應一個數組中的值(就是以前講的數組 \(a\) ),咱們假設節點 A 表示區間 \([1,2]\) (即 \(a_1+a_2\) ),節點 B 表示區間 \([1,1]\) (即 \(a_1\) ),節點 C 表示區間 \([2,2]\) (即 \(a_2\) ),它們的初始值都爲 \(0\) (如今纔剛開始呢,還沒拿到紅包,因此都沒錢)。

如圖:

注:這裏 D 表示當前節點的值(即所表示區間的區間和)。
爲何節點 A 的 D 是 \(2\times 1=2\) 呢?緣由很簡單:節點 A 表示的區間是 \([1,2]\) ,一共包含 \(2\) 個元素。咱們是讓 \([1,2]\) 這個區間的每一個元素都加上 \(1\) ,因此節點 A 的值就加上了 \(2\times 1=2\) 咯。

若是這時候咱們要查詢區間 \([1,1]\) (即節點 B 的值),A 就把它欠的還給 B,此時的操做稱爲 下傳懶惰標記

具體是這樣操做(如圖):

注:爲何是加上 \(1\times 1=1\) 呢?由於 B 和 C 表示的區間中只有 \(1\) 個元素。

由此咱們能夠獲得,區間 \([1,1]\) 的區間和就是 \(1\)

區間修改(區間加上某個值):

void update(int l, int r, int c, int s, int t, int p) {
  // [l,r] 爲修改區間,c 爲被修改的元素的變化量,[s,t] 爲當前節點包含的區間,p
  // 爲當前節點的編號
  if (l <= s && t <= r) {
    d[p] += (t - s + 1) * c, b[p] += c;
    return;
  }  // 當前區間爲修改區間的子集時直接修改當前節點的值,而後打標記,結束脩改
  int m = (s + t) / 2;
  if (b[p] && s != t) {
    // 若是當前節點的懶標記非空,則更新當前節點兩個子節點的值和懶標記值
    d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
    b[p * 2] += b[p], b[p * 2 + 1] += b[p];  // 將標記下傳給子節點
    b[p] = 0;                                // 清空當前節點的標記
  }
  if (l <= m) update(l, r, c, s, m, p * 2);
  if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
  d[p] = d[p * 2] + d[p * 2 + 1];
}

區間查詢(區間求和):

int getsum(int l, int r, int s, int t, int p) {
  // [l,r] 爲查詢區間,[s,t] 爲當前節點包含的區間,p爲當前節點的編號
  if (l <= s && t <= r) return d[p];
  // 當前區間爲詢問區間的子集時直接返回當前區間的和
  int m = (s + t) / 2;
  if (b[p]) {
    // 若是當前節點的懶標記非空,則更新當前節點兩個子節點的值和懶標記值
    d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m),
        b[p * 2] += b[p], b[p * 2 + 1] += b[p];  // 將標記下傳給子節點
    b[p] = 0;                                    // 清空當前節點的標記
  }
  int sum = 0;
  if (l <= m) sum = getsum(l, r, s, m, p * 2);
  if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
  return sum;
}

若是你是要實現區間修改成某一個值而不是加上某一個值的話,代碼以下:

void update(int l, int r, int c, int s, int t, int p) {
  if (l <= s && t <= r) {
    d[p] = (t - s + 1) * c, b[p] = c;
    return;
  }
  int m = (s + t) / 2;
  if (b[p]) {
    d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),
          b[p * 2] = b[p * 2 + 1] = b[p];
    b[p] = 0;
  }
  if (l <= m) update(l, r, c, s, m, p * 2);
  if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
  d[p] = d[p * 2] + d[p * 2 + 1];
}
int getsum(int l, int r, int s, int t, int p) {
  if (l <= s && t <= r) return d[p];
  int m = (s + t) / 2;
  if (b[p]) {
    d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),
          b[p * 2] = b[p * 2 + 1] = b[p];
    b[p] = 0;
  }
  int sum = 0;
  if (l <= m) sum = getsum(l, r, s, m, p * 2);
  if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
  return sum;
}

一些優化

這裏總結幾個線段樹的優化:

  • 在葉子節點處無需下放懶惰標記,因此懶惰標記能夠不下傳到葉子節點。

  • 下放懶惰標記能夠寫一個專門的函數 pushdown ,從兒子節點更新當前節點也能夠寫一個專門的函數 maintain (或者對稱地用 pushup ),下降代碼編寫難度。

  • 標記永久化,若是肯定懶惰標記不會在中途被加到溢出(即超過了該類型數據所能表示的最大範圍),那麼就能夠將標記永久化。標記永久化能夠避免下傳懶惰標記,只需在進行詢問時把標記的影響加到答案當中,從而下降程序常數。具體如何處理與題目特性相關,需結合題目來寫。這也是樹套樹和可持久化數據結構中會用到的一種技巧。

線段樹基礎題推薦

<<| 是位運算,n << 1 == n * 2n << 1 | 1 == n * 2 + 1(再具體能夠查看 blog )。

luogu P3372【模板】線段樹 1

https://www.luogu.com.cn/problem/P3372

#include <iostream>
typedef long long LL;
LL n, a[100005], d[270000], b[270000];
void build(LL l, LL r, LL p) {
    if (l == r) {
        d[p] = a[l];
        return;
    }
    LL m = (l + r) >> 1;
    build(l, m, p << 1), build(m + 1, r, (p << 1) | 1);
    d[p] = d[p << 1] + d[(p << 1) | 1];
}
void update(LL l, LL r, LL c, LL s, LL t, LL p) {
    if (l <= s && t <= r) {
        d[p] += (t - s + 1) * c, b[p] += c;
        return;
    }
    LL m = (s + t) >> 1;
    if (b[p])
        d[p << 1] += b[p] * (m - s + 1), d[(p << 1) | 1] += b[p] * (t - m),
        b[p << 1] += b[p], b[(p << 1) | 1] += b[p];
    b[p] = 0;
    if (l <= m) update(l, r, c, s, m, p << 1);
    if (r > m) update(l, r, c, m + 1, t, (p << 1) | 1);
    d[p] = d[p << 1] + d[(p << 1) | 1];
}
LL getsum(LL l, LL r, LL s, LL t, LL p) {
    if (l <= s && t <= r) return d[p];
    LL m = (s + t) >> 1;
    if (b[p])
        d[p << 1] += b[p] * (m - s + 1), d[(p << 1) | 1] += b[p] * (t - m),
        b[p << 1] += b[p], b[(p << 1) | 1] += b[p];
    b[p] = 0;
    LL sum = 0;
    if (l <= m) sum = getsum(l, r, s, m, p << 1);
    if (r > m) sum += getsum(l, r, m + 1, t, (p << 1) | 1);
    return sum;
}
int main() {
    std::ios::sync_with_stdio(0);
    LL q, i1, i2, i3, i4;
    std::cin >> n >> q;
    for (LL i = 1; i <= n; i++) std::cin >> a[i];
    build(1, n, 1);
    while (q--) {
        std::cin >> i1 >> i2 >> i3;
        if (i1 == 2)
            std::cout << getsum(i2, i3, 1, n, 1) << std::endl;
        else
            std::cin >> i4, update(i2, i3, i4, 1, n, 1);
    }
    return 0;
}

luogu P3373【模板】線段樹 2

https://www.luogu.com.cn/problem/P3373

相比較於 P3372 ,此題多了個區間乘法。

一個 tag 彷佛應付不了了,那麼來兩個 tag 啊: addmul

1. 區間加法

仍是同樣。

s[pos].add = (s[pos].add + k) % mod;
s[pos].sum = (s[pos].sum + k * (s[pos].r - s[pos].l + 1)) % mod;

2. 區間乘法

這裏就有點不同了。

先把 mulsum 乘上 k

對於以前已經有的 add ,把它乘上 k 便可。在這裏,咱們把乘以後的值直接更新add的值。

你想, add 其實應該加到 sum 裏面,全部乘上 k 後,運用乘法分配律, (sum + add) * k == sum * k + add * k

這樣來實現 addsum 有序進行。

s[pos].add = (s[pos].add * k) % mod;
s[pos].mul = (s[pos].mul * k) % mod;
s[pos].sum = (s[pos].sum * k) % mod;

3. pushdown的維護

如今要下傳兩個標記: addmul

sum :由於 add 以前已經乘過,因此在子孩子乘過 mul 後直接加就行。

mul :直接乘。

add :由於 add 的值是要包括乘以後的值,因此子孩子要先乘上 mul

s[pos << 1].sum = (s[pos << 1].sum * s[pos].mul + s[pos].add * (s[pos << 1].r - s[pos << 1].l + 1)) % mod;

s[pos << 1].mul = (s[pos << 1].mul * s[pos].mul) % mod;

s[pos << 1].add = (s[pos << 1].add * s[pos].mul + s[pos].add) % mod;

代碼

#include<bits/stdc++.h>
using namespace std;
#define maxn 100010
typedef long long ll;
ll n, m, mod;
int a[maxn];
struct Segment_Tree {
	ll sum, add, mul;
	int l, r;
}s[maxn << 2];

void update(int pos) {
	s[pos].sum = (s[pos << 1].sum + s[(pos << 1) | 1].sum) % mod;
	return;
}

void pushdown(int pos) {//pushdown的維護
	s[pos << 1].sum = (s[pos << 1].sum * s[pos].mul + s[pos].add * (s[pos << 1].r - s[pos << 1].l + 1)) % mod;
	s[pos << 1 | 1].sum = (s[pos << 1 | 1].sum * s[pos].mul + s[pos].add * (s[pos << 1 | 1].r - s[pos << 1 | 1].l + 1)) % mod;

	s[pos << 1].mul = (s[pos << 1].mul * s[pos].mul) % mod;
	s[pos << 1 | 1].mul = (s[pos << 1 | 1].mul * s[pos].mul) % mod;

	s[pos << 1].add = (s[pos << 1].add * s[pos].mul + s[pos].add) % mod;
	s[pos << 1 | 1].add = (s[pos << 1 | 1].add * s[pos].mul + s[pos].add) % mod;

	s[pos].add = 0;
	s[pos].mul = 1;
	return;
}

void build_tree(int pos, int l, int r) { //建樹
	s[pos].l = l;
	s[pos].r = r;
	s[pos].mul = 1;
	if (l == r) {
		s[pos].sum = a[l] % mod;
		return;
	}
	int mid = (l + r) >> 1;
	build_tree(pos << 1, l, mid);
	build_tree(pos << 1 | 1, mid + 1, r);
	update(pos);
	return;
}

void ChangeMul(int pos, int x, int y, int k) { //區間乘法
	if (x <= s[pos].l && s[pos].r <= y) {
		s[pos].add = (s[pos].add * k) % mod;
		s[pos].mul = (s[pos].mul * k) % mod;
		s[pos].sum = (s[pos].sum * k) % mod;
		return;
	}
	pushdown(pos);
	int mid = (s[pos].l + s[pos].r) >> 1;
	if (x <= mid) ChangeMul(pos << 1, x, y, k);
	if (y > mid) ChangeMul(pos << 1 | 1, x, y, k);
	update(pos);
	return;
}

void ChangeAdd(int pos, int x, int y, int k) { //區間加法
	if (x <= s[pos].l && s[pos].r <= y) {
		s[pos].add = (s[pos].add + k) % mod;
		s[pos].sum = (s[pos].sum + k * (s[pos].r - s[pos].l + 1)) % mod;
		return;
	}
	pushdown(pos);
	int mid = (s[pos].l + s[pos].r) >> 1;
	if (x <= mid) ChangeAdd(pos << 1, x, y, k);
	if (y > mid) ChangeAdd(pos << 1 | 1, x, y, k);
	update(pos);
	return;
}

ll AskRange(int pos, int x, int y) { //區間詢問
	if (x <= s[pos].l && s[pos].r <= y) {
		return s[pos].sum;
	}
	pushdown(pos);
	ll val = 0;
	int mid = (s[pos].l + s[pos].r) >> 1;
	if (x <= mid) val = (val + AskRange(pos << 1, x, y)) % mod;
	if (y > mid) val = (val + AskRange(pos << 1 | 1, x, y)) % mod;
	return val;
}

int main() {
	//freopen("in.txt","r",stdin);
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
	ll q, i1, i2, i3, i4;
	cin >> n >> m >> mod;
	for (int i = 1; i <= n; ++i)cin >> a[i];
	build_tree(1, 1, n);
	for (int i = 1; i <= m; ++i) {
		cin >> i1 >> i2 >> i3;
		if (i1 == 1) 
			cin>>i4, ChangeMul(1, i2, i3, i4);
		if (i1 == 2)
			cin >> i4, ChangeAdd(1, i2, i3, i4);
		if (i1 == 3)
			cout << AskRange(1, i2, i3) << endl;
	}
	return 0;	
}

HihoCoder 1078 線段樹的區間修改

https://cn.vjudge.net/problem/HihoCoder-1078

#include <iostream>

int n, a[100005], d[270000], b[270000];
void build(int l, int r, int p) {
    if (l == r) {
        d[p] = a[l];
        return;
    }
    int m = (l + r) >> 1;
    build(l, m, p << 1), build(m + 1, r, (p << 1) | 1);
    d[p] = d[p << 1] + d[(p << 1) | 1];
}
void update(int l, int r, int c, int s, int t, int p) {
    if (l <= s && t <= r) {
        d[p] = (t - s + 1) * c, b[p] = c;
        return;
    }
    int m = (s + t) >> 1;
    if (b[p]) {
        d[p << 1] = b[p] * (m - s + 1), d[(p << 1) | 1] = b[p] * (t - m);
        b[p << 1] = b[(p << 1) | 1] = b[p];
        b[p] = 0;
    }
    if (l <= m) update(l, r, c, s, m, p << 1);
    if (r > m) update(l, r, c, m + 1, t, (p << 1) | 1);
    d[p] = d[p << 1] + d[(p << 1) | 1];
}
int getsum(int l, int r, int s, int t, int p) {
    if (l <= s && t <= r) return d[p];
    int m = (s + t) >> 1;
    if (b[p]) {
        d[p << 1] = b[p] * (m - s + 1), d[(p << 1) | 1] = b[p] * (t - m);
        b[p << 1] = b[(p << 1) | 1] = b[p];
        b[p] = 0;
    }
    int sum = 0;
    if (l <= m) sum = getsum(l, r, s, m, p << 1);
    if (r > m) sum += getsum(l, r, m + 1, t, (p << 1) | 1);
    return sum;
}
int main() {
    std::ios::sync_with_stdio(0);
    std::cin >> n;
    for (int i = 1; i <= n; i++) std::cin >> a[i];
    build(1, n, 1);
    int q, i1, i2, i3, i4;
    std::cin >> q;
    while (q--) {
        std::cin >> i1 >> i2 >> i3;
        if (i1 == 0)
            std::cout << getsum(i2, i3, 1, n, 1) << endl;
        else
            std::cin >> i4, update(i2, i3, i4, 1, n, 1);
    }
    return 0;
}

2018 Multi-University Training Contest 5 Problem G. Glad You Came

http://acm.hdu.edu.cn/showproblem.php?pid=6356

維護一下每一個區間的永久標記就能夠了,最後在線段樹上跑一邊 dfs 統計結果便可。注意打標記的時候加個剪枝優化,不然會 T。

拓展 - 貓樹

衆所周知線段樹能夠支持高速查詢某一段區間的信息和,好比區間最大子段和,區間和,區間矩陣的連乘積等等。

可是有一個問題在於普通線段樹的區間詢問在某些毒瘤的眼裏可能仍是有些慢了。

簡單來講就是線段樹建樹的時候須要作 \(O(n)\) 次合併操做,而每一次區間詢問須要作 \(O(\log{n})\) 次合併操做,詢問區間和這種東西的時候還能夠忍受,可是當咱們須要詢問區間線性基這種合併複雜度高達 \(O(\log^2{w})\) 的信息的話,此時就算是作 \(O(\log{n})\) 次合併有些時候在時間上也是不可接受的。

而所謂 "貓樹" 就是一種不支持修改,僅僅支持快速區間詢問的一種靜態線段樹。

構造一棵這樣的靜態線段樹須要 \(O(n\log{n})\) 次合併操做,可是此時的查詢複雜度被加速至 \(O(1)\) 次合併操做。

在處理線性基這樣特殊的信息的時候甚至能夠將複雜度降至 \(O(n\log^2{w})\)

原理

在查詢 \([l,r]\) 這段區間的信息和的時候,將線段樹樹上表明 \([l,l]\) 的節點和表明 \([r,r]\) 這段區間的節點在線段樹上的 lca 求出來,設這個節點 \(p\) 表明的區間爲 \([L,R]\) ,咱們會發現一些很是有趣的性質:

  1. \([L,R]\) 這個區間必定包含 \([l,r]\)

顯然,由於它既是 \(l\) 的祖先又是 \(r\) 的祖先。

  1. 這個區間必定跨越 \([L,R]\) 的中點

因爲 \(p\)\(l\)\(r\)lca,這意味着 \(p\) 的左兒子是 \(l\) 的祖先而不是 \(r\) 的祖先, \(p\) 的右兒子是 \(r\) 的祖先而不是 \(l\) 的祖先。

所以 \(l\) 必定在 \([L,MID]\) 這個區間內, \(r\) 必定在 \((MID,R]\) 這個區間內。

有了這兩個性質,咱們就能夠將詢問的複雜度降至 \(O(1)\) 了。

實現

具體來說咱們建樹的時候對於線段樹樹上的一個節點,設它表明的區間爲 \((l,r]\)

不一樣於傳統線段樹在這個節點裏只保留 \([l,r]\) 的和,咱們在這個節點裏面額外保存 \((l,mid]\) 的後綴和數組和 \((mid,r]\) 的前綴和數組。

這樣的話建樹的複雜度爲 \(T(n)=2T(n/2)+O(n)=O(n\log{n})\) 同理空間複雜度也從原來的 \(O(n)\) 變成了 \(O(n\log{n})\)

下面是最關鍵的詢問了~

若是咱們詢問的區間是 \([l,r]\) 那麼咱們把表明 \([l,l]\) 的節點和表明 \([r,r]\) 的節點的 lca 求出來,記爲 \(p\)

根據剛纔的兩個性質, \(l,r\)\(p\) 所包含的區間以內而且必定跨越了 \(p\) 的中點。

這意味這一個很是關鍵的事實是咱們可使用 \(p\) 裏面的前綴和數組和後綴和數組,將 \([l,r]\) 拆成 \([l,mid]+(mid,r]\) 從而拼出來 \([l,r]\) 這個區間。

而這個過程僅僅須要 \(O(1)\) 次合併操做!

不過咱們好像忽略了點什麼?

彷佛求 lca 的複雜度彷佛還不是 \(O(1)\) ,暴力求是 \(O(\log{n})\) 的,倍增法則是 \(O(\log{\log{n}})\) 的,轉 ST 表的代價又太大……

堆式建樹

具體來將咱們將這個序列補成 2 的整次冪,而後建線段樹。

此時咱們發現線段樹上兩個節點的 lca 編號,就是兩個節點二進制編號的 lcp

稍做思考便可發現發如今 \(x\)\(y\) 的二進制下 lcp(x,y)=x>>log[x^y]

因此咱們預處理一個 log 數組便可輕鬆完成求 lca 的工做。

這樣咱們就完成了一個貓樹。

因爲建樹的時候涉及到求前綴和和求後綴和,因此對於線性基這種雖然合併是 \(O(\log^2{w})\) 可是求前綴和倒是 \(O(n\log{n})\) 的信息,使用貓樹能夠將靜態區間線性基從 \(O(n\log^2{w}+m\log^2{w}\log{n})\) 優化至 \(O(n\log{n}\log{w}+m\log^2{w})\) 的複雜度。

應用

經典問題:區間最大子段和

給出一個序列,屢次詢問區間的最大子段和。

這是一個經典的模型。不一樣於經典作法,咱們只須要記錄 prelprel、prerprer 爲對應前(後)綴的最大子段和、最大前(後)綴和便可。

複雜度:\(O(nlog⁡n)+O(1)+O(nlog⁡n)\)。實測在 \(n=m=200000,a_i≤10^9\) 的狀況下,此作法的運行時間接近經典作法(非遞歸線段樹實現)的 \(2/{3}\)

經典問題:NAND

給出一個序列,屢次詢問一個 xx 對一個區間中全部數按順序依次 NAND 的結果。

NAND 沒有結合律,所以咱們要用一些經典的處理方式。NAND 是按位獨立的,所以咱們能夠對每一位維護信息:若是這一位剛開始是 0,那麼按順序 NAND 了這個區間中的全部數後會變成什麼;若是這一位是 1 那麼會變成多少。用位運算能夠優化爲 \(O(1)\) 的信息合併。

使用貓樹,便可直接支持 \(O(nlog⁡n)+O(1)+O(nlog⁡n)\)

經典問題:區間凸包

參考

immortalCO 大爺的博客

線段樹(segment tree)

三點水 線段樹 (區間樹)

其它

文章開源在 Github - blog-articles,點擊 Watch 便可訂閱本博客。 若文章有錯誤,請在 Issues 中提出,我會及時回覆,謝謝。

若是您以爲文章不錯,或者在生活和工做中幫助到了您,不妨給個 Star,謝謝。

(文章完)

相關文章
相關標籤/搜索