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


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

    一、题目描述

    给定长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,每条指令可能是以下两种之一:

    1 x y,查询区间 \([x,y]\) 中的 最大连续子段和,即 \(\displaystyle \max_{x≤l≤r≤y}{\sum_{i=l}^rA[i]}\)
    2 x y,把 \(A[x]\) 改成 \(y\)

    对于每个查询指令,输出一个整数表示答案。

    输入格式
    第一行两个整数 \(N,M\)

    第二行 \(N\) 个整数 \(A[i]\)

    接下来 \(M\) 行每行 \(3\) 个整数 \(k,x,y,k=1\) 表示查询(此时如果 \(x>y\),请交换 \(x,y\)),\(k=2\) 表示修改。

    输出格式
    对于每个查询指令输出一个整数表示答案。

    每个答案占一行。

    数据范围
    \(N≤500000,M≤100000, −1000≤A[i]≤1000\)

    二、题目解析

    单点修改+带跨区间查询:最长连续子段和 无懒标
    本题依据题意需要进行 单点修改,因此无需懒标记 \(pushdown\) 操作,只需要 \(pushup\) 操作即可。

    对于查询区间内部的最大子段和,我们需要想想每个节点内部需要存储哪些信息 才能保证儿子节点向父亲节点顺利pushup传递信息。(这是我们面对线段树题目时的一个很重要的思考点)

    设线段树中的节点为\(node\),我们显然最先应该存储 区间的左、右端点\(l、r\),因为线段树本质上是一棵由区间作为节点的二叉树。

    同时,依据题意 还应该存区间内部的最大连续子段和,我们用total_max表示,简写为tmax

    我们来想一下,只存这些信息就够了吗?只利用当前存储的信息,能够实现 父节点的最大连续子段和tmax两个儿子节点的最大连续子段和 求出吗?

    显然是不够的。

    原因:两个 儿子节点 各自tmax可能会出现完全处于区间内部的情况,且不包含边界,然而父亲节点tmax可能会出现 横跨两个区间的情况 。就好比下图所示(红色括号所包括的范围表示节点的tmax):

    对于像上图的这种 父节点tmax横跨两个区间 的这种情况,我们其实需要再 额外存储两个信息

    • 以左儿子区间右端点为起点 向左 的最大后缀和

    • 以右儿子区间左端点为起点 向右 的最大前缀和

    小结一下,也就是说每个节点还需要存储它的 最大前缀和最大后缀和,这样, 父节点 横跨两区间 的最大连续子段和 = 左儿子的最大后缀和 + 右儿子的最大前缀和

    (左右儿子区间完全独立,两者没有任何关系,没有限制,左右两区间取max即为父节点tmax

    我们设节点最大前缀和lmax,最大后缀和为rmax

    我们有下面三种情况
    父节点 tmax 没有横跨区间的情况包含两种:

    • ① 完全在左儿子区间内部:tmax = 左儿子tmax
    • ② 完全在右儿子区间内部:tmax = 右儿子tmax

    父节点tmax 横跨两区间 情况为一种:

    • 父节点u.tmax = 左儿子rmax + 右儿子lmax

    综合一下得出表达式

    \[父节点u.tmax = max(L_{son}.tmax, R_{son}.tmax, L_{son}.rmax + R_{son}.lmax) \]

    至此,我们已有方法计算出每个节点内部的\(tmax\)了。

    新的问题

    不过我们还需要想一下新加的两个变量:\(lmax\)最大前缀和) 和 \(rmax\)最大后缀和)如何得到。

    和之前的思考方式类似,我们分情况来讨论:

    对于一个父节点的\(lmax\),我们也可以分为两种情况:

    • ① 没有跨过分界点,如下图,\(父节点lmax = 左儿子lmax\)
    • ② 跨过了分界点,如下图

    对于上方的情况 ②,我们发现运用现有的条件是无法得到的:

    父节点最大前缀和\(u.lmax\) = 左儿子\(L_{son}.\)区间总和 + 右儿子\(R_{son}.lmax\)

    同理:

    父节点最大后缀和\(u.rmax\) = 右儿子\(R_{son}.\)区间总和 + 左儿子\(L_{son}.rmax\)

    所以说,我们的节点最后还需要一个新的信息区间和(设为\(sum\)

    而对于 父节点\(u.sum\)也是可以计算出来的,我们可以由左儿子\(L_{son}.sum\) + 右儿子\(R_{son}.sum\)

    表达式:

    \[父节点 u.sum = L_{son}.sum + R_{son.sum} \]

    我们综合一下得出两个最大前后缀和的表达式:

    ① 父节点\(u.lmax = max(L\_son.lmax, L\_son.sum + R\_son.lmax)\)

    ② 父节点\(u.rmax = max(R\_son.rmax, R\_son.sum + L\_son.rmax)\)

    至此,我们已经能够确定好存储线段树的结构体包含了哪些变量,进而可以确定\(pushup\)函数的编写,整个编码的大体框架不变,由四个函数构成:

    序号 功能 函数名
    \(build\) 建立
    \(pushup\) 将自己子孙后代的变化向上级领导汇报
    \(modify\) 修改
    \(query\) 查询

    本题对于\(query\)函数另有分类等细节处理,详见代码。

    时间复杂度

    \(O(mlogn)(m<=1e5,n<=5e5)\)

    三、实现代码

    include <bits/stdc++.h>

    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({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) {
    // lmax:因为只有一个,所以是a[l]
    // rmax:因为只有一个,所以是a[l]
    // mx:因为只有一个,所以是a[l]
    // sum:区间总和是a[l]
    tr[u] = {l, r, a[l], a[l], a[l], a[l]};
    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,mx=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); //有点类似于后序遍历输出
    }
    }

    //查询的时候也需要算那四个数的,因为查的时候也可能涉及到区间合并的
    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 ls = query(u << 1, l, r);
    Node rs = query(u << 1 | 1, l, r);
    
    //将左右儿子送回来的信息进行合并
    Node res; //这里的res其实并不是一个真正在线段树中存在的节点,而是一个临时变量,方便利用pushup
    //函数的计算逻辑,计算出mx,这么写确实是可以复用代码,就是理解起来麻烦了
    pushup(res, ls, rs);
    
    return res;
    

    }

    int main() {
    //加快读入
    ios::sync_with_stdio(false), cin.tie(0);

    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;
    

    }

  • 相关阅读:
    以太坊公开拍卖智能合约案例
    部署智能合约
    ERC20 Token
    查看crontab运行状态
    php把excel数值格式转成日期格式
    PHP识别中文编码并自动转换为UTF-8
    python中文件内容替换
    mysqldump备份指定的数据
    linux设置服务器时间
    git常用命令
  • 原文地址:https://www.cnblogs.com/littlehb/p/16152783.html
Copyright © 2020-2023  润新知