【AC军团周报(第三周)第三篇】线段树从入门到入土【3】

本文章连载AC军团周报

-> 线段树 : 从入门到入土【3】

前言

这期我们就万能的线段树,讲一讲线段树的奇葩用法。

你可能不知道这些线段树用法:线段树维护区间最大子段和、线段树优化Dijkstra(大雾)……

三、线段树部分用法

1.线段树维护区间最大子段和

我们已经学过线段树,再仔细的回想下吧!

…………………………………………日常回想时间………………………………………………

我们应该还记得线段树维护的是区间的一些特性,如果我们不维护区间和了,试试维护区间最大子段和。

我记得最大子段和问题是DP题啊?

对,我们可以在线段树上进行DP。

我相信做过 P1115 最大子段和的同学应该还记得怎么维护最大子段和,但是我们在线段树上维护有点区别:

我们线段树上维护的是区间的最大子段和啊,线段树的节点是代表一个区间的最大子段和啊,我们怎么只通过它的左右儿子节点来维护区间最大子段和?

我们可以维护四个变量:

struct tree{
	int sum;  //区间和
    int lsum; //区间内紧靠左端点的最大子段和
    int rsum; //区间内紧靠右端点的最大子段和
    int msum; //区间内最大子段和
}

我们可以思考下如何更新这几个值:

首先是sum,这个直接把左右儿子的值加起来就可以了

其次是lsum与rsum,这两个值的维护方法是基本相似的。因为左/右端点是固定的,所以,靠近左/右端点的最大子段和就只可能是两种情况:要不就是左/右半部分的靠近左/右端点的最大子段和,要不就是左/右半部分的和加上右/左半部分的靠近左/右端点的最大子段和。

最后是msum,最大子段和所在区间可能完全在左半部分,可能完全在右半部分,也有可能是跨越了左右半边。所以我们取最大的,其中跨越左右半部分的可以贪心的取左半部分靠近右端点的最大子段和,加上右半部分靠近左端点的最大子段和。

如果会了更新,也就基本做完了这道题。

如果我之前的一堆话看不懂的话,可以参考下面的图理解一下。(图片来自网络,若有侵权,请联系作者删除)

更新的代码如下:

inline void update(int rt){
    z[rt].sum = z[rt << 1].sum + z[rt << 1 | 1].sum;
    z[rt].lsum = max(z[rt << 1].lsum, z[rt << 1 | 1].lsum + z[rt << 1].sum);
    z[rt].rsum = max(z[rt << 1 | 1].rsum, z[rt << 1].rsum + z[rt << 1 | 1].sum);
    z[rt].msum = max(max(z[rt << 1].msum, z[rt << 1 | 1].msum), z[rt << 1].rsum + z[rt << 1 | 1].lsum);
}

建树和普通线段树是一致的,想看这部分代码的同学直接翻到最下方代码部分。

在查询时有一些特殊操作。(也不算太特殊)

普通的线段树(区间求和)在询问时会有一个求和更新的过程,我们只要把这个过程改成和update函数类似的方法。

如果看不懂部分类似变量的东西,那可能是我的宏定义,请参考完整代码中的宏定义

inline tree query(int l,int r,int rt,int nowl,int nowr){
    if(nowl<=l && r<=nowr){
        return z[rt];
    }
    int m = (l + r) >> 1;
    if(nowl<=m){
        if(m<nowr){
            tree tl, tr, res;
            tl = query(lson, nowl, nowr);//读取左半部分的信息
            tr = query(rson, nowl, nowr);//读取右半部分的信息
            res.sum = tl.sum + tr.sum;
            res.lsum = max(tl.lsum, tl.sum + tr.lsum);
            res.rsum = max(tr.rsum, tr.sum + tl.rsum);
            res.msum = max(max(tl.msum, tr.msum), tl.rsum + tr.lsum);
            //这部分和之前的update函数十分的像。
            //实际上就是把左半部分与右半部分合并时更新节点信息
            return res;
        }else{//只在左半部分,直接返回左半部分
            return query(lson, nowl, nowr);
        }
    }else{//只在右半部分,直接返回右半部分
        return query(rson, nowl, nowr);
    }
}

主函数十分简单,这里就不讲了。我这里只求了整个链上的最大子段和,实际上可以通过调用query函数求出区间最大子段和。

