線段樹動態開點詳解

線段樹是一種用於處理區間問題的數據結構,其應用非常廣泛。但在實際應用過程中,線段樹的空間複雜度往往是固定的,而事實上,我們並不總是需要處理整個區間,這樣就導致了空間浪費。動態開點線段樹就是為了解決這個問題而誕生的。本文將從多個方面對線段樹動態開點做詳細的闡述。

一、讀入數據

為了實現動態開點,首先需要我們先讀入數據。一個線段樹最大的葉子結點編號為n,如果我們已知了需要處理區間的右端點r,那麼這棵線段樹空間就可以直接開到2n。但是如果我們不知道r的值,線段樹的空間就不能提前開到2n,否則就會造成空間浪費。正確的做法是使用動態開點技術,根據需要動態的開啟和關閉每一個節點,這樣就可以避免空間浪費了。

下面是讀入數據的代碼示例:

struct SegmentTree {
    int l, r, sum; // sum 表示該區間內元素的和
    int lt, rt; //左右兒子的編號
    #define lson tr[u].lt
    #define rson tr[u].rt
} tr[MAXN*40]; //開到足夠大的數量

int n, m, idx; //idx 表示當前空閑結點編號
int root[MAXN]; // root[i] 表示以第 i 個數為右端點建立的線段樹的根節點

void build(int u, int l, int r) {
    if(l == r) return; //到達葉節點,返回
    int mid = (l + r) >> 1;
    lson = ++idx; // 左兒子編號為 idx + 1,並將 idx 加 1
    rson = ++idx; // 右兒子編號為 idx + 2,並將 idx 再次加 1
    build(lson, l, mid);
    build(rson, mid + 1, r);
}

int modify(int u, int l, int r, int x, int k) {
    int p = ++idx; // 開啟新的結點 p
    tr[p] = tr[u]; // 複製原節點的內容
    if(l == r) {
        tr[p].sum += k; // 到達葉子結點,直接修改數據
        return p;
    }
    int mid = (l + r) >> 1;
    if(x <= mid) tr[p].lt = modify(lson, l, mid, x, k);
    else tr[p].rt = modify(rson, mid + 1, r, x, k);
    tr[p].sum = tr[tr[p].lt].sum + tr[tr[p].rt].sum;
    return p;
}

二、動態開點線段樹區間修改

在線段樹上做區間修改的時候,我們需要先找到要修改的區間的左右端點,然後不斷遞歸地向下處理,直到到達葉子節點,最後再遞歸回來更新每個節點的信息。由於我們使用了動態開點技術,所以在遍歷的時候,我們不能像普通線段樹一樣通過左右兒子的編號訪問它們,而是需要使用動態開點得到它們的實際編號。

下面是區間修改的代碼示例:

