4270. 【NOIP2015模拟10.27】魔道研究

2 篇文章 0 订阅
2 篇文章 0 订阅

Description

“我希望能使用更多的魔法。不对,是预定能使用啦。最终我要被大家称呼为大魔法使。为此我决定不惜一切努力。”
——《The Grimoire of Marisa》雾雨魔理沙
魔理沙一如既往地去帕秋莉的大图书馆去借魔导书(Grimoire) 来学习魔道。
最开始的时候,魔理沙只是一本一本地进行研究。然而在符卡战中,魔理沙还是战不过帕秋莉。
好在魔理沙对自己的借还和研究结果进行了记录,从而发现了那些魔导书的精妙之处。
帕秋莉的那些魔导书,每本都有一个类别编号ti 和威力大小pi。而想要获得最有威力的魔法,就必须同时研究一些魔导书。而研究的这些魔导书就必须要满足,类别编号为T 的书的本数小于等于T,并且总共的本数小于等于一个给定的数N。而研究这些魔导书之后习得的魔法的威力就是被研究的魔导书的威力之和。
为了击败帕秋莉,魔理沙想要利用自己发现的规律来获得最有威力的魔法。
她列出了计划中之后M 次的借还事件,并想要知道每个事件之后自己所能获得的魔法的最大威力。可她忙于魔法材料——蘑菇的收集,于是这个问题就交给你来解决了。

Input

输入文件grimoire.in。
第1 行2 个整数N,M,分别表示魔理沙能研究的魔导书本数的上限和她的借还事件数。
之后M 行,每行的形式为“op t p”(不含引号)。Op 为“BORROW” 或“RETURN”,分别表示借书和还书。T 为一个整数,表示这本书的类别编号。P为一个整数,表示这本书的威力大小。注意,还书时如果有多本书满足类别编号为t,威力大小为p,这表明这些书都是相同的,魔理沙会任选其中一本书还回去。如果你问我为何会有相同的书,多半因为这是魔导书吧。

Output

输出文件grimoire.out。
一共M 行,每行一个整数,即每个事件之后的最大威力。

Sample Input

5 10
BORROW 1 5811
BORROW 3 5032
RETURN 3 5032
BORROW 3 5550
BORROW 5 3486
RETURN 1 5811
RETURN 3 5550
BORROW 4 5116
BORROW 3 9563
BORROW 5 94

Sample Output

5811
10843
5811
11361
14847
9036
3486
8602
18165
18259

Data Constraint

对于5% 的数据,1 <= t,N,M <= 50。
对于10% 的数据,1 <= t,N,M <= 100。
对于30% 的数据,1 <= t,N,M<= 10 000。
另有30% 的数据,1 <= p <= 1 000。
对于100% 的数据,1 <= t,N,M <= 300 000,1<= p<= 1 000 000 000。
另外,总共有30% 的数据,满足没有“RETURN” 操作。这部分数据均匀分布。

思路

1、平衡树

平衡树支持查找树中第i大,还有招它的前驱,所以我们可以找到第i大,然后不停找前驱,这样就维护了每种系列的最大值。
然后维护包含所有序列树我们可以用动态规划(当然太麻烦,而且时间也没优化多少),所以也可以开个数组存,然后每次就一次快排,当然这样再一看数据规模,TLE,除非你的常数处理的非常好。所以我们可以再开一棵平衡树,然后和每个序列的平衡树对接,这样就比较优秀了。
而且如果用splay可以省内存,会的人都懂。。。
再jzoj这边有位同学splay跑了500多ms,而我1000多ms,orz~~~,所以平衡树还是很好用的,就是代码量大了点,300多行。当然看个人写法。

2对顶堆

(⊙o⊙)…这个博主我不会,据说差不多要成为时代的眼泪了,所以有兴趣的的同学请自行百度或谷歌。

3线段树

这个是用权值线段树,而且要动态开点,动态开点就是线段树动态开节点,也就是说我们不把那些空节点建起来,只保留那些非空的节点。
具体来说,假如说,一个用来区间求和的线段树,初始的时
候序列里所有元素都是 0, 每个节点就都是空的——全是 0。
我们就没有必要把这些空节点都建起来——最开始的时候线
段树就是空的。当我们进行修改操作时,才把这个修改所涉及的
点开出来。
比如,将 3 号位置变为 4。
我们就把根节点到 [3,3] 节点这条链建起来,并进行相应的
修改。
这样就算序列长度有 1e9 也可以线段树啦。
不过注意,动态开节点是不能堆式存储的。

