一、线段树
简单说就是用二叉树来构造,树的每一个结点代表一条线段[L,R],用于区间处理。
如果L=R,说明这个结点只有一个点,是叶子结点;如果L<R,说明不止一个点,左儿子[L,M],右儿子[M+1,R],其中M=(L+R)/2。
线段树中,如果一个节点的编号为x,那么左儿子的编号为2x,右儿子的编号为2x+1。
N个元素的线段树的高度为logN+1。
二、建树
普通二叉树建线段树
const int Max=10000;
struct {
int l, r, len;//len储存这个区间下数字的个数
} tree[4*Max];//线段树空间需要
void BuildTree(int left, int right, int u)//建树
{
tree[u].l=left;
tree[u].r=right;
tree[u].len=right-left+1;//更新结点u的值
if(left==right)
return;
BuildTree(left,(left+right)>>1,u<<1);//递归左子树
BuildTree(((left+right)>>1)+1,right,(u<<1)+1);//递归右子树
}
完全二叉树建立线段树
void BuildTree(int n,int last_left)//用完全二叉树建一个线段树
{
int i;
for(i=last_left; i<last_left+n; i++)
tree[i]=1;//给二叉树的最后一行赋值,左边n个结点是n头牛
while(last_left!=1)//从二叉树的最后一行倒推到根结点,根结点的值是总数
{
for(i=last_left/2; i<last_left; i++)
tree[i]=tree[i*2]+tree[i*2+1];
last_left=last_left/2;
}
}
建树模板
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1//这里或运算即rt*2+1
void build(int l,int r,int rt)//满二叉树建树
{
if(l==r)
{
scanf("%lld",&tree[rt]);
return;
}
int mid=(l+r)>>1;
build(lson);
build(rson);
tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}
关于以上结构体数组的范围要乘4,因为线段树空间需要。
三、单点修改
模板:d为更新值,index为更新点,lr为更新范围
void update(int d,int index,int l,int r,int node){
if(l == r) {
tree[node] += d; // 更新方式,可以变化
return;
}
int mid = (l+r) / 2;
// push_down(node,mid-l+1,r-mid); 若既有点更新又有区间更新,需要这句话
if(index <= mid){
update(d,index,l,mid,node*2);
}else{
update(d,index,mid+1,r,node*2+1);
}
tree[node] = tree[node*2] + tree[node*2 + 1]; // 向上更新
}
依然是学习书上例题Lost Cows
题意:编号1~n的数字乱序排列,每个位置的数字知道前面比它小的数字个数,求乱序数列。
例子:
由图可知,需要在剩下的编号里找到第pre[n]+1大的数,即为ans[n]
•普通二叉树建立线段树
线段树求解本题,每个结点表示区间内数字的个数,向左找的过程,实际上是缩小找第pre[n]+1大的数的范围,向右找是找去掉左边的个数m后也就是找第pre[n]+1-m大的数,所以每向右找一次区间个数都要变化,进一步找到ans[n]。
代码实现步骤:建树->查询+更新(分两种情况,找左或找右,递归)->得序列
#include<iostream>
#include<cstdio>
using namespace std;
const int Max=10000;
struct {
int l, r, len;//len储存这个区间下数字的个数,即这个结点下牛的数量
} tree[4*Max];//线段树空间需要
int pre[Max], ans[Max];
void BuildTree(int left, int right, int u)//建树
{
tree[u].l=left;
tree[u].r=right;
tree[u].len=right-left+1;//更新结点u的值
if(left==right)
return;
BuildTree(left,(left+right)>>1,u<<1);//递归左子树
BuildTree(((left+right)>>1)+1,right,(u<<1)+1);//递归右子树
}
int query(int u,int num)//查询+维护,所求值为当前区间中左起第num个元素
{
tree[u].len--;//对访问到的区间维护len,即把这个结点上牛的数量减1
if(tree[u].l==tree[u].r)
return tree[u].l;
//情况1:左子区间内牛的个数不够,则查询右子区间中左起第nun-tree(u << 1]. len个元素
if(tree[u<<1].len < num)
return query( (u<<1) +1, num- tree[u<< 1]. len);
//情况2:左子区间内牛的个数足够,依旧查询左子区间中左起第num个元素
if (tree[u<<1].len >= num)
return query(u << 1, num);
}
int main()
{
int n, i;
scanf("%d", &n);
pre[1] =0;
for(i =2; i<=n; i++)
scanf("%d", &pre[i]);
BuildTree(1, n, 1);
for(i= n; i>=1; i--)
ans[i] = query(1, pre[i]+1);
//从后往前推断出每次最后一个数字
for(i =1; i<=n; i++)
printf("%d\n", ans[i]);
return 0;
}
完全二叉树建立线段树
这种方式更好理解,每次只需要数最后一行第几大数就可以,图解如下:
#include<bits/stdc++.h>
using namespace std;
const int Max=10000;
int pre[Max]= {
0},tree[4*Max]= {
0},ans[Max]= {
0};
void BuildTree(int n,int last_left)//用完全二叉树建一个线段树
{
int i;
for(i=last_left; i<last_left+n; i++)
tree[i]=1;//给二叉树的最后一行赋值,左边n个结点是n头牛
while(last_left!=1)//从二叉树的最后一行倒推到根结点,根结点的值是牛的总数
{
for(i=last_left/2; i<last_left; i++)
tree[i]=tree[i*2]+tree[i*2+1];
last_left=last_left/2;
}
}
int query(int u,int num,int last_left)
{
//u表示结点,num表示剩余序列里第pre[i]+1大的数,用来找ans[i],last_left表示最后一行左边第一个结点的标号
//查询+维护,关键的一点是所求值为当前区间中左起第num个元素m
tree[u]--;//对访问到的区间维护剩下的牛的个数m
if(tree[u]==0&&u>=last_left)
return u;
//情况1:找右
if(tree[u<<1]<num)
return query((u<<1)+1,num-tree[u<<1],last_left);
//情况2:找左
if(tree[u<<1]>=num)
return query(u<<1,num,last_left);
}
int main()
{
int n,last_left;//last_left表示最后一行左边第一个结点的标号
scanf("%d",&n);
pre[1]=0;
last_left=1<<(int(log(n)/log(2))+1);//二叉树最后一行的最左边一个数的计算方法是找离n最近的2的指数,例如3->4, 4->4, 5->8
for(int i=2; i<=n; i++)
scanf("%d",&pre[i]);
BuildTree(n,last_left);//n=5个数,last_left=8
for(int i=n; i>=1; i--)//从后往前推断出每次最后一个数字
ans[i]=query(1,pre[i]+1,last_left)-last_left+1;//每次都从根结点开始查起
for(int i=1; i<=n; i++)
printf("%d\n", ans[i]);
return 0;
}
学以致用:Mayor’s posters:线段树+离散化
题意:在长 10000000 的墙上贴海报,告诉你海报数,每张海报的要贴的区间,按照给出的顺序贴,求最后有几张海报能露出来。
思路:•离散化:测试样例范围过大,先压缩区间
未完…