线段树-基础

线段树是一种高级数据结构,可以用于解决动态的区间最值问题和区间求和问题。这一种算法比纯粹的模拟算法要快。例如每次修改单个数,求一个区间的最优值。用模拟算法每一次修改单个数要O(1)的时间,查询要O(N)的时间,总共修改M个数,时间复杂度为O(NM),而线段树每一次修改需要O(logN)的时间,修改M次,总时间为O(MlogN),整体会比模拟算法快得多。
二叉树通常是一棵完全二叉树,占2^N的空间,如果不是刚好2^N,就会稍微浪费一点空间。根据名字,可以知道,它储存的是一段数据,如下图表示。
  线段树 - 赵子睿 - 赵子睿的博客
其中的(1,8)表示的是从第一个值到第八个值的区间,这一段就由(1,8)来保存,下面的以此类推。最下层只保存单个数字,但也表示了一个只有一个值的区间。
这样的树不断将区间二分,每一个节点都可以直接管理自己的左右儿子,左儿子管理这个区间的前半部分,右儿子管理这个区间的后半部分。
这样的一颗线段树可以近似地看做是一棵满二叉树。同样,它的深度不会超过 线段树-基础 - 赵子睿 - 赵子睿的博客
管理每一个节点,可以用一个结构体数组来保存,每一个节 点都记录自己的左端点和右端点,左儿子和右儿子,以及该区间的最优值。
创建一个节点,可以用一个指针指向这一个数组的空的节点,然后把要替代的值都放进去。
要建立一颗二叉树,可以运用递归的过程实现,首先建立根节点,检查左端点和右端点,发现左端点在右端点的前面,证明这一个区间可以一分为二,再建立它的左子树和右子树,直到发现左端点和右端点相同,只剩一个值,就不用在二分了。
具体程序如下:

int getPoint(int l, int r, int max)
{
fp++;//新建一个空节点
f[fp].l = l;
f[fp].r = r;
f[fp].max = max;
return fp;//返回该节点的地址
}

int create(int l, int r)
{
int now = getPoint(l,r,-oo);//创建节点,记录节点地址
if (l<r)
{
f[now].lc = create(l, (l+r)/2);//建立左子树
f[now].rc = create((l+r)/2+1, r);//建立右子树
}
return now;
}

那么,有了创造,自然也要有修改。修改分为两种,区间修改和单个节点修改。单个节点修改很简单,从根节点开始递归,往下面扫描,发现要修改的节点,就修改它,并向上更新树的最优值(和),最后回到根节点。

代码如下:

void update(int root , int p, int x)
{
int l = f[root].l;
int r = f[root].r;
if ( p<l || r<p ) return ;//如果要修改的节点不在此区间内,停止递归,避免过多操作

if (l==r) { f[root].max=x; return ; }//如果这个区间刚好是要修改的叶节点,就修改它

update(f[root].lc, p, x);
update(f[root].rc, p, x);

f[root].max = max ( f [ f[root].lc ].max, f [ f[root].rc ].max );//更新当前节点
}

单个节点修改很简单,但是还有区间修改,一次更改整个区间,如果一个一个数进行单个修改,速度会比模拟还要慢,所以,这里就有一种更好的办法,使用lazy-tag的思想,就是给每一个要修改的节点标记一下,记录需要增加的数值,而不需要现在就把这个值加上去。当调用到这一个节点时,发现被标记过,就把增量加进去,并且给子节点也增加需要增加的数。

这里对于部分人来说有一个问题:修改某个区间,最优值就直接加上了增量。事实上,修改这一个区间,区间中每个数都相加同样的值,最大值依然不变,只比原来多了增量,就算增量是负数,也同样是这样。

事实上,更新单个节点也是区间更新中的一个特例,用区间更新同样能够操作,只不过左端点等于右端点。

代码如下:

void push(int root)//将当前节点的增量压入当前节点中
{
if (!f[root].s) return;
if (f[root].lc != 0)//增量传递给左儿子
{
f[f[root].lc].s = true;
f[f[root].lc].delta += f[root].delta;
}
if (f[root].rc != 0)//增量传递给右儿子
{
f[f[root].rc].s = true;
f[f[root].rc].delta += f[root].delta;
}
if (f[root].rc == 0 && f[root].lc == 0)
{
f[root].max += f[root].delta;
}
f[root].s = false;
f[root].delta = 0;//增加完当前节点,增量清空
}

void update(int root, int l, int r, int d)
{
push(root);
if (l==f[root].l && r==f[root].r)//区间刚好匹配
{
f[root].s = true;
f[root].delta += d;
return;//标记增量
}
int mid = (f[root].l+f[root].r) / 2;
if (l <= mid) update(f[root].lc,l,min(mid,r),d);//更新左子树
if (r >= mid+1) update(f[root].rc,max(mid+1,l),r,d);//更新右子树
}

有了创造,有了修改,可是没有查询,这一棵线段树也发挥不了多大作用。查询线段树,用到的一样是很巧妙的递归操作。查询一个区间的最优值,从根节点开始,向下递归,如果是查询的区间被一分为二,就查询左右儿子的区间,返回查询到的答案,合并两个答案,得到最优值。如果在左(右)区间,就直接递归向下找。最后返回的结果就是查询的区间的最优值。

  代码如下:

int query(int root,int l,int r)
{
if (l==f[root].l && r==f[root].r) return f[root].max;//区间完全匹配,返回当前区间最优值

int mid = (f[root].l+f[root].r) / 2;
int ans = -oo;
if (l <= mid) ans = max(ans,query(f[root].lc,l,min(mid,r)));//查找左儿子区间
if (r >= mid+1) ans = max(ans,query(f[root].rc,max(mid+1,l),r));//查找右儿子区间

return ans;//返回最优值
}

注意,如果是区间修改需要在一开始将增量增加到节点中。即push(root);


//以上区间修改为不正确的……尚未修改完毕……
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值