权值线段树。
我们描述一个可重集合里面的元素,可以开一个以权值为下
标的数组,数组内容为对应权值的出现次数。这个数组称为权值
数组。
而用来维护权值数组的线段树,就是权值线段树。
然后,求某个权值在集合里的排名什么的,只要在线段树里
查查前缀和就知道了。

可权值范围有 1e9 怎么办?
不虚!我们可以动态开节点!
当然也可以结合离散化来更好地压缩空间和时间。

线段树二分
线段树本身就是一个二分的结构,我们没有必要外面
再套一重二分。我们可以直接在线段树上走。
比如我们现在站在线段树的某个节点 [l,r] 上,我们要求权值
在这个区间里的第 k 小元素。
假如 l = r,我们就可以直接报出答案了。
假如 l ̸= r。
我们可以先看一看这个点的左儿子——[l,mid]。假如 [l,mid]
中的元素个数 ≥ k,那第 k 小元素显然在 [l,mid] 中,我们走到
[l,mid],继续求解即可。
假如 [l,mid] 中的元素个数 < k,那么第 k 小元素显然在
[mid+1,r],也就是右儿子中。我们令 k:=k-[l,mid] 中的元素个数,
走到 [mid+1,r],继续求解即可。
这样复杂度是 O(nlogn) 的。
类似地,前 k 小元素的权值和,还有第 k 大,前 k 大之类的
问题都可以这么解决。

“我们有若干个可重集合,然后我们从第 i 个可重集合中拿前
i 大组成一个新的可重集合 S。我们的目的是动态维护 S 的前 n
大的和。”
显然,我们需要维护所有的小集合以及 S。
维护 maxt 棵权值线段树,第 i 棵线段树对应第 i 个小集合。
我们称这些线段树为小树。
另外维护一棵权值线段树,用来维护 S。我们称这棵线段树
为大树。
对于 BORROW 操作,我们就是在第 t 棵小树中加入一个元
素 p。这个可以通过线段树单点修改完成。
之后,我们要确定要不要将 p 加入 S 中。这个只要知道 p
在第 t 个集合中的排名是否 ≤ t 即可。
加入了 S 以后,我们还要将原来的第 t 大,即现在的第 t+1
大从 S 中删去。
对于 RETURN 操作,我们可以类似地,将 BORROW 操作
取反即可。
而动态维护 S 前 n 大的和自然也是小菜一碟了。

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<set>
#include<deque>
#include<map>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define M 300007
#define K 1000000007
#define N 100000
using namespace std;
typedef long long LL;
int i,j,k,l,tt,n,m,p,num;
LL ans,ans1,ans2;
int di[M];
struct node{
    int tot,l,r;
    LL sum;
}t[M*100];
char aa[7];


void dikda(int x,int l,int r,int z){
    if(l==r){
        ans1=l;
        return;
    }
    int mid=(l+r)/2;
    if(t[t[x].r].tot>=z)dikda(t[x].r,mid+1,r,z);
    else dikda(t[x].l,l,mid,z-t[t[x].r].tot);
}
void get(int x,int l,int r,int z){
    if(l==r){
        ans=ans+min(t[x].tot,z)*l;
        return;
    }   
    int mid=(l+r)/2;
    if(t[t[x].r].tot>=z)get(t[x].r,mid+1,r,z);
    else get(t[x].l,l,mid,z-t[t[x].r].tot),ans+=t[t[x].r].sum;
}
int main(){
    freopen("grimoire.in","r",stdin);
    freopen("grimoire.out","w",stdout);
    scanf("%d%d",&n,&m);
    fo(i,1,m){
        scanf("%s%d%d",aa,&tt,&p);
        if(aa[0]=='B'){
            if(t[di[tt]].tot>=tt){
                dikda(di[tt],1,K,tt);
            }
            else ans1=0;
            if(p>=ans1){
                insert(di[0],1,K,p);
                if(ans1>0)delet(di[0],1,K,ans1);
            }
            insert(di[tt],1,K,p);
        }
        else{
            if(t[di[tt]].tot>tt){
                dikda(di[tt],1,K,tt+1);
            }
            else ans1=0;
            if(p>=ans1){
                delet(di[0],1,K,p);
                if(ans1>0)insert(di[0],1,K,ans1);
            }
            delet(di[tt],1,K,p);
        }
        ans=0;
        get(di[0],1,K,n);
        printf("%lld\n",ans);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值