void modify(int p, int u, int l, int r, int ql, int qr, int k) {
    if(ql = r) { // 區間在當前區間內
        tr[p].sum += k * (r - l + 1);
        tr[p].tag += k; // 標記 one
        return;
    }
    int mid = (l + r) >> 1;
    if(ql <= mid) {
        if(!tr[u].lt) tr[u].lt = ++idx; // 左兒子第一次被訪問時才開點
        modify(p < mid) {
        if(!tr[u].rt) tr[u].rt = ++idx; // 右兒子第一次被訪問時才開點
        modify(p << 1 | 1, tr[u].rt, mid + 1, r, ql, qr, k);
    }
    tr[p].sum = tr[p << 1].sum + tr[p << 1 | 1].sum + tr[u].tag * (r - l + 1); //更新信息
}

三、動態線段樹選取相關特殊操作

動態開點線段樹有一些特殊操作和普通線段樹不同,它們可以提高我們求解某些問題的效率。下面介紹一些比較常見的操作:

1. 求區間某個位置的值

在普通線段樹中,我們可以很輕鬆地求區間 [l, r] 中的最大值或最小值,但是如果要求區間內任意一個位置 i 的值時,就需要使用另外一種方法了。我們可以用一個數組 id 保存每個節點對應的區間範圍,然後根據查詢的位置不斷遞歸向下至葉子結點。如圖3-1所示。

下面是查詢區間某個位置的代碼示例:

int query(int p, int u, int l, int r, int x) {
    if(l == r) return tr[p].sum; // 已經到達葉節點
    int mid = (l + r) >> 1;
    if(x <= mid) {
        if(!tr[u].lt) tr[u].lt = ++idx; // 左兒子第一次被訪問時才開點
        return query(p << 1, tr[u].lt, l, mid, x) + tr[p].tag * (x - l + 1);
    }
    else {
        if(!tr[u].rt) tr[u].rt = ++idx; // 右兒子第一次被訪問時才開點
        return query(p << 1 | 1, tr[u].rt, mid + 1, r, x) + tr[p].tag * (r - x + 1);
    }
}

2. 求區間 [r-i+1, r] 任意數的和

在維護一段數列區間的前綴和時,我們可以直接使用線段樹來維護前綴和。但是,在動態開點線段樹中,如果要求區間 [r-i+1, r] 中任意數的和時,我們可以使用數據結構「主席樹」來維護。

下面是求區間 [r-i+1, r] 任意數的和的代碼示例:

void modify(int p, int u, int l, int r, int x, int k) {
    tr[p].sum += k;
    if(l == r) return;
    int mid = (l + r) >> 1;
    if(x <= mid) { // 修改左兒子的值
        if(!tr[u].lt) tr[u].lt = ++idx; // 左兒子第一次被訪問時才開點
        modify(p << 1, tr[u].lt, l, mid, x, k);
    }
    else { // 修改右兒子的值
        if(!tr[u].rt) tr[u].rt = ++idx; // 右兒子第一次被訪問時才開點
        modify(p << 1 | 1, tr[u].rt, mid + 1, r, x, k);
    }
}

int query(int p, int ul, int ur, int l, int r, int k) {
    if(l == r) return l;
    int cnt = tr[tr[p << 1 | 1].lt].sum - tr[tr[p <> 1;
    if(cnt >= k) return query(p << 1 | 1, tr[ul].rt, tr[ur].rt, mid + 1, r, k);
    else return query(p << 1, tr[ul].lt, tr[ur].lt, l, mid, k - cnt);
    // 在左兒子中查找
}

3. 求區間第 k 大/小的數

求解區間第 k 大/小的數,可以用上面的「主席樹」來解決。我們可以對每個節點開一個小根堆,記錄著區間內所有數字,然後就用主席樹的方法求解區間第 k 大/小的數。

下面是查詢區間第 k 大/小的代碼示例:

void pushup(int u) {
    tr[u].sum = tr[tr[u].lt].sum + tr[tr[u].rt].sum;
}
void modify(int p, int u, int l, int r, int x, int k) {
    tr[p].st.push(k); //將當前數加入小根堆中
    if(l == r) return;
    int mid = (l + r) >> 1;
    if(x <= mid) {
        if(!tr[u].lt) tr[u].lt = ++idx; // 左兒子第一次被訪問時才開點
        modify(p << 1, tr[u].lt, l, mid, x, k);
    }
    else {
        if(!tr[u].rt) tr[u].rt = ++idx; // 右兒子第一次被訪問時才開點
        modify(p << 1 | 1, tr[u].rt, mid + 1, r, x, k);
    }
    pushup(u);
}

int query(int p, int ul, int ur, int l, int r, int k) {
    if(l == r) return l;
    int cnt = 0;
    //找出區間 [ul, ur] 中包含的數的個數
    for(int i = 0; i < tr[tr[p << 1].rt].st.size(); i++)
        if(tr[tr[p << 1].rt].st.top() <= r && tr[tr[p <= l) cnt++;
    int mid = (l + r) >> 1;
    if(cnt >= k) return query(p << 1, tr[ul].lt, tr[ur].lt, l, mid, k);
    else return query(p << 1 | 1, tr[ul].rt, tr[ur].rt, mid + 1, r, k - cnt);
}

總結

動態開點線段樹是一種非常實用的數據結構,其空間利用率非常高,能夠滿足很多實際問題的需求。在實現過程中,需要認真分析問題,考慮到各種特殊情況,才能寫出優秀的代碼,提高算法效率。

原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/290901.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
小藍的頭像小藍
上一篇 2024-12-24 13:14
下一篇 2024-12-24 13:14

相關推薦

  • QML 動態加載實踐

    探討 QML 框架下動態加載實現的方法和技巧。 一、實現動態加載的方法 QML 支持從 JavaScript 中動態指定需要加載的 QML 組件,並放置到運行時指定的位置。這種技術…

    編程 2025-04-29
  • Python愛心代碼動態

    本文將從多個方面詳細闡述Python愛心代碼動態,包括實現基本原理、應用場景、代碼示例等。 一、實現基本原理 Python愛心代碼動態使用turtle模塊實現。在繪製一個心形的基礎…

    編程 2025-04-29
  • t3.js:一個全能的JavaScript動態文本替換工具

    t3.js是一個非常流行的JavaScript動態文本替換工具,它是一個輕量級庫,能夠很容易地實現文本內容的遞增、遞減、替換、切換以及其他各種操作。在本文中,我們將從多個方面探討t…

    編程 2025-04-28
  • 使用easypoi創建多個動態表頭

    本文將詳細介紹如何使用easypoi創建多個動態表頭,讓表格更加靈活和具有可讀性。 一、創建單個動態表頭 easypoi是一個基於POI操作Excel的Java框架,支持通過註解的…

    編程 2025-04-28
  • Python動態輸入: 從基礎使用到應用實例

    Python是一種高級編程語言,因其簡單易學和可讀性而備受歡迎。Python允許程序員通過標準輸入或命令行獲得用戶輸入,這使得Python語言無法預測或控制輸入。在本文中,我們將詳…

    編程 2025-04-28
  • Python動態規劃求解公共子串

    本文將從以下多個方面對公共子串Python動態規划進行詳細闡述: 一、什麼是公共子串? 公共子串是指在兩個字符串中同時出現且連續的子串。例如,字符串”ABCD&#822…

    編程 2025-04-27
  • 使用Thymeleaf動態渲染下拉框

    本文將從下面幾個方面,詳細闡述如何使用Thymeleaf動態渲染下拉框: 一、Thymeleaf是什麼 Thymeleaf是一款Java模板引擎,可用於Web和非Web環境中的應用…

    編程 2025-04-27
  • 動態規劃例題用法介紹

    本文將以動態規劃(Dynamic Programming, DP)例題為中心,深入闡述動態規劃的原理和應用。 一、最長公共子序列問題 最長公共子序列問題(Longest Commo…

    編程 2025-04-27
  • Linux sync詳解

    一、sync概述 sync是Linux中一個非常重要的命令,它可以將文件系統緩存中的內容,強制寫入磁盤中。在執行sync之前,所有的文件系統更新將不會立即寫入磁盤,而是先緩存在內存…

    編程 2025-04-25
  • IPv6動態域名解析的實現和應用

    一、IPv6的動態域名解析概述 IPv6是下一代互聯網協議,解決了IPv4中IP地址不足的問題。IPv6的地址長度為128位,地址空間巨大,同時支持更多的安全和網絡管理特性。動態域…

    編程 2025-04-25

發表回復

登錄後才能評論