AcWing 245. 你能回答这些问题吗


题目传送门

主要说一下一个新手普遍疑惑的地方。

为什么\(query\)返回值是结构体

假设你查询区间\([l, r]\)

并且现在你递归到当前节点\(p\),其左儿子为\(p1\),右儿子为\(p2\)

并且现在区间\([l, r]\)同时跨到了\(p1\)\(p2\)

按照以前区间查询单点修改的线段树,我们会这样写:
return max(query(p1,l,mid), query(p2,mid+1,r))
这样写就漏解了,注意,上面这句话的实际含义是只比较了\(p1\)的最大子段和 和 \(p2\)的最大子段和,漏掉了跨过\(mid\)的最大子段和。

所以你应该这么写:

return max(query(p1,l,mid), query(p2,mid+1,r), 跨过mid的最大子段和)

我们可以发现,“跨过\(mid\)的最大子段和”无法表达出来,怎么办呢?

其实很简单,你把query(p1,l,mid)看成一个左儿子,query(p2,mid+1,r)看成右儿子,然后新开一个父亲。对这三个节点做一次\(pushup\)就行了。然后返回父亲的\(tmax\)域即可。

因为\(pushup\)里头父亲、左儿子、右儿子的每个域都要用到,所以将\(query\)函数的返回类型换成结构体就行了。

#include 

using namespace std;
const int N = 500010;
int n, m;
int a[N]; //临时数组,用于装一下输入的数字

struct Node {
    int l, r;
    int sum;  // 区间和
    int lmax; //左后缀最大和
    int rmax; //右前缀最大和
    int tmax; //整体最大和
} tr[N << 2];

//向祖先推送信息
//注意这里的Node是用的地址符,即按地址传递参数,函数内修改的是原来的变量
//因为pushup需要在很多地方用,所以这里多写一层
void pushup(Node &u, Node &l, Node &r) {
    u.sum = l.sum + r.sum;                              //区间和
    u.lmax = max(l.lmax, l.sum + r.lmax);               //左端区间和+右端前缀最大和
    u.rmax = max(r.rmax, r.sum + l.rmax);               //右端区间和+左端后缀最大和
    u.tmax = max(max(l.tmax, r.tmax), l.rmax + r.lmax); //三者取max
}
void pushup(int u) { //函数重载,这样主要是方便写起来方便
    pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}

//构建
void build(int u, int l, int r) {
    if (l == r) {
        int x = a[r];
        // lmax:因为只有一个,所以是a[r]
        // rmax:因为只有一个,所以是a[r]
        // tmax:因为只有一个,所以是a[r]
        // sum:区间总和是a[r]
        tr[u] = {l, r, x, x, x, x};
        return;
    }
    tr[u] = {l, r}; //构建最重要的就是设置好范围
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
    //由子节点信息算一下父节点信息
    pushup(u);
}

//在以u节点为根的子树中,将位置x的值修改为v
void modify(int u, int x, int v) {
    //如果已经到了叶节点
    if (tr[u].l == x && tr[u].r == x)
        tr[u] = {x, x, v, v, v, v}; // l=r=x:因为只有老哥一个 sum=v lmax=v,rmax=v,tmax=v
    //注意:叶子节点的更改,是不需要pushup(u)的!!可以理解为此时u没有子节点了
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) //那么x一定在左半边
            modify(u << 1, x, v);
        else // x一定在右半边
            modify(u << 1 | 1, x, v);
        //由于更新了左半边或者右半边中的某一个数据,所以需要再次由子节点信息算一下父节点信息
        pushup(u);
    }
}
//查询的时候也需要算那四个数的,因为查的时候也可能涉及到区间合并的,WHY?
Node query(int u, int l, int r) {
    //要查找的区间tr[u].l ~ tr[u].r 包含了[l,r]这个区间
    if (tr[u].l >= l && tr[u].r <= r) return tr[u];

    int mid = tr[u].l + tr[u].r >> 1;
    if (r <= mid) return query(u << 1, l, r);    //返回左半边
    if (l > mid) return query(u << 1 | 1, l, r); //返回右半边
    //左右各半
    Node left = query(u << 1, l, r);
    Node right = query(u << 1 | 1, l, r);
    //将左右儿子送回来的信息进行合并
    Node res; //这里的res其实并不是一个真正在线段树中存在的节点,而是一个临时变量,方便利用pushup
    //函数的计算逻辑,计算出tmax,这么写确实是可以复用代码,就是理解起来麻烦了
    pushup(res, left, right);
    return res;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    //构建树,root=1,范围[1,n]
    build(1, 1, n);

    int k, x, y;
    while (m--) {
        cin >> k >> x >> y;
        if (k == 1) {
            if (x > y) swap(x, y);
            printf("%d\n", query(1, x, y).tmax);
        } else
            modify(1, x, y);
    }
    return 0;
}