菜鳥都能理解的線段樹入門經典

線段樹的定義

首先,線段樹既是線段也是樹,並且是一棵二叉樹,每個結點是一條線段,每條線段的左右兒子線段分別是該線段的左半和右半區間,遞歸定義之後就是一棵線段樹,圖示如下



圖1.線段樹示意圖


定義線段樹的數據結構

struct Line{
  int left, right, count;
  Line *leftChild, *rightChild;
  Line(int l, int r): left(l), right(r) {}

};

PS:其中的count字段表示該條線段有幾條

明白了線段樹的定義之後,我們來舉一個例子來說明線段樹的應用

例題:給定N條線段,{[2, 5], [4, 6], [0, 7]}, M個點{2, 4, 7},判斷每個點分別在幾條線段出現過

看到題目,有的人第一感覺想出來的算法便是對於每一個點,逐個遍歷每一條線段,很輕鬆地判斷出來每個點在幾條線段出現過,小學生都會的算法,時間複雜度爲O(M*N)

如國N非常大,比如2^32-1, M也非常大M = 2^32 - 1, O(M*N)的算法將是無法忍受的,這個時候,線段樹便隆重登場了

線段樹的解法:

1.首先,我們找出一個最大的區間能夠覆蓋所有的線段,遍歷所有的線段,找線段的最值(左端點的最小值,右端點的最大值)便可以確定這個區間,對於{[2, 5], [4, 6], [0, 7]}, 這個區間爲[0, 7],時間複雜度爲O(N)

2.然後,根據此區間建一棵線段樹(見圖1), 時間複雜度爲O(log(MAX-MIN))

3.對於每一條線段A,從根節點開始遍歷這棵線段樹,對於每一個當前遍歷的結點NODE(其實線段樹中每一個結點就是一條線段),考慮三種情況

   a)如果線段A包含在線段NODE的左半區間,那麼從NODE的左兒子(其實就是NODE的左半區間)開始遍歷這棵樹

   b)如果線段A包含在線段NODE的右半區間,那麼從NODE的右兒子(其實就是NODE的右半區間)開始遍歷這棵樹

   c)如果線段A剛好和線段NODE重合,停止遍歷,並將NODE中的count字段加1

   d)除了以上的情況,就將線段A的左半部分在NODE的左兒子處遍歷,將A的右半部分在NODE的右兒子處遍歷

補充說明:對於以上的步驟,所做的工作其實就是不斷地分割每一條線段,使得分割後的每一條小線段剛好能夠落在線段樹上,舉個例子,比如要分割[2, 5],首先將[2, 5]和[0, 7]比較,符合情況d, 將A分成[2, 3]與[4, 5] 

I)對於[2, 3]從[0, 7]的左半區間[0, 3]開始遍歷

     將[2, 3]與[0, 3]比較,滿足情況b,那麼從[0, 3]的右半區間[2, 3]開始遍歷,發現剛好重合,便將結點[2, 3]count字段加1

II)對於[4, 5]從[0, 7]的右半區間[4, 7]開始遍歷

     將[4, 5]與[4, 7]比較,滿足情況b,從[4, 7]的左半區間[4, 5]開始遍歷,發現剛好重合,便將結點[4, 5]count字段加1

於是對於[2, 5]分割之後線段樹的情況爲圖2


圖2.分割[2,5]之後線段樹的情況


顯然,我們看到,上述的遍歷操作起始就是將[2, 5]按照線段樹中的線段來分割,分割後的[2, 3]與[4, 5]其實是與[2, 5]完全等效的

最後,我們將剩下的兩條線段按照同樣的步驟進行分割之後,線段樹的情況如下圖3



這一步的時間複雜度爲 O(N*log(MAX-MIN))

4.最後,對於每一個值我們就可以開始遍歷這一顆線段樹,加上對於結點的count字段便是在線段中出現的次數

比如對於4,首先遍歷[0, 7],次數 = 0+1=1;4在右半區間,遍歷[4, 7],次數 = 1+0=0;4在[4, 7]左半區間, 次數 = 1+2=3;4在[4, 5]左半區間,次數 = 3+0 = 4,遍歷結束,次數 = 3說明4在三條線段中出現過,同理可求其他的值,這一步的時間複雜度爲O(M*log(MAX-MIN))

最後,總的時間複雜度爲O(N)+O(log(MAX-MIN))+O(N*log(MAX-MIN))+(M*log(MAX-MIN)) = O((M+N)*log(MAX-MIN))

由於log(MAX-MIX)<=64所以最後的時間複雜度爲O(M+N)


最後,放出源碼

#include <iostream>
using namespace std;
struct Line{
  int left, right, count;
  Line *leftChild, *rightChild;
  Line(int l, int r): left(l), right(r) {}
};

//建立一棵空線段樹
void createTree(Line *root) {
  int left = root->left;
  int right = root->right;
  if (left < right) {
    int mid = (left + right) / 2;
    Line *lc =  new Line(left, mid);
    Line *rc =  new Line(mid + 1, right);
    root->leftChild = lc;
    root->rightChild = rc;
    createTree(lc);
    createTree(rc);
  }
}

//將線段[l, r]分割
void insertLine(Line *root, int l, int r) {
  cout << l << " " << r << endl;
  cout << root->left << " " << root->right << endl << endl;
  if (l == root->left && r == root->right) {
    root->count += 1;
  } else if (l <= r) {
    int rmid = (root->left + root->right) / 2;
    if (r <= rmid) {
      insertLine(root->leftChild, l, r);
    } else if (l >= rmid + 1) {
      insertLine(root->rightChild, l, r);
    } else {
      int mid = (l + r) / 2;
      insertLine(root->leftChild, l, mid);
      insertLine(root->rightChild, mid + 1, r);
    }
  }
}
//樹的中序遍歷(測試用)
void inOrder(Line* root) {
  if (root != NULL) {
    inOrder(root->leftChild);
    printf("[%d, %d], %d\n", root->left, root->right, root->count);
    inOrder(root->rightChild);
  }
}

//獲取值n在線段上出現的次數
int getCount(Line* root, int n) {
  int c = 0;
  if (root->left <= n&&n <= root->right)
    c += root->count;
  if (root->left == root->right)
    return c;
  int mid = (root->left + root->right) / 2;
  if (n <= mid)
    c += getCount(root->leftChild, n);
  else
    c += getCount(root->rightChild, n);
  return c;
}
int main() {
  int l[3] = {2, 4, 0};
  int r[3] = {5, 6, 7};
  int MIN = l[0];
  int MAX = r[0];
  for (int i = 1; i < 3; ++i) {
    if (MIN > l[i]) MIN = l[i];
    if (MAX < r[i]) MAX = r[i];
  }
  Line *root = new Line(MIN, MAX);
  createTree(root);
  for (int i = 0; i < 3; ++i) {
    insertLine(root, l[i], r[i]);
  }
  inOrder(root);
  int N;
  while (cin >> N) {
    cout << getCount(root, N) << endl;
  }
  return 0;
}

發佈了140 篇原創文章 · 獲贊 70 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章