完整代码:

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
using namespace std;
#define go(i, j, n, k) for (int i = j; i <= n; i += k)
#define fo(i, j, n, k) for (int i = j; i >= n; i -= k)
#define rep(i, x) for (int i = h[x]; i; i = e[i].nxt)
#define mn 200020
#define inf 1 << 30
#define ll long long
#define ld long double
#define fi first
#define se second
#define root 1, n, 1
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
#define bson l, r, rt
inline int read(){
    int f = 1, x = 0;char ch = getchar();
    while (ch > '9' || ch < '0'){if (ch == '-')f = -f;ch = getchar();}
    while (ch >= '0' && ch <= '9'){x = x * 10 + ch - '0';ch = getchar();}
    return x * f;
}
inline void write(int x){
    if (x < 0)putchar('-'),x = -x;
    if (x > 9)write(x / 10);
    putchar(x % 10 + '0');
}
//This is AC head above...
struct tree{
    int sum, lsum, rsum, msum;
} z[mn << 2];
inline void update(int rt){
    z[rt].sum = z[rt << 1].sum + z[rt << 1 | 1].sum;
    z[rt].lsum = max(z[rt << 1].lsum, z[rt << 1 | 1].lsum + z[rt << 1].sum);
    z[rt].rsum = max(z[rt << 1 | 1].rsum, z[rt << 1].rsum + z[rt << 1 | 1].sum);
    z[rt].msum = max(max(z[rt << 1].msum, z[rt << 1 | 1].msum), z[rt << 1].rsum + z[rt << 1 | 1].lsum);
}
inline void build(int l,int r,int rt){
    if(l==r){
        z[rt].lsum = z[rt].msum = z[rt].rsum = z[rt].sum = read();
        return;
    }
    int m = (l + r) >> 1;
    build(lson);
    build(rson);
    update(rt);
}
inline tree query(int l,int r,int rt,int nowl,int nowr){
    if(nowl<=l && r<=nowr){
        return z[rt];
    }
    int m = (l + r) >> 1;
    if(nowl<=m){
        if(m<nowr){
            tree tl, tr, res;
            tl = query(lson, nowl, nowr);
            tr = query(rson, nowl, nowr);
            res.sum = tl.sum + tr.sum;
            res.lsum = max(tl.lsum, tl.sum + tr.lsum);
            res.rsum = max(tr.rsum, tr.sum + tl.rsum);
            res.msum = max(max(tl.msum, tr.msum), tl.rsum + tr.lsum);
            return res;
        }else{
            return query(lson, nowl, nowr);
        }
    }else{
        return query(rson, nowl, nowr);
    }
}
int n;
int main(){
    n = read();
    build(root);
    cout << query(root, 1, n).msum << "
";
    return 0;
}

如果我们加一些操作呢?

区间加+区间最大子段和?

大家可能觉得挺简单的QwQ,实际上:

放个类似的题目:P4680 [Ynoi2018]末日时在做什么?有没有空?可以来拯救吗

题目难度NOI/NOI+/CTSC,,,

我可以透露一下为什么这么难,,,

如果给区间加一个值的话,会使得lsum、rsum、当前区间答案(msum)都发生变化,有一些负数可能会变正,答案区间可能会变长,同样的如果这个值是负数,就可能变短,就不能用之前的方法维护了

如果您是dalao的话可以试一试这道题,提示:

用分块,然后用线段树维护每个块,然后为了解决上面的问题,不难想到半平面交,,,

这啥东西,看不懂

我们课后留了一些推荐题目供您水题练习,有时间练一下还是不错的。

2.线段树维护Dijkstra

图论?

走错了吧?

我们重新回想下Dijkstra我们原来是怎么优化的:

…………………………………………回想时间……………………………………

我们是不是拿堆优化的?

如何优化?

我们本来是把一个寻找dis[]中最小的一个,并用这个值来更新其他的dis[]值,我们优化是把找最小的这个步骤用堆来实现。

也就是说,本来我们本来想用线段树优化Dijkstra的问题变成了如何用线段树办掉堆能办的事。

怎么办?

我们在这里是求的区间最小值,明显可以用线段树。但是最令人脑袋疼的是如何出队入队?

我们求的是区间最小值,如果这个值是INF(我们绝对取不到的最大值),我们一定不会在维护区间的时候出现这个值,如果我们用单点修改把一个有实数的点修改成INF,不就是把这个点删掉了吗?添加和修改点就更容易了,也是单点修改就好啦。

