bzoj 3747 题解

本文介绍了bzoj 3747题目,讨论如何找到一段区间,使得区间内只出现过一次的电影的好看值之和最大。通过枚举右端点,利用pre数组记录电影上次出现位置,并使用线段树处理区间加减和求最大值的问题,最终得出最优解。

题意简述

mmm个电影,编号111 ~ mmm,第iii部电影有一个好看值wiw_iwi。并且,每天都会放某一个电影,第iii天放第aia_iai部电影。请选出一段区间lll ~ rrr,使得这一段区间中只出现过一次的电影好看值和最大。

数据

输入
9 4
2 3 1 1 4 1 2 4 1
5 3 6 6
输出
15

解释

222 ~ 777部电影,只出现过一次的是第222333,444部电影,好看值和=3+6+6=15=3+6+6=15=3+6+6=15最大。

思路

这题是我一次考试题,然鹅并没有想出正解,直接n2n^2n2,由于老师比较仁慈,拿了404040分,就rank1了rank1了rank1
但是,O(n2)O(n^2)O(n2)只是土匪方法,要积极想想正解。
考完之后,翻了翻题解。其实,这题还是很简单的(只是提醒您不要对以下内容有所恐惧)。

首先我们会发现,枚举区间是必要的。那么,是枚举左端点还是右端点呢?
都枚举
显然不珂以。考虑到我们要记录出现了多次的电影,我更喜欢到前面找,所以就枚举右端点吧。除此之外,还需要记录一个preprepre数组,pre[i]pre[i]pre[i]表示a[i]a[i]a[i]这部电影上次出现的位置,=0=0=0表示第一次出现。(这个如何记录是入门难度,我就不细讲了)。

在枚举右端点iii的过程中,还要维护一个数组ssss[j]s[j]s[j]表示选择jjjiii这一段区间的答案。会发现,pre[i]+1pre[i]+1pre[i]+1iii这一段是肯定只包含一个a[i]a[i]a[i]的,所以在右端点iii往后+1+1+1的时候,pre[i]+1pre[i]+1pre[i]+1iii要区间加上w[a[i]]w[a[i]]w[a[i]]。但是也要考虑重复的答案,如果pre[i]pre[i]pre[i]非0,说明a[i]a[i]a[i]出现过多次,要减掉多算的答案。显然,这一段多算的就是pre[pre[i]]+1pre[pre[i]]+1pre[pre[i]]+1pre[i]pre[i]pre[i],要区间减掉w[a[i]]w[a[i]]w[a[i]]

那么,在右端点为iii时的答案就是max{s1,s2⋯si}max\{s_1,s_2\cdots s_i\}max{s1,s2si}

我们发现,在iii不断+1+1+1时,我们对sss数组做的更新操作有两种:

  1. 区间加/减
  2. 求区间最大值

这就变成了一个很裸的线段树,裸的有点R18R18R18

最后,在所有的右端点答案中取最大即可。

代码:

#include<bits/stdc++.h>
#define N 1001000
#define int long long
using namespace std;
class SegmentTree//资瓷区间+-和询问最大值的线段树
{
    private:
        struct node
        {
            int l,r;
            int s,a;
        }tree[N<<2];
    public:
        #define ls index<<1
        #define rs index<<1|1

        #define L tree[index].l
        #define R tree[index].r
        #define S tree[index].s
        #define A tree[index].a
        void Update(int index)
        {
            S=max(tree[ls].s,tree[rs].s);
        }
        void BuildTree(int l,int r,int index)
        {
            L=l,R=r;
            S=A=0;
            if (l==r)
            {
                return;
            }
            int mid=(l+r)>>1;
            BuildTree(l,mid,ls);
            BuildTree(mid+1,r,rs);
            Update(index);
        }
        void AddOne(int x,int index)
        {
            A+=x;
            S+=x;
        }
        void PushDown(int index)
        {
            if (A)
            {
                AddOne(A,ls);
                AddOne(A,rs);
                A=0;
            }
        }
        void Add(int l,int r,int x,int index)
        {
            if (l>R or L>r) return;
            if (l<=L and R<=r)
            {
                AddOne(x,index);
                return;
            }
            PushDown(index);
            Add(l,r,x,ls);
            Add(l,r,x,rs);
            Update(index);
        }
        int Query(int l,int r,int index)
        {
            if (l>R or L>r) return 0;
            if (l<=L and R<=r)
            {
                return S;
            }
            PushDown(index);
            return max(Query(l,r,ls),Query(l,r,rs));
        }
}T;

int n,m;
int a[N],w[N];
int pre[N],last[N];
void Input()
{
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%lld",&w[i]);
    }
    for(int i=1;i<=n;i++)
    {
        pre[i]=last[a[i]],last[a[i]]=i;
    }//处理pre数组
}

void Solve()
{
    T.BuildTree(1,n,1);//写线段树一定不要忘了建树(当然有些写法珂以不写这句)
    int ans=0;
    for(int i=1;i<=n;i++)
    {
        T.Add(pre[i]+1,i,w[a[i]],1);//区间加上w[a[i]]
        if (pre[i])
        {
            T.Add(pre[pre[i]]+1,pre[i],-w[a[i]],1);//减掉多算的w[a[i]]
        }
        ans=max(ans,T.Query(1,i,1));//取所有右端点答案的最大值
    }
    printf("%lld\n",ans);
}
main()
{
    Input();
    Solve();
    return 0;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值