小白初识线段树(线段树原理+专题练习)

来源:互联网 发布:m2m 数据采集 编辑:程序博客网 时间:2024/06/17 05:59
希望再次遇到线段树的题目时可以AC!
线段树其实就是个二叉树。我们一般用数组来进行模拟(如果用结构体+指针也是可以的)。注意,这个二叉树不一定是个完全二叉树。


问题1.如果当前有个数组是的下表是0-(n-1),即有n个元素,那么需要多那么大数组来存放线段树呢。
其实,他的实际有效的空间是2*n-1个,但是我们如果用数组来进行模拟,父亲节点的两个儿子节点我们是如何表示的?
我们是father*2+1,father*2+2来表示的吧?这样,虽然你确确实实是用了2*n-1个空间,但是这些空间反应在数组内是存在间隙的。
我查阅了一下网上的资料,发现,如果数组中有n个元素,那么用来模拟线段树的数组开到4*n是足够的.


问题2.如何建立一个线段树(节点表示区间最小值)。
大体思路:就如同二分法,不断地对折区间,极限状态时区间的左边等于右边,这个时候赋值。代码如下
const int M=1000;int save[M];int tree[4*M];void creat_tree(int l,int r,int now){    if(l==r)//递归结束点    {        tree[now]=save[l];//赋值        return ;    }    else    {        int mid=(r+l)/2;        creat_tree(l,mid,now*2+1);//建立左儿子        creat_tree(mid+1,r,now*2+2);//建立右儿子        tree[now]=min(tree[now*2+1],tree[now*2+2]);//回溯的时候把最小值求出来    }}
问题3.如何查询?
大体思路:假设我们的查询区间是ql,qr,当前区间是l,r
只有当前区间是查询区间的子集的时候,我们才能返回当前的值。如果这两个区间没有交集,那么对于最小值来说就直接返回正无穷(为了不干扰其他值的向上的传递)。
若有交集,那么就继续查找儿子节点。代码如下。
int query(int l,int r,int now,int ql,int qr){    if(ql<=l&&qr>=r)//当前区间是查询区间的子集    {        return tree[now];    }    if(ql>r||qr<l)//完全不相交    {        return 0x3f3f3f3f;//INF    }    int mid=(l+r)/2;    return min(query(l,mid,now*2+1,ql,qr),query(mid+1,r,now*2+2,ql,qr));//返回查找到的最小值}

问题4。单点更新?
大体思路:idx为我们要更新元素的下表,newval是我们要更新为什么值。二分二分二分............之后分到一个点之后就是了
代码:
void update(int l,int r,int now,int idx,int newval)//把下表为ind的元素更新为newval{    if(l==r)//逼近到一个点之后开始更新    {        tree[now]=newval;        return ;    }    int mid=(l+r)/2;    if(mid>idx)//又儿子更新,左儿子更新    {        update(mid+1,r,now*2+2,idx,newval);    }    else    {        update(l,mid,now*2+1,idx,newval);    }    tree[now]=min(tree[now*2+1],tree[now*2+2]);}
问题5:我想一次更新一个区间的值怎么办?
最精华的部分-------延迟标记!其实按照我的理解就是什么时候用,什么时候更新,用不着根本不用跟新!
这个意思就跟并查集的合并节点是一样的。我们令pre[a]=b;就真让以a为头节点的集合全都并到b上去了吗?当然不是,只是头节点a并上去了而已。
这时候你再去输出一下以a为头结点的元素的头结点试试?还是a,并没有因为a并过去了而a麾下所有的节点都并过去了。
但是当你需要找到以a为头结点元素的头结点的时候又能更新到b上去了,这个其实就是延迟标记的原理。

想一下,确实是延迟,但是不等于没做,只是在最需要的时候做了而已,不需要的时候不要有多余的动作。


怎么去实现?
这时候我们引进一个标记元素,用来标记当前的操作。然后我们还需要写一个函数,用来传递关系。这里看不懂没关系,只要红字能看懂了代码也能看懂,稍加思考就可以学会。学会之后就会做题了。
引入标记数组key[],操作区间cl,cr,操作(增加)值val。找区间的时候就跟区间查询一样一样的。找到了之后直接更新就行,但是要在这个点进行标记,说明这个点还是要
向下更新的,只不过现在不需要!

const int M=1000;int save[M];int tree[4*M];int key[4*M];void pushdown(int now){    if(now)    {        key[now*2+1]+=key[now];        key[now*2+1]+=key[now];//向下传递        tree[now*2+1]+=key[now];//向下赋值        tree[now*2+2]+=key[now];        key[now]=0;//传递结束后自己的使命完成,恢复为0    }}void creat_tree(int l,int r,int now){    key[now]=0;//创建的时候就清空他    if(l==r)//递归结束点    {        tree[now]=save[l];//赋值        return ;    }    else    {        int mid=(r+l)/2;        creat_tree(l,mid,now*2+1);//建立左儿子        creat_tree(mid+1,r,now*2+2);//建立右儿子        tree[now]=min(tree[now*2+1],tree[now*2+2]);//回溯的时候把最小值求出来    }}int query(int l,int r,int now,int ql,int qr){    if(ql<=l&&qr>=r)//当前区间是查询区间的子集    {        return tree[now];    }    if(ql>r&&qr<l)//完全不相交    {        return 0x3f3f3f3f;//INF    }    pushdown(now);    int mid=(l+r)/2;    return min(query(l,mid,now*2+1,ql,qr),query(mid+1,r,now*1+2,ql,qr));//返回查找到的最小值}void update(int l,int r,int now,int idx,int newval)//把下表为ind的元素更新为newval{    if(l==r)//逼近到一个点之后开始更新    {        tree[now]=newval;        return ;    }    int mid=(l+r)/2;    if(mid>idx)//又儿子更新,左儿子更新    {        update(mid+1,r,now*2+2,idx,newval);    }    else    {        update(l,mid,now*2+1,idx,newval);    }    tree[now]=min(tree[now*2+1],tree[now*2+2]);}void Xupdate(int l,int r,int now,int cl,int cr,int cval){    if(cl<=l&&cr>=r)    {        tree[now]+=cval;//更新该区间节点的值        key[now]+=cval;//延迟标记的累计,因为可能连续的执行好几次这样的标记,所以要累加起来        return ;    }    if(cl>r||cr<l)    {        return ;    }    pushdown(now);//顺风车,不搭白不搭?错了错了!不是这样!!!是必须要!因为你必须保证每次回溯的值都是对的,如果没有这一步,那么回溯的值很可能就是个错的    int mid=(r+l);    Xupdate(l,mid,now*2+1,cl,cr,cval);    Xupdate(mid+1,r,now*2+2,cl,cr,cval);    tree[now]=min(tree[now*2+1],tree[now*2+2]);}
由于小白,会有一些错误,望指正,之后会更新kuangbin的线段树专题QQQQQQAQQQQQQQ




原创粉丝点击