注意,我们要维护两个值,一个是dis[]值,一个是这个dis[]的位置(下标)。这样你就可以快速的保证出队操作了。

我们如果最后查到区间最小值就是INF了,就是这个区间里并没有实数了,也就是队列空了。

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
using namespace std;
#define go(i, j, n, k) for (int i = j; i <= n; i += k)
#define fo(i, j, n, k) for (int i = j; i >= n; i -= k)
#define rep(i, x) for (int i = h[x]; i; i = e[i].nxt)
#define mn 100010
#define mm 200020
#define inf 2147483647
#define ll long long
#define ld long double
#define fi first
#define se second
#define root 1, n, 1
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
#define bson l, r, rt
inline int read(){
    int f = 1, x = 0;char ch = getchar();
    while (ch > '9' || ch < '0'){if (ch == '-')f = -f;ch = getchar();}
    while (ch >= '0' && ch <= '9'){x = x * 10 + ch - '0';ch = getchar();}
    return x * f;
}
inline void write(int x){
    if (x < 0)putchar('-'),x = -x;
    if (x > 9)write(x / 10);
    putchar(x % 10 + '0');
}
//This is AC head above...
struct node{
    int v, nxt, w;
} e[mm << 1];
int h[mn], p;
inline void add(int a,int b,int c){
    e[++p].nxt = h[a];
    h[a] = p;
    e[p].v = b;
    e[p].w = c;
}
int dis[mn];
int n, m, s, t;
struct tree{
    int minw, minv;
};
struct SegmentTree{
    tree z[mn << 2];
    inline void update(int rt){
        z[rt].minw = min(z[rt << 1].minw, z[rt << 1 | 1].minw);//维护区间最小值
        z[rt].minv = (z[rt << 1].minw < z[rt << 1 | 1].minw) ? z[rt << 1].minv : z[rt << 1 | 1].minv;//维护区间最小值位置
    }
    inline void build(int l,int r,int rt){//建树
        if(l==r){
            z[rt].minw = l == s ? 0 : inf;//我们可以直接建树时把s的点设置为0
            z[rt].minv = l;//记录最小值位置,方便修改
            return;
        }
        int m = (l + r) >> 1;
        build(lson);
        build(rson);
        update(rt);
    }
    inline void modify(int l,int r,int rt,int now,int v){//单点修改
        if(l==r){
            z[rt].minw = v;
            return;
        }
        int m = (l + r) >> 1;
        if(now<=m)
            modify(lson, now, v);
        else
            modify(rson, now, v);
        update(rt);
    }
} tr;
inline void Dij(){//Dijkstra的核心部分
    go(i,1,n,1){
        dis[i] = inf;
    }//初始化dis
    dis[s] = 0;
    while(tr.z[1].minw < inf){//这里就是判断是否为空
        int x = tr.z[1].minv;//取整个线段树中最小的点
        tr.modify(root, x, inf);//单点修改最小的点为inf
        rep(i,x){
            int v = e[i].v;
            if(dis[v] > dis[x] + e[i].w){
                dis[v] = dis[x] + e[i].w;
                tr.modify(root, v, dis[x] + e[i].w);//这里就是类似入队操作
            }
        }
    }
}
int main(){
    n = read(), m = read(), s = read(), t=read();
    go(i,1,m,1){
        int x = read(), y = read(), v = read();
        add(x, y, v);
        add(y, x, v);//这个一定记住,无向图要正反两条边QAQ
    }
    tr.build(root);//建树
    Dij();//Dijkstra
    cout << dis[t];
    return 0;
}

P1339 AC代码

对于线段树的用法我们就先讲到这里,以后还会讲一些

推荐习题:

P4779 【模板】单源最短路径(标准版) //不会的话这里有个我认识的大佬的题解是线段树优化Dijkstra,如果看不懂我讲的话去看看他的题解吧
SP1043 GSS1 - Can you answer these queries I //线段树维护区间最大子段和裸体
SP1716 GSS3 - Can you answer these queries III //上面那道题加个单点修改,这个是没有问题的
SP1557 GSS2 - Can you answer these queries II //建议先做完上面两道题目再看这道,这个有一些其他的东西
GSS系列题目真的是数据结构题里的好题,大家可以去尝试着去做一下。

下一期我们讲一个更优的线段树,它光荣的采用堆式存储,更简洁更快,敬请期待。

NOIP2018并不是结束,而是开始
原文地址:https://www.cnblogs.com/yizimi/p/10056180.html