分苹果(线段树)
原题连接
https://www.dotcpp.com/oj/problem1501.html
这一题用线段树可以ac,我第一次做的时候也没往这想直接带着键盘就敲,不出所料直接时间超限(狗头)。
然后细细想来感觉不简单,不愧是算法提高题。这就要拿出线段树来镇场子。
原题:
小朋友排成一排,老师给他们分苹果。
小朋友从左到右标号1…N。有M个老师,每次第i个老师会给第Li个到第Ri个,一共Ri-Li+1个小朋友每人发Ci个苹果。
最后老师想知道每个5小朋友有多少苹果。
输入
5 3
1 2 1
2 3 2
2 5 3
输出
1 6 5 3 3
先来说说线段树的基本框架:
void function(node,left,right,...(其他参数))//node是当前结点索引,left是当前节点代表区间的左区间,right是当前节点代表区间的右区间
{
对树进行的操作
mid=(left+right)/2;//中间节点
function(left,mid);//左子树递归
function(mid+1,right);//右子树递归
return;
}
让我们看一看ac代码,你会发现所涉及到关于线段树的操作都是基于这个框架。
猛一看下面的代码加上注释密密麻麻,不要害怕,都是一步一步走过来的。当初我做题找题解时随便点进去一个题解,一看密密麻麻的都是直接退出,然后下一个。。到头来还是老老实实一点一点啃。
#include <iostream>
using namespace std;
int tree[400004];//一般使用线段树都是至少开辟4倍空间
//建树
void buildtree(int node,int left,int right){
if(left==right){//对叶子节点赋值:1==1,2==2,3==3,4==4,5==5.
tree[node]=0;
return;
}
int mid=(left+right)>>1;//mid是[left,right]的中间节点,>>1代表除于2,相比较/2更快。<<1代表乘2
buildtree(node<<1,left,mid);//建左子树
buildtree(node<<1|1,mid+1,right);//建右子树,node<<1|1:代表node*2+1;
tree[node]=0;//非叶子节点赋值
}
//按照给的区间加苹果。
void addapple(int node,int left,int right,int l,int r,int count){
//列举所给区间[l,r]和当前节点所代表区间[left,right]的关系
if(l<=left&&right<=r){//关系1:当前节点所带代表的区间小区所给区间,直接将苹果加到这个节点不需要再往下找具体的叶子节点(后面会有图解)
tree[node]+=count;
return;
}
if(left>r||right<l){//关系2:所给节点与当前节点所代表的区间不相交,不用再进行操作,直接返回。
return ;
}
if(right==left){//加到叶子节点上
tree[node]+=count;
return;
}
int mid=(left+right)>>1;
if(mid>=l)addapple(node<<1,left,mid,l,r,count);//同样递归左右子树
if(mid<r)addapple(node<<1|1,mid+1,right,l,r,count);
}
//打印结果
void printtree(int node,int left,int right){
if(left==right){//打印每个学生手中的苹果
cout<<tree[node]<<" ";
return;
}
int mid=(left+right)>>1;
tree[node<<1]+=tree[node];//当前节点的值加到他的左孩子上。
tree[node<<1|1]+=tree[node];//当前节点的值加到他的右孩子上。
printtree(nextnode,left,mid);//左右子树递归
printtree(nextnode+1,mid+1,right);
}
int main(){
int n,m;
int li,ri,ci;
cin>>n>>m;//n学生,m老师;
buildtree(1,1,n);//建树
for(int i=1;i<=m;i++){
cin>>li>>ri>>ci;//[li,ri]老师发苹果的区间,ci老师分发苹果的数量
addapple(1,1,n,li,ri,ci);
}
printtree(1,1,n);
}
每个函数的图示
1.建树
建完树后
(本来理想是弄个动态图更加直观,奈何本人不会啊。。)
叶子节点:代表学生,叶子节点的大框中红色的数字代表的就是学生的编号。下面小方框中的数字代表的是每个学生手中的苹果。
建完树后每个学生手中的苹果数量都是0
建树中体现出来的框架
void buildtree(int node,int left,int right){
if(left==right){//这是对线段树的操作,这个函数的功能是对节点赋值所以是赋值操作,这是对叶子节点进行赋值
tree[node]=0;
return;
}
int mid=(left+right)>>1;//这是对线段树左右子树进行递归
buildtree(node<<1,left,mid);
buildtree(node<<1|1,mid+1,right);
tree[node]=0;//这是对非叶子节点赋值
//这个赋值想不明白的。可以自己举个特殊的例子比如就三个节点的树:
1~2
/ \
1 2
}
2.给学生加苹果
输入
5 3
1 2 1
2 3 2
2 5 3
执行完1 2 1后的状态
执行完2 3 2后的状态
执行完2 5 3后的状态
图中蓝色就是操作后的变化;初学线段树的朋友可以想一想为什么这3个苹果直接加到2号和3号同学,而4号和5号同学却加到他们的父亲节点上。
框架的体现
void addapple(int node,int left,int right,int l,int r,int count){
//列举所给区间[l,r]和当前节点所代表区间[left,right]的关系
//这三个if就是对线段树进行的操作,
//这个函数的功能是对所给区间[l,r]进行加count个苹果的操作。
//所以你看这三个if里面的语句都是对节点进行相加的操作
if(l<=left&&right<=r){//关系1:加到父亲节点上
tree[node]+=count;
return;
}
if(left>r||right<l){//关系2:所给节点与当前节点所代表的区间不相交,不用再进行操作,直接返回。
return ;
}
if(right==left){//加到叶子节点上
tree[node]+=count;
return;
}
//对线段树左右子树进行递归
int mid=(left+right)>>1;
if(mid>=l)addapple(node<<1,left,mid,l,r,count);//同样递归左右子树
if(mid<r)addapple(node<<1|1,mid+1,right,l,r,count);//node<<1|1:代表node*2+1;
}
3.打印结果
打印第一个学生
打印第二个学生
以此类推全部打印。
框架的体现
void printtree(int node,int left,int right){
if(left==right){//这个函数对线段树的操作是打印,故打印每个学生手中的苹果
cout<<tree[node]<<" ";
return;
}
int mid=(left+right)>>1;
tree[node<<1]+=tree[node];//当前节点的值加到他的左孩子上。
tree[node<<1|1]+=tree[node];//当前节点的值加到他的右孩子上。
//对线段树左右子树进行递归
printtree(nextnode,left,mid);
printtree(nextnode+1,mid+1,right);
}
总结一下线段树的框架函数就是:
- 相关操作:这个需要根据自己的需求,就比如这一题的需求是:建树,给学生添加苹果,打印节点。
- 左右子树的递归
注:学习算法本来就不是一蹴而就的事情,刚开始看别人的代码都是头大无从下手。建议多看几遍,多想想。共勉!
对本文有什么疑问或建议直接评论或者私信即可。