2019牛客暑期多校训练营(第七场)F-Energy stones(思维+树状数组)

>传送门<

题意:n块能量石,每秒钟会增加Li的能量,但是一旦增长到了Ci它就不会增长了,它初始的能量为Ei。 现在有若干个时刻ti,会选择下标在[Si,Ti]的能量石吸取它们的能量,这些能量石的能量变为0,并依据上述规则继续增长。 问最后一共吸取了多少能量?
思路:
本来我写的时候没考虑到时间复杂度,蜜汁自信觉得应该能跑出来,然并卵,我模拟了整个过程,毫无疑问的TLE了。这题的做法好像是被称作为线性期望性,大概可以理解为,比如有m次查询,我直接每次求多个数据的期望和,最后把这m次期望加起来就是答案,但是你发现每次求多个数据的期望的时候操作次数太多了,导致很难求或者时间复杂度过高。那么我们换个思路想一下,要是我们能分别求出每个数据在这m次查询中的期望,那么最后加起来不就是所有数据的期望了么。

这里我们考虑每块石头吸取了多少能量(即每个石头对答案的贡献)

假如我们能够维护出每块石头被吸取能量的若干个时间点,那么它们的增长能量的时间区间会是一条条线段(时间差)。

那么考虑这些线段长度大于等于[Ci/Li]的线段,它们的贡献是Ci,其他线段都没有长满,那么是ti*Li 。

那么考虑如何维护线段?用set + 树状数组

用一个set,考虑插入一个时间点会让一条原本的线段分裂成两段,删除一个时间点会让两个线段合并。(至多修改三个区间差)

那么对于一次吸取操作(ti,Si,Ti),相当于我们枚举到Si的时候插入ti这个时间点,枚举到Ti+1的时候删除ti这个时间点。

我自己在敲代码的时候就在想,你只在Si插入ti,那么之后的Si+1Si+2. . . 那还有好多能量石怎么办,后来发现原来只是在没遇到Ti+1前一直保留这个时间点ti,这样就能保证在这个区间内每个点都能有这个时间点的信息

然后我们用树状数组维护这个线段

两个树状数组分别维护:

  1. 每个时间段一共有多少时间(可以求出<[Ci/Li]累计的时间)
  2. 每个时间段的个数(可以求出>=[Ci/Li]的时间段总数)

最后要注意Li等于0的情况,处理一下初始状态就可以了

关于计算初始状态,我自己开始看的时候也有些地方看不明白,这里解释一下:

对于计算每个石头的贡献,我们实际是由最开始一个时间点出发,然后不断到下一个时间点,直到最后一个时间点得出答案。如果合并成线段的话,就变成最开始的一个时间点加上相邻的点构成的线段也可以到达最后一个时间点。因此我们计算初始状态是用的第一个时间点,与我们树状数组存的时间差没有关系,不要搞混淆了。ans更新的时候s.size()-1减的就是第一个时间点。

感谢咖啡鸡 巨巨(大佬的意思~)的代码

Code

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
typedef long long ll;
const int maxn=2e5+6;

int t, n, m;
ll d, ans;
ll E[maxn], L[maxn], C[maxn], a[maxn], b[maxn];
vector<int> h[maxn];
set<int> s; //记录时间点 
//求和 
ll qry1(int x){
    ll ret = 0;
    while (x){
        ret += a[x];
        x -= x&(-x);
    }
    return ret;
}
ll qry2(int x){
    ll ret = 0;
    while (x){
        ret += b[x];
        x -= x&(-x);
    }
    return ret;
}
//更新树状数组 
void Add(int x,int y){
    while (x<maxn){
        if (y>0) a[x]++; else a[x]--;
        b[x]+=y;
        x+=x&(-x);
    }
}
//往set里加入时间点并对树状数组对应区间进行维护 
void add(int x){
    if (s.size()==0) {s.insert(x);return;}
    auto p=s.lower_bound(x);
    if (p==s.begin()){
        int t=(*p)-x;
        Add(t,t);
    } else if (p==s.end()){
        int t=x-(*(prev(p)));
        Add(t,t);
    } else {
        int s=(*p)-x,t=x-(*(prev(p)));
        Add(s,s);Add(t,t);
        Add(s+t,-s-t);
    }
    s.insert(x);
}
//在set里删除时间点并对树状数组对应区间进行维护 
void del(int x){
    auto p=s.find(x);
    if (s.size()==1) {s.erase(p);return;}
    if (p==s.begin()){
        int t=(*(next(p)))-x;
        Add(t,-t);
    } else if (p==prev(s.end())){
        int t=x-(*(prev(p)));
        Add(t,-t);
    } else {
        int s=(*(next(p)))-x;
        int t=x-(*(prev(p)));
        Add(t,-t);
        Add(s,-s);
        Add(t+s,t+s);
    }
    s.erase(p);
}
//初始化 
void init()
{
    memset(a,0,sizeof(a));
    memset(b,0,sizeof(b));
    for (int i=1;i<=n+1;i++) h[i].clear();
    s.clear(); ans = 0;
}
int main()
{
    int kase = 0;
    scanf("%d", &t);
    while (t--) {
        scanf("%d",&n); init();
        for (int i=1;i<=n;i++) scanf("%lld%lld%lld",&E[i],&L[i],&C[i]);
        scanf("%d",&m);
        while (m--){
            int t,l,r;
            scanf("%d%d%d",&t,&l,&r);
            h[l].pb(t); h[r+1].pb(-t);
        }
        for (int i=1;i<=n+1;i++){
            for (auto x:h[i]){
                if (x>0) add(x); else del(-x);
            }
            if (!s.size()) continue;
            ans+=min(C[i],E[i]+(*s.begin())*L[i]); //计算初始情况 
            if (L[i]==0) continue;
            d=C[i]/L[i];
            ans+=qry2(d)*L[i]+(s.size()-1-qry1(d))*C[i];
        }
        printf("Case #%d: %lld\n", ++kase, ans);
    }
    return 0;
}
View Code

 附:迭代器的下一个或上一个分别用next()和prev()获取

 这里用set是因为它插入删除效率比用其他序列容器高

posted @ 2019-08-12 11:15  sparkyen  阅读(337)  评论(0编辑  收藏  举报