菜鸟都能理解的线段树入门经典

线段树的定义

首先,线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树,图示如下



图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)


最后,放出源码

  1. #include <iostream>  
  2. using namespace std;  
  3. struct Line{  
  4.   int left, right, count;  
  5.   Line *leftChild, *rightChild;  
  6.   Line(int l, int r): left(l), right(r) {}  
  7. };  
  8.   
  9. //建立一棵空线段树  
  10. void createTree(Line *root) {  
  11.   int left = root->left;  
  12.   int right = root->right;  
  13.   if (left < right) {  
  14.     int mid = (left + right) / 2;  
  15.     Line *lc =  new Line(left, mid);  
  16.     Line *rc =  new Line(mid + 1, right);  
  17.     root->leftChild = lc;  
  18.     root->rightChild = rc;  
  19.     createTree(lc);  
  20.     createTree(rc);  
  21.   }  
  22. }  
  23.   
  24. //将线段[l, r]分割  
  25. void insertLine(Line *root, int l, int r) {  
  26.   cout << l << " " << r << endl;  
  27.   cout << root->left << " " << root->right << endl << endl;  
  28.   if (l == root->left && r == root->right) {  
  29.     root->count += 1;  
  30.   } else if (l <= r) {  
  31.     int rmid = (root->left + root->right) / 2;  
  32.     if (r <= rmid) {  
  33.       insertLine(root->leftChild, l, r);  
  34.     } else if (l >= rmid + 1) {  
  35.       insertLine(root->rightChild, l, r);  
  36.     } else {  
  37.       int mid = (l + r) / 2;  
  38.       insertLine(root->leftChild, l, mid);  
  39.       insertLine(root->rightChild, mid + 1, r);  
  40.     }  
  41.   }  
  42. }  
  43. //树的中序遍历(测试用)  
  44. void inOrder(Line* root) {  
  45.   if (root != NULL) {  
  46.     inOrder(root->leftChild);  
  47.     printf("[%d, %d], %d\n", root->left, root->right, root->count);  
  48.     inOrder(root->rightChild);  
  49.   }  
  50. }  
  51.   
  52. //获取值n在线段上出现的次数  
  53. int getCount(Line* root, int n) {  
  54.   int c = 0;  
  55.   if (root->left <= n&&n <= root->right)  
  56.     c += root->count;  
  57.   if (root->left == root->right)  
  58.     return c;  
  59.   int mid = (root->left + root->right) / 2;  
  60.   if (n <= mid)  
  61.     c += getCount(root->leftChild, n);  
  62.   else  
  63.     c += getCount(root->rightChild, n);  
  64.   return c;  
  65. }  
  66. int main() {  
  67.   int l[3] = {2, 4, 0};  
  68.   int r[3] = {5, 6, 7};  
  69.   int MIN = l[0];  
  70.   int MAX = r[0];  
  71.   for (int i = 1; i < 3; ++i) {  
  72.     if (MIN > l[i]) MIN = l[i];  
  73.     if (MAX < r[i]) MAX = r[i];  
  74.   }  
  75.   Line *root = new Line(MIN, MAX);  
  76.   createTree(root);  
  77.   for (int i = 0; i < 3; ++i) {  
  78.     insertLine(root, l[i], r[i]);  
  79.   }  
  80.   inOrder(root);  
  81.   int N;  
  82.   while (cin >> N) {  
  83.     cout << getCount(root, N) << endl;  
  84.   }  
  85.   return 0;  
  86. }  
#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;
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于计算机专业的学生而言,参加各类比赛能够带来多方面的益处,具体包括但不限于以下几点: 技能提升: 参与比赛促使学生深入学习和掌握计算机领域的专业知识与技能,如编程语言、算法设计、软件工程、网络安全等。 比赛通常涉及实际问题的解决,有助于将理论知识应用于实践中,增强问题解决能力。 实践经验: 大多数比赛都要求参赛者设计并实现解决方案,这提供了宝贵的动手操作机会,有助于积累项目经验。 实践经验对于计算机专业的学生尤为重要,因为雇主往往更青睐有实际项目背景的候选人。 团队合作: 许多比赛鼓励团队协作,这有助于培养学生的团队精神、沟通技巧和领导能力。 团队合作还能促进学生之间的知识共享和思维碰撞,有助于形成更全面的解决方案。 职业发展: 获奖经历可以显著增强简历的吸引力,为求职或继续深造提供有力支持。 某些比赛可能直接与企业合作,提供实习、工作机会或奖学金,为学生的职业生涯打开更多门路。 网络拓展: 比赛是结识同行业人才的好机会,可以帮助学生建立行业联系,这对于未来的职业发展非常重要。 奖金与荣誉: 许多比赛提供奖金或奖品,这不仅能给予学生经济上的奖励,还能增强其成就感和自信心。 荣誉证或奖状可以证明学生的成就,对个人品牌建设有积极作用。 创新与研究: 参加比赛可以激发学生的创新思维,推动科研项目的开展,有时甚至能促成学术论文的发表。 个人成长: 在准备和参加比赛的过程中,学生将面临压力与挑战,这有助于培养良好的心理素质和抗压能力。 自我挑战和克服困难的经历对个人成长有着深远的影响。 综上所述,参加计算机领域的比赛对于学生来说是一个全面发展的平台,不仅可以提升专业技能,还能增强团队协作、沟通、解决问题的能力,并为未来的职业生涯奠定坚实的基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值