重要算法线段树入门
部分图片是从网络获得,侵删!
熊猫人线段树什么是线段树
入门算法的同学一定对数据结构的线段树不陌生,线段树也是算法比赛常考且非常重要的一种高效解决区间的方法。线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 下面我会对线段树的基本方法展开详细的介绍。线段树的概念
线段树是一种二叉搜索树,什么叫做二叉搜索树,首先满足二叉树,每个结点度小于等于二,即每个结点最多有两颗子树,何为搜索,我们要知道,线段树的每个结点都存储了一个区间,也可以理解成一个线段,而搜索,就是在这些线段上进行搜索操作得到你想要的答案。
建立一个线段树的时间复杂度是O(N)(不是O(nlogn)!!)使用线段树可以快速地查找某一个节点在若干条线段中出现的次数,其查找的时间复杂度为O(logN)。而未优化的空间复杂度为O(2N),因此有时需要离散化让空间压缩。
线段树采用分治思想(主要是二分法, 一般可以高效率地解决区间问题。线段树的叶子节点是最简单的节点,叶子节点的父节点是两个叶子节点的区间的并集。对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的节点数目为N,即整个线段区间的长度。区间[1-5]的线段树如下图所示。
1到5区间的线段树线段树使用场景
上文已经提到线段树主要是可以高效地解决区间问题,那么具体过程是怎么样的呢?
大家可以先看看这个经典选段树的题,后续我会把题解发到推文上 。
链接:http://acm.hdu.edu.cn/showproblem.php?pid=1166(无法在内容中引用外文网址 链接我会放在下面)
我们打开后我们会发现,这个题目是典型的区间处理题目,但是数据范围极大而且还要处理各种操作,直接用数组存储并遍历操作会直接超时。这时线段树(快且方便)就派上用场了。线段树主要处理极大范围的区间操作。线段树主要代码操作及其讲解线段树的定义
父节点val值是所有子节点值的和,这样有利于下面的操作,叶子节点的值就是区间内输入的值。struct Nd //定义线段树节点 { int left;//左孩子 int right;//右孩子 int val;//该节点值 };
2.建树
利用递归建立树是常用操作,当r==l时表明到达叶子节点。最后arr[i].val = arr[i<<1].val+arr[i<<1|1].val;是递归回溯时完成子节点对父节点的赋值,因此父节点val值是所有子节点值的和。x<<1与x<<1|1相当于x*=2和x=x*2+1,分别寻找两个子节点。详细如下图所示。
父节点对应子节点void build(int l,int r,int i)//利用递归构成线段树 { arr[i].left=l;//将左右区间的值赋值给节点 arr[i].right=r; if(r==l) { std::cin >> arr[i].val;//最后叶子节点才存储要存储的值 return ; } build(l,(l+r)/2,i<<1);//递归到左节点 build((r+l)/2+1,r,i<<1|1); //递归到右节点 arr[i].val = arr[i<<1].val+arr[i<<1|1].val;//递归回溯时完成子节点对父节点的赋值 }
arr[i].val = arr[i<<1].val+arr[i<<1|1].val;完成时的值是下面所有节点(有亲缘关系)的和,递归是从深到浅,从左到右(左右可变)遍历的过程(DFS就是如此)。
3.DFS打印树
这个是方便看树每一个节点以及是否健全,找bug用~void dfs_print(int l,int r,int i) { /*if(r==l) { std::cout << arr[i].val << " ";//打印叶子节点 return ; }*/ std::cout << arr[i].val << " "; if(r==l) { return ; } dfs_print(l,(l+r)/2,i<<1); dfs_print((r+l)/2+1,r,i<<1|1); }
4.更新节点
arr[d].left==arr[d].right时就是叶子,别忘了对父节点回溯时的刷新值。递归找叶节点后更改,void update(int d,int r,int b)// d为当前节点 r为更新的节点 b为改变节点的值 这个递归算法实际就是二分查找 { if(arr[d].left==arr[d].right)//递归到叶子节点时 肯定下标是r 注意只能更改叶子节点 { arr[d].val=b;//更改值 return; } int mid = (arr[d].left+arr[d].right)>>1; if(r>mid)update(d<<1|1,r,b);//递归到右son else update(d<<1,r,b);//递归到左son arr[d].val = arr[d<<1].val+arr[d<<1|1].val;//递归回溯时完成子节点对父节点的赋值 }
5.区间查询(返回区间内所有节点和)
将范围一步一步划分小,充分利用父节点的值增加效率。//区间查询 我们不要忘记,父节点可是存储着它俩儿子值的和 int query(int d,int l,int r)//这个是返回区间的和 { int sum = 0;//区间总和 if(l==arr[d].left&&r==arr[d].right)//1.如果在目的范围内的父亲节点 可以增加效率 sum += arr[d].val; else{ int mid = ((arr[d].left+arr[d].right)/2); if(l<=mid) (r>mid)?sum+=query(d<<1,l,mid):sum+=query(d<<1,l,r); if(r>mid) (l<=mid)?sum+=query(d<<1|1,mid+1,r):sum+=query(d<<1|1,l,r); } return sum; }
父节点存储所有子节点的总值,所以我们只需查找范围内的父节点即可。可能您不明白9到12行代码的意义以及如何利用父节点查找:请看下面的图片
分割操作
虽然图片只有两种情况,别忘了有左右两个子节点,所有必须有四个if判断(代码中部分用三目式代替)。当l==arr[d].left&&r==arr[d].right(分割的目的范围与节点范围相同时返回值)
6.单节点查询
其实直接调用区间查询即可,两个范围一样就是单节点,这样效率不会影响,还是O(logn)//单点查询 直接调用区间查询即可 效率不受影响 int find(int d,int o)//d 为当前节点 o为查询节点 { return query(d,o,o); }
7.区间修改
8-12行代码原理如上图所示。将区间范围一步一步划分为节点操作,这样只需要一个递归就可以解决,具有很高的效率,别忘了父节点的值。void changeSegment(int d,int l,int r,int x)//l和r是修改的范围 x是范围内每一个节点改变的值 { if(arr[d].left==l&&arr[d].right==r&&arr[d].left==arr[d].right){//一定要修改叶子节点 不要误修改父节点 arr[d].val+=x; return ; }else{ int mid = ((arr[d].left+arr[d].right)/2); if(l<=mid) (r>mid)?changeSegment(d<<1,l,mid,x):changeSegment(d<<1,l,r,x); if(r>mid) (l<=mid)?changeSegment(d<<1|1,mid+1,r,x):changeSegment(d<<1|1,l,r,x); arr[d].val = arr[d<<1].val+arr[d<<1|1].val;//回溯 改变父节点的值 } }测试总代码#include struct Nd //定义线段树节点 { int left;//左孩子 int right;//右孩子 int val;//该节点值 }; Nd arr[1 << 10]; //随便设有1024个节点 //初始化 void build(int l,int r,int i)//利用递归构成线段树 { arr[i].left=l;//将左右区间的值赋值给节点 arr[i].right=r; if(r==l) { std::cin >> arr[i].val; return ; } build(l,(l+r)/2,i<<1);//递归到左节点 build((r+l)/2+1,r,i<<1|1); //递归到右节点 arr[i].val = arr[i<<1].val+arr[i<<1|1].val;//递归回溯时完成子节点对父节点的赋值 } //打印节点 void dfs_print(int l,int r,int i) { /*if(r==l) { std::cout << arr[i].val << " ";//打印叶子节点 return ; }*/ std::cout << arr[i].val << " "; if(r==l) { return ; } dfs_print(l,(l+r)/2,i<<1); dfs_print((r+l)/2+1,r,i<<1|1); } //更新节点 void update(int d,int r,int b)// d为当前节点 r为更新的节点 b为改变节点的值 这个递归算法实际就是二分查找 { if(arr[d].left==arr[d].right)//递归到叶子节点时 肯定下标是r 注意只能更改叶子节点 { arr[d].val=b;//更改值 return; } int mid = (arr[d].left+arr[d].right)>>1; if(r>mid)update(d<<1|1,r,b);//递归到右son else update(d<<1,r,b);//递归到左son arr[d].val = arr[d<<1].val+arr[d<<1|1].val;//递归回溯时完成子节点对父节点的赋值 } //区间查询 我们不要忘记,父节点可是存储着它俩儿子值的和 int query(int d,int l,int r)//这个是返回区间的和 { int sum = 0; if(l==arr[d].left&&r==arr[d].right)//1.如果在目的范围内 sum += arr[d].val; else{ int mid = ((arr[d].left+arr[d].right)/2); if(l<=mid) (r>mid)?sum+=query(d<<1,l,mid):sum+=query(d<<1,l,r); if(r>mid) (l<=mid)?sum+=query(d<<1|1,mid+1,r):sum+=query(d<<1|1,l,r); } return sum; } //单点查询 直接调用区间查询即可 效率不受影响 int find(int d,int o)//d 为当前节点 o为查询节点 { return query(d,o,o); } void changeSegment(int d,int l,int r,int x) { if(arr[d].left==l&&arr[d].right==r&&arr[d].left==arr[d].right){ arr[d].val+=x; return ; }else{ int mid = ((arr[d].left+arr[d].right)/2); if(l<=mid) (r>mid)?changeSegment(d<<1,l,mid,x):changeSegment(d<<1,l,r,x); if(r>mid) (l<=mid)?changeSegment(d<<1|1,mid+1,r,x):changeSegment(d<<1|1,l,r,x); arr[d].val = arr[d<<1].val+arr[d<<1|1].val; } } int main() { using namespace std; build(1,5,1); dfs_print(1,5,1); int n,a,x; cin >> n >> a >> x; /*update(1,n,a); dfs_print(1,5,1); cout << endl; cin >> n >> a; cout << "sum=" << query(1,n,a)<> n; cout << find(1,n);*/ changeSegment(1,n,a,x); dfs_print(1,5,1); return 0; } 感谢
感谢观看,希望大家能够尽情的指点,如果有什么错误的地方可以指出来。
创作不易,如果大家感觉有帮助请点一下赞,谢谢。
下回会更新它的一道例题题解,链接如下所示。
终结行业暴利!小米笔记本Pro花最少的钱,享受最顶级的配置一款好的工具会让你在工作的时候更加得心应手,工作效率翻倍。大家应该或多或少都对OLED的屏幕有所了解,无奈的屏幕在全黑的时候更加省电,而且显示色彩会更加的鲜艳,比LED屏幕更加的轻
捐赠清华大学977680元!这次雷军又火了一把小米最近可以说是又火了一把,对清华大学的捐赠直接刷爆了朋友圈引发网友们怒赞。而我认为小米这一次的捐赠真的值得让我们点赞,首先与清华大学的捐赠让我们看到了小米校友们的感恩。在母校11
格力看了也直呼内行!小米新风空调24小时只要0。3度电?小米这场新品发布会发布的这款新风空调,真的给了我很大的惊喜,首先第1点就是智能化作为小米智能生态链中的智能空调,这一次米家新风库的尊享版可以通过米家实现全屋互联,可以和更多的小米智
小米生生不息百城同跑一万个米粉一起干了件大事这次米粉节厉害了!除了举行生生不息百城同跑活动,有超过一万名米粉参与之外,还取得了全渠道总支付金额46。1亿的成绩,这是比去年翻了一倍的成绩,表现十分惊艳了。而且第5000家小米之
奥睿科户外储能电源五一自驾旅游必备用电伴侣如果有人问我什么时候开始有了想给自己置办一个户外电源的想法?起因就是我前段时间无聊在网上刷视频,刷到很多的自驾旅行或者改造房车的一些视频博主的视频,他们出行途中需要解决的最大问题就
飞行模式除了飞机上使用,还有什么用处?手机有一个功能叫飞行模式,飞行模式其实最初目的是为了让用户在乘坐飞机途中,让手机不发射不接收信号,不干扰飞机上的仪器。但飞行模式其实还有这几个实用的地方用处防广告骚扰和诈骗骚扰我们
手机屏幕脏了千万别用衣服擦!教你一招让屏幕恢复如新,太厉害了手机屏幕脏了你会怎么办?多数人随手拿到什么就擦,比如衣服纸巾。有些人就不大一样你们千万别跟着跟着学你手机上已经有100万个细菌,并且每天都在接触你的皮肤。手机细菌会给我们带来哪些隐
手机越用越卡?打开这些功能,再用几年也不卡很多朋友抱怨手机越用越慢,经常卡,有什么办法能解决呢?今天小宁君就教大家6个方法,让你的手机可以恢复以往的流畅PART1清理APP缓存清理缓存,打开vivo手机i管家即可轻松清理,
手机剩1的电,能用多久?你知道吗?有的手机最后1的电量最耐用,每次都够看完一个十分钟的视频。而有的手机,每次电池报警还有10的电量,你以为还能再撑一会儿,去找个充电器的时间手机就关机了其实,手机电池的剩余电量很难估
秋天的第一杯奶茶刷屏了!什么梗?昨天,朋友圈被这样一句话刷屏了秋天的第一杯奶茶跟不上网速的还不止小编一个什么奶茶?要52块钱???昨天下午秋天的第一杯奶茶甚至冲上了微博热搜!所以,这到底是什么意思呢?有人说是因为
你在放假,骗子却在加班,警惕这几种诈骗十一中秋双节庆吃吃喝喝玩玩闹闹的时间到伙伴们是不是分外逍遥呢?但是!骗子们可没有放假说不定正在暗处想方设法把你的钱骗走这份防骗攻略,请收好!骗术一ETC卡异常诈骗十一中秋双节假期,