天天看點

資料結構 線段樹--權值線段樹 詳解

😊 | Powered By HeartFireY | Weighted Segment Tree

一、權值線段樹 簡介

1.線段樹

線段樹是一種用于維護區間資訊的高效資料結構,可以在

線段樹維護的資訊必須滿足可加性,即能以可以接受的速度合并資訊和修改資訊,包括在使用Lazy标記時,标記也要滿足可加性

具體有關線段樹的的講解請參考部落格:線段樹 詳解與模闆

2.權值線段樹

權值線段樹是一種建立在基本線段樹之上的資料結構。是以它的基本原理仍是基于對區間的維護操作。但與線段樹相區分的點是:權值線段樹維護的資訊與基本線段樹有所差異:

  • 基本線段樹中每個系欸但用來維護一段區間的最大值或總和等;
  • 權值線段樹每個結點維護的是一個區間的數出現的次數。

實際上,權值線段樹可以看作是一個桶,桶的操作它都支援,并且能夠以更快的方式去完成。

根據權值線段樹的性質,我們可以知道其主要用途:

權值線段樹一般用于維護一段區間的數出現的次數,從它的定義來看,它可以快速計算出一段區間的數的出現次數。

在實際應用中,我們使用權值線段樹查詢區間第大的值。

二、權值線段樹的基本原理與操作

1.權值線段樹維護資訊的原理

基礎線段樹和權值線段樹的一個較大的差別點在于:(此處特指使用數組儲存樹結構)基礎線段樹根據初始的一個序列建立樹結構,具有單獨的建樹操作;而權值線段樹不需要進行建樹操作,初始狀态下預設建立一棵所有點權為的空樹,對于每個元素,在周遊的時候相應的結點做自增運算。換而言之:

  • 權值線段是一棵已經建好的線段樹,儲存樹結構的下标範圍根據區間最大值最小值确定(一般開大空間即可);
  • 初始狀态下所有的點權均為;
  • 執行插入元素的操作時,會導緻插入值對應的,同時引發單點更新操作。
  • 可以從上述描述中得到基本推論:向空的權值線段樹中插入一個無序序列,則插入完成後在樹上無法再得到原序列的周遊,但能通過周遊得到有序序列(無序序列變有序序列)

我們可以通過結構對比線段樹與權值線段樹的差別,以此了解原理。

我們給定序列,以該序列為例建立兩種線段樹。

首先,序列長度為,建立基礎區間和查詢線段樹,結構圖如下:

資料結構 線段樹--權值線段樹 詳解

對應具體的細節不再闡述。我們繼續建立權值線段樹:首先給出一棵空樹:

(注:為示範友善,A數組下标做+1操作,樹上的範圍也對應變化,這裡懶得改了~)

資料結構 線段樹--權值線段樹 詳解

然後按照序列順序插入元素:

資料結構 線段樹--權值線段樹 詳解

解釋:

:原序列中存在值為的元素個

:原序列中存在值為的元素個

:原序列中存在值為的元素個

:原序列中存在值為的元素個

:一定區間内所含元素的個數

我們繼續探究查找第大元素的方法:

由于權值線段樹每個節點記錄了某段區間内包含的元素個數,且元素在葉子節點構成的序列中是有序的:

  • 對于整列數中第大/小的值,我們從根節點開始判斷(這裡按第大為例),如果比右兒子大,就說明第大的數在左兒子所表示的值域中。然後,要減去右兒子。(這是因為繼續遞歸下去的時候,正确答案,整個區間的第大已經不再是左兒子表示區間的第大了)。如此遞歸到葉子節點時即為答案。
  • 整棵線段樹的根節點就表示整個值域有幾個數。

我們很容易看出:在權值線段樹中,采用元素到下标數組映射的方式進行插入。這樣會導緻一個問題:在資料離散程度較大的情況下,空間的利用效率比較低。是以我們對于線段樹所開空間不足時,常采用離散化的方式進行解決。

📕 | 此處的前導知識:離散化(具體的參見離散化的講解)

2.權值線段樹的基本操作與實作

①.查詢第大/小元素

假設,那麼查詢過程是怎樣的?

首先我們從根節點出發,由于要查詢第大,是相對于終點而言的,是以從右子結點開始判斷:

資料結構 線段樹--權值線段樹 詳解

目前節點右子樹包含個元素,是以應該向左子樹周遊,注意:此時應該減去右子樹的個元素!

資料結構 線段樹--權值線段樹 詳解

尋找第小的操作與上方類似,差別在于相對于起點OR終點而言(周遊時對左右子樹的判斷順序)。

再次回顧整個步驟:對于整列數中第大/小的值,我們從根節點開始判斷(這裡按第大為例),如果比右兒子大,就說明第大的數在左子樹中。然後,要減去右子節點的值。(這是因為繼續遞歸下去的時候,正确答案,整個區間的第大已經不再是左子樹表示區間的第大了)。如此遞歸到葉子節點時即為答案

根據以上分析步驟,我們可以寫出查詢第大和第小的函數:

int query_kth(int pos, int l, int r, int k){
    if(l == r) return l;
    int mid = (l + r) >> 1, right = tree[pos << 1 | 1];
    if(k <= right) return query(pos << 1 | 1, mid + 1, r, k);
    else return query(pos << 1, l, mid, k - right);
}      
int query_kth(int pos, int l, int r, int k){
    if(l == r) return l;
    int mid = (l + r) >> 1, left = tree[pos << 1];
    if(k <= left) return query(pos << 1, l, mid, k);
    else return query(pos << 1 | 1, mid + 1, r, k - left);
}      

②.單點更新操作

類似基礎線段樹,遞歸到葉子節點然後回溯

void add(int l, int r, int v, int x){
    if (l == r) tree[v]++;
    else{
        int mid = (l + r) >> 1;
        if (x <= mid) add(l, mid, v << 1, x);
        else add(mid + 1, r, v << 1 | 1, x);
        tree[v] = tree[v << 1] + tree[v << 1 | 1];
    }
}      

③.查詢某個數出現的個數

樹上二分查找的思想,代碼如下:

int find(int l, int r, int v, int x){
    if (l == r) return tree[v];
    else{
        int mid = (l + r) >> 1;
        if (x <= mid) return find(l, mid, v << 1, x);
        else return find(mid + 1, r, v << 1 | 1, x);
    }
}      

④.查詢一段區間的數出現的次數

int find(int l, int r, int v, int x, int y){
    if (l == x && r == y) return tree[v];
    else{
        int mid = (l + r) >> 1;
        if (y <= mid) return find(l, mid, v << 1, x, y);
        else if (x > mid) return find(mid + 1, r, v << 1 | 1, x, y);
        else return find(l, mid, v << 1, x, mid) + find(mid + 1, r, v << 1 | 1, mid + 1, y);
    }
}      

3.區間離散化

int get_discretization(){
    for (int i = 1; i <= n; ++i) a[i] = read(), b[i] = a[i]; //讀入資料
    sort(b + 1, b + n + 1);
    int len = unique(b + 1, b + n + 1) - b - 1;
    for (int i = 1; i <= n; ++i){
        int pos = lower_bound(b + 1, b + len + 1, a[i]) - b;
        a[i] = pos;
    } //離散化
}      

繼續閱讀