LA3938 Ray, Pass me the dishes! 线段树前后缀和

题目链接
这题是非常经典的线段树例题,建议自己试着完成代码,对加深线段树理解很有帮助。

首先是建树

这道题实际上建了三棵线段树,前缀和,后缀和,连续和各一棵。
对于结点node[n],前缀和的伪代码为:

node[n].pre = max(node[left_son].pre, node[left_son].sum + node[right_son].pre);

在左子树的前缀和,左子树的区间和加上右子树的前缀和中取最大的。后缀和的做法相似。
对于最大连续和,伪代码为:

node[n].sub = max(node[left_son].sub, node[right_son].sub, node[left_son].suf + node[right_son].pre);

在左右子树最大连续和以及左子树后缀和加上右子树前缀和中取最大的。

然后是查询

如果当前结点被查询的区间所包含:
返回这个结点的sub。
否则:
进入左子树查询
进入右子树查询
返回左右子树的sub和左子树suf+右子树pre中的最大值

代码的思路比较清楚,结合上面的解释应该比较容易理解:

//Time:309ms
#include <bits/stdc++.h>
#define MAXN 500005
using namespace std;

int length, data[MAXN], ql, qr;

struct node
{
    int l, r;
    long long v;
};

long long sum[MAXN]; //sum[i]表示从1到i的区间和,如果要求[l,r]的区间和,只要计算sum[r]-sum[l-1]即可
node pre[MAXN * 4], suf[MAXN * 4], sub[MAXN * 4]; //前缀和,后缀和,最大连续和

inline node max(node a, node b)
{
    if (a.v >= b.v)
        return a;
    return b;
}

//建树
void build(int l, int r, int root = 1)
{
    if (l == r)
    {
        pre[root] = suf[root] = sub[root] = {l, l, data[l]};
        return;
    }
    int mid = (l + r) >> 1;
    build(l, mid, root << 1);
    build(mid + 1, r, root << 1 | 1);
    //更新pre,左子树的pre 或者 左子树的和+右子树的pre
    pre[root] = max(pre[root << 1], node{l, pre[root << 1 | 1].r, sum[(l + r) >> 1] - sum[l - 1] + pre[root << 1 | 1].v});
    //更新suf,左子树的suf+右子树的和 或者 右子树的suf
    suf[root] = max(node{suf[root << 1].l, r, sum[r] - sum[(l + r) >> 1] + suf[root << 1].v}, suf[root << 1 | 1]);
    //更新sub 左子树的sub 或者 左子树的suf+右子树的pre 或者 右子树的sub
    node temp = node{suf[root << 1].l, pre[root << 1 | 1].r, suf[root << 1].v + pre[root << 1 | 1].v};
    sub[root] = max(sub[root << 1], max(temp, sub[root << 1 | 1]));
}

//查询[l,r]的最大前缀和,最终返回的结果是从l开始的最大前缀和,查询过程也用了分治思想
//可以画个简单的线段树进行理解
node query_pre(int l, int r, int root = 1)
{
    if (ql <= l && qr >= r)
        return pre[root];
    int mid = (l + r) >> 1;
    if (qr <= mid)
        return query_pre(l, mid, root << 1);
    if (ql > mid)
        return query_pre(mid + 1, r, root << 1 | 1);
    node p1 = query_pre(l, mid, root << 1);
    node p2 = query_pre(mid + 1, r, root << 1 | 1);
    //这一步容易漏,比较重要,递归的深度不同,ql和l的大小关系会发生变化
    //始终要取二者中较大的一个,防止区间越界,qr和r同理
    int L = ql > l ? ql : l;
    int R = qr < r ? qr : r;
    node p3 = node{L, p2.r, p2.v + sum[mid] - sum[L - 1]};
    return max(p1, p3);
}

//查询[l,r]的最大后缀和
node query_suf(int l, int r, int root = 1)
{
    if (ql <= l && qr >= r)
        return suf[root];
    int mid = (l + r) >> 1;
    if (qr <= mid)
        return query_suf(l, mid, root << 1);
    if (ql > mid)
        return query_suf(mid + 1, r, root << 1 | 1);
    node s1 = query_suf(l, mid, root << 1);
    node s2 = query_suf(mid + 1, r, root << 1 | 1);
    int L = ql > l ? ql : l;
    int R = qr < r ? qr : r;
    node s3 = node{s1.l, R, s1.v + sum[R] - sum[mid]};
    return max(s3, s2);
}

//查询[l,r]的最大连续和
node query(int l, int r, int root = 1)
{
    if (ql <= l && qr >= r)
        return sub[root];
    int mid = (l + r) >> 1;
    if (qr <= mid)
        return query(l, mid, root << 1);
    if (ql > mid)
        return query(mid + 1, r, root << 1 | 1);
    node sub1 = query(l, mid, root << 1);
    node sub2 = query(mid + 1, r, root << 1 | 1);
    node suf1 = query_suf(l, mid, root << 1);
    node pre1 = query_pre(mid + 1, r, root << 1 | 1);
    node sub3 = node{suf1.l, pre1.r, suf1.v + pre1.v};
    return max(sub1, max(sub3, sub2));
}

int main()
{
    sum[0] = 0;
    int q, cnt = 1;
    while (cin >> length >> q)
    {
        for (int i = 1; i <= length; i++)
        {
            scanf("%d", &data[i]);
            sum[i] = data[i] + sum[i - 1];
        }
        build(1, length);
        printf("Case %d:\n", cnt);
        while (q--)
        {
            scanf("%d%d", &ql, &qr);
            node temp = query(1, length);
            printf("%d %d\n", temp.l, temp.r);
        }
        cnt++;
    }
    return 0;
}

代码存在的优化空间:

pre只需存储右端点即可,suf只需存储左端点即可,这样可以减少赋值运算和空间。但是要创建三种结构体,代码量会增加不少。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值