题意:对一个栈,有三种操作:pop,push,peak(查询当前栈顶元素的值)。但在此题中,每个操作都有时间戳。操作按顺序给出,但如果后面的操作的时间戳小,会把之前,大于该时间戳的所有操作全部取消,即恢复到该时间戳时的状态,执行完该操作后,再把取消的操作重新执行。
但,peak操作不会重新执行,即,对于给定的peak操作,它也会把大于它的时间戳的操作取消,完成自己的查询操作。但,对于后来的小于该时间戳的操作,不会重新执行这个peak操作。(可以认为,这个打印操作只会执行一次。)
思路:只是简单的按照时间排序是行不通的。因为peak操作只执行一次,它对前后操作的依赖关系很大,只按时间排序后,会把依赖关系的信息破坏掉。我们只能按照给定的操作顺序去处理。
我们可以利用线段树去解决这道题。
首先,因为时间的范围很大,而总的操作数很少,我们首先离散化时间。
然后:对于每个push操作,我们对对应的时间点+1,pop操作,我们对对应的时间点-1;
而peak操作,设对应的时间点为t,在这种情况下,就等价于,找出一个最大的时间点t',使 [ t', t)区间的和大于0(大于0保证栈不为空,最大的时间点保证是在当前时间点下,栈顶的元素。)
那,如何去求出这个最大的时间点t'呢?或者说,线段树该保存怎样的信息?
每个节点对应区间的区间和,这个是一定要有的。但我们如果只保存区间和,会发现,如果多次求不同区间的和,实际上会大大增加复杂度。
因为我们要求的是区间[t',t)的起点位置,而这个区间和,也相当于以t'为起点的后缀和,所以我们还要维护每个节点对应区间的最大后缀和。
最大后缀和的维护公式:
设节点为o,左儿子为ls,右儿子为rs,区间和为sum,最大后缀和为rsum,则 rum[o] = max(rsum[r], rsum[l] + sum[r]),即最大后缀和的起点要么位于右儿子对应区间,要么位于左儿子对应区间。
我们再回头来看如何求出t'。
我们可以利用线段树,将[1,t)分成log(t)个不相交的区间(分的方法就是递归调用,直到完全覆盖),从右到左,每个区间分开考虑,同时有一个变量v保存考虑过的区间的和。对于每个区间o:
1.如果v+ rsum[0] <= 0, 说明在这个区间内,无法找到t',那么 累加v = v + sum[o],继续向左考虑。
2.如果v + rsum[o] > 0,说明在这个区间内,存在某个时间点t',使[t',t)区间和大于0。由步骤1可知,这个区间右面的所有区间都不满足,而该区间满足,所以t'一定是最大的。
然后递归考虑这个区间,直到找到准确的时间点为止。
3.如果已经到了最左端仍然没满足要求,说明不存在。
这样,处理这个题的整体思路就很清晰了。
代码如下:
#include
#include
#include
using namespace std;
struct info{
int s;
int x;
int t;
};
const int MAX = 50010;
int N,x,t;
char str[20];
info a[MAX];
int b[MAX];
int ans;
struct interval{
int left, right;
int sum, rsum;
};
struct SegmentTree{
static const int MAX = 50010;
interval node[MAX<<3];
#define ls(o) (o<<1)
#define rs(o) (o<<1|1)
void build(int l, int r, int o){
node[o].left = l;
node[o].right = r;
node[o].sum = node[o].rsum = 0;
if(l == r) return;
int mid = (l + r)>>1;
build(l,mid,ls(o));
build(mid+1,r,rs(o));
}
void pushup(int o){
node[o].sum = node[rs(o)].sum + node[ls(o)].sum;
node[o].rsum = max(node[rs(o)].rsum,node[ls(o)].rsum + node[rs(o)].sum);
}
void update(int value,int pos,int o){
if(node[o].left == node[o].right &&
node[o].left == pos){
node[o].sum = node[o].rsum = value;
return ;
}
int mid = (node[o].left + node[o].right) >> 1;
if(pos <= mid) update(value,pos,ls(o));
else update(value,pos,rs(o));
pushup(o);
}
void getans(int v,int o){
if(node[o].left == node[o].right){
ans = b[node[o].left];
//printf("%d ",node[o].left);
return;
}
if(v + node[rs(o)].rsum > 0)
getans(v,rs(o));
else
getans(v + node[rs(o)].sum,ls(o));
}
void query(int &v,int L, int R, int o){
if(~ans) return;
if(L <= node[o].left && R >= node[o].right){
if(v + node[o].rsum <= 0) v += node[o].sum;
else getans(v,o);
return;
}
int mid = (node[o].left + node[o].right) >> 1;
if(R > mid) query(v,L,R,rs(o));
if(L <= mid) query(v,L,R,ls(o));
}
};
SegmentTree tree;
int main(void)
{
int cas = 1;
//freopen("input.txt","r",stdin);
while(scanf("%d", &N),N){
for(int i = 0 ; i < N; ++i){
scanf("%s",str);
if(str[1] == 'u'){
a[i].s = 1;
scanf("%d", &a[i].x);
}
else if(str[1] == 'o')
a[i].s = 2;
else
a[i].s = 3;
scanf("%d", &a[i].t);
b[i] = a[i].t;
}
sort(b,b + N);
for(int i = 0 ; i < N; ++i)
a[i].t = lower_bound(b, b + N, a[i].t) - b + 1;
memset(b,0,sizeof(b));
for(int i = 0 ; i < N; ++i)
if(a[i].s == 1)
b[a[i].t] = a[i].x;
// for(int i = 1; i <= N; ++i)
// printf("%d%c",b[i],i == N?'\n':' ');
printf("Case #%d:\n",cas++);
tree.build(1,N,1);
for(int i = 0 ; i < N; ++i){
if(a[i].s == 1)
tree.update(1,a[i].t,1);
else if(a[i].s == 2)
tree.update(-1,a[i].t,1);
else{
int v =0;
ans = -1;
tree.query(v,1,a[i].t,1);
printf("%d\n",ans);
}
}
}
return 0;
}
好的姿势:
1.利用引用,使递归调用查询的时候,能对考虑过的节点累加和。
2.递归调用的顺序决定了搜索的方向。在这个题中,我们要先求右子树,在求左子树,才能保证是从右向左的查找。
3.对于答案,我们可以直接初始化为不存在的答案,在输出结果时,不需要判断。
4.-1的妙用,~-1 = 0,代码中,利用~ans结束求解过程,妙!