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

希望再次遇到线段树的题目时可以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




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值