恶魔之妹&秘弹「之后就一个人都没有了吗?」

题面:

image.png


(今日的芙兰也很可爱哦(^_^))
二小姐芙兰朵露在地下室很无聊,在听说妹红的灭罪后心血来潮,决定去找雾之湖的妖精们玩游戏。
芙兰朵露成功寻找到了N名愿意陪她玩的妖精,依次编号为1~N,游戏是一个简单的捉迷藏,奖品是二小姐最爱的巧克力蛋糕,并且芙兰朵露始终扮演猎人,妖精们始终扮演猎物,猎人通过找到猎物得分,猎物通过逃离到指定的地点得分。
但是对于这个规则,妖精们不满意,因为总是芙兰朵露来充当猎人的角色,于是规定芙兰朵露只可以选择其中一次找到妖精的记录作为最终结算成绩,并且,被抓住的妖精还可以继续进行游戏,得分也不会清除

为了避免捉迷藏时会晕头转向,她们把捉迷藏的得分记录写在了本子上,共有M行,每行写3个正整数,有且仅可能有以下两种内容:
i)1 id val,操作1,代表妖精方得分,编号为id的妖精得到了值为val的分数
ii)2 l r ,操作2,代表二小姐芙兰朵露得分,她找到了编号在l~r闭区间范围内的妖精,在本次记录中获得的分数为:编号l~r的妖精当前分数总和
(因为雾之湖的妖精们都是智商为⑨的群居生物,她们总是会以一个个集体来行动,被找到的时候也会是一个集体)
初始情况下,所有人得分均为0。

现在,芙兰朵露想请你帮忙计算出,她依据上述规则最多可以获得的得分。

输入格式:

本题仅有一组数据
第一行给出一个正整数N1<=N<=1e5)和一个正整数M1<=M<=1e5),含义如上述题意。
接下来M行,每行给出三个正整数(op, id, valop, l, r)代表得分记录(1<=op<=2, 1<=id<=1e5, 1<=val<=1e5, 1<=l<=r<=1e5),含义如上述题意。
题目数据保证两方一定都有至少一次不为0的有效得分且芙兰朵露必须选择一次有效得分记录。

输出格式:

输出一个整数,代表芙兰朵露最多可以获得的分数。
题目数据保证所有的的求和结果均在长整型范围内。

输入样例1:

10 2
1 1 5
2 1 10

输出样例1:

5

样例解释1:

初始芙兰朵露和10名妖精均为0分,依据两条得分记录,
第一条表示第1号妖精获得5分,此时妖精方得分为5 0 0 0 0 0 0 0 0 0,
第二条表示芙兰朵露找到了编号1~10的所有妖精,获得5+0+0+0+0+0+0+0+0+0=5分,且必须选择这唯一一条有效记录,获得5分。

输入样例2:

10 4
1 1 4
2 1 10
1 8 9
2 7 9

输出样例2:

9

样例解释2:

依据四条记录,芙兰朵露拥有两条有效分数,分别是4和9,
显然,为了获得最多的得分,芙兰朵露会选择9分的记录。

题解思路:

其实一开始是想基于上题的灭罪,做成一个进阶版运用。后来由于各种原因只能把各种东西压缩,成了现在的样子,可能一些去做题的也能看出一些在铺垫上的不协调吧(无语子)。

题意还是较好理解,根据操作表,多次进行单一元素修改区间元素求和操作,并找出每次求和得到的值中的最大者,输出即可。(原本这个求最大值操作是想结合一点点贪心,竞争一下25分题压轴难度的,但是数据太难造了,真的很佩服那些大网站的出题人orz,现在就只是简单的一个if-else操作来寻找最大值就OK,成了25分中的送分题,主要是想避免和其他模版题撞车,乱加一点小东西(笑))。

虽然本题和灭罪那题都是对区间求和,但相信细心的读者会看见有一些改动,上题的区间修改过程中,原数组序列的值是一直不会改动的,而本题中则是会随机穿插有对单一元素的修改,这一点操作就会导致:原本使用前缀和思想解上题是绰绰有余的O(1~N)时间复杂度,在本题变成了O(N²),因为你需要随时去维护因为操作1而变动的前缀和元素值,而数据范围限制刚好卡在1e4~1e5,导致最终提交TLE

怎么去解决这个问题呢,这里我们要引入一种基于区间求和思想而衍生的一种数据结构——线段树,它不仅可以解决简单的静态区间查询求和,也可以解决这种随时在变化的,动态区间值求和问题,最终的复杂度也是稳定秉持在O(NlogN)之内的。本文仅简单给出线段树的定义和基本运用,具体的更详细的底层实现与高阶运用还是要读者去大佬的帖子下面仔细研读。这里推荐一个线段树入门链接:线段树详解(附图解,模板及经典例题分析)-CSDN博客。

下面给出一张图方便理解:

线段树也属于是数据结构中的树形结构之一,我们学习的二叉树性质,在线段树中也是基本遵守的。比如,每个结点附带有左右子树结点,层层递归至末端叶子结点。只是相对于一般的二叉树,线段树每个结点中所保存的值不单单只是一个单纯存在的值,而是保存它之下的所有结点的值之和(看看上面的图辅助理解),比如当前有一个结点,它拥有着左右子结点,且左右子结点都是叶子结点了,值分别为3,4;那作为这两个结点的根结点,它的值就该是3+4=7,以此类推,线段树的最大根节点存储的值就正好是给定序列的总求和值。

这种存储数据的结构有什么好处呢?首先,基于二叉树自带二分的特性,我们在遍历线段树结点的同时相当于是在一次次的使用二分查找来寻找区间;二是在修改区间值时,只需要遍历到对应的结点处修改值,再一层层反遍历出来,依次更新它所属的所有上级结点的值(这每一次的操作也都是二分查找),就可以简单快捷地进行序列区间或单一源点的值修改了;最后就是求和操作,这个比较容易理解,基于线段树的这种特性,完全可以把每一个结点的值看作是一个个区间的求和值,同样只需要遍历到对应的子结点(子区间),拿出该结点的数据来使用即可。

本题目的即是让做题者能够了解并学习简单运用线段树的单点修改(操作1)和区间查询(操作2)操作。

题解代码:

事先提醒,线段树的基本模板底层实现代码是较为冗长的,但是实际上的理解还是不难,主要是看学习的耐心了,像是本题的线段树是基于树形数组的思想来建立的,基本的底层操作还是多次的二分+递归,只要打好二分查找和递归基础,理解起来很容易。

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);

using namespace std;
using ll=long long;
const int MAX=1e5+10;

int n, m;
ll a[MAX], tree[MAX<<2], ans;        //这里是把线段树本体存放在一个数组tree中的,由于树形结构特性,每层所耗空间是一般线性数组的4倍,这里就需要开原数组的4倍空间大小

void build(int, int, int);            //建立线段树函数
void modify(int, int, int, int, int);    //修改单点元素值函数
int quray(int, int, int, int, int);    //对区间元素值求和函数

int main(void){
    IOS;
      cin>>n>>m;
    for(int i=1;i<=n;i++)
        a[i]=0;

    build(1, 1, n);        //一开始肯定是基于原有序列进行建树

    while(m--){
        int f, x, y;
          cin>>f>>x>>y;
        if(f==1) modify(1, 1, n, x, y);    //操作1,修改单源点值

        else{
            ll temp=quray(1, 1, n, x, y);    //操作2,对指定区间求和
            ans=max(ans, temp);        //寻找单次求和的最大值
        }
    }
      cout<<ans<<'\n';
    return 0;
}


void build(int p, int l, int r){        //建立一个下标范围为 l~r 的线段树,根节点从p开始
    if(l==r){
        tree[p]=a[l];        //递归到叶子节点,直接赋值
        return ;
    }
    
    int mid=l+r>>1;            //向下二分查找左右子树
    build(p<<1, l, mid);        //建立左子树(上一层结点的下标乘以2)
    build((p<<1)+1, mid+1, r);    //建立右子树(上一层乘以2后+1)
    
    tree[p]=tree[p<<1]+tree[(p<<1)+1];    //上层结点保存它左右子树结点的和
}

void modify(int p, int l, int r, int id, int val){    //在范围为 l~r 的线段树中修改值
                                            //在第id个位置上增加val
    if(l==r){
        tree[p]+=val;            //二分查找到第x个位置,给当前值+val
        return ;
    }
    
    int mid=l+r>>1;
    if(id<=mid) modify(p<<1, l, mid, id, val);
    else modify((p<<1)+1, mid+1, r, id, val);        //递归遍历+二分查找左右子树
    
    tree[p]=tree[p<<1]+tree[(p<<1)+1];        //修改结点的值后,同样需要更新它祖先结点的值
}


int quray(int p, int begin, int end, int l, int r){    //计算下标为 l~r 范围的和
    if(l<=begin&&end<=r){
        return tree[p];        //当递归遍历的区间真包含了待查找的区间,直接返回当前结点的值
    }
     
    int mid=begin+end>>1;                                   //递归+二分左右子树
    if(r<=mid) return quray(p<<1, begin, mid, l, r);    //如果待查找的右区间都比当前遍历区间中点要小
                                                        //(即待查找的区间完全在左子树部分),递归左子树
    else if(l>mid) return quray((p<<1)+1, mid+1, end, l, r);        //同上理解,遍历右子树
     
    return quray(p<<1, begin, mid, l, mid)+quray((p<<1)+1, mid+1, end, mid+1, r);
                                //如果左右子树两边都与待查找区间存在交集情况,那么两边都需要递归
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值