“栈”这个数据结构,想必大家都很熟悉,它满足“后进先出”的性质。
squee_spoon做题的时候,有一种强迫症,即他总是先做最近的题目。例如,squee_spoon还没有解决问题a的时候,看到了问题b,那么他就会暂时放下a,先解决b。我们可以认为,squee_spoon做题总是严格满足栈的性质。然而squee_spoon太弱了,总会有一大堆未解决的问题,在问题积压数达到m的时候,他将难以维护他的“补题栈”,此时他不再接受新的问题(换句话说,他在任意时刻,最多存在m个未解决的问题)。
现在,squee_spoon拿到了一份题单,上面按顺序列有n道题的题号p1~pn,求squee_spoon按顺序读题号的情况下,可能的字典序最小的做题顺序。
题目要求字典序最小的做题顺序,所以很容易有个朴素的想法,首先让做的第一道题题号尽可能小,然后让第二道题题号尽可能小......这样得到的顺序肯定是最小的。
顺着这个思路往下想,有哪些题可以成为第一道呢,显然是前m题都可以,因为“补题栈”的大小是m,你就可以一直读到那一题,然后做掉,问题就转化为了“求数组p在区间[1,m]的最小值”。同理,哪些题可以成为当前能做的题呢,自然是往后的某个区间(区间长度取决于当前栈被占用了多少空间),以及当前的栈顶。
每次要做的题就是能做的题里面题号最小的那题,也就是相应区间的最小值和栈顶的值中更小的那个。于是这题的姿势就是快速求区间最小值辣!比如线段树(O(nlogn)),spare table(O(nlogn)),分块(O(n^1.5))都是可以过的。下面给一个线段树的解法:
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<iostream>
#include<stack>
using namespace std;
#define ll long long
int a[100010];
int seg_tree[100010<<2];
int tl[100010<<2];
int tr[100010<<2];
void build_tree(int rt,int l,int r){
tl[rt] = l; tr[rt] = r;
if(l==r){
seg_tree[rt] = a[l];
return;
}
int mid = (l+r)>>1;
build_tree(rt<<1,l,mid);
build_tree((rt<<1)|1,mid+1,r);
seg_tree[rt] = min(seg_tree[rt<<1],seg_tree[(rt<<1)|1]);
}
int query(int rt,int l,int r){
if(l==tl[rt] && r==tr[rt]){
return seg_tree[rt];
}
int mid = (tl[rt] + tr[rt])>>1;
if(r<=mid){
return query(rt<<1,l,r);
}else{
if(l>mid){
return query((rt<<1)|1,l,r);
}else{
int ql = query(rt<<1,l,mid);
int qr = query((rt<<1)|1,mid+1,r);
return min(ql,qr);
}
}
}
stack<int> sta;
bool first = 1;
void Print(int x){
if(first){
first=0;
}else{
printf(" ");
}
printf("%d",x);
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
build_tree(1,1,n);
for(int i=1;i<=n;i++){
int rangeL=i;
int rangeR=i+(m-sta.size()-1);
rangeR=min(rangeR,n);
int MIN = query(1,rangeL,rangeR);
if(sta.size() && sta.top() < MIN){
Print(sta.top());
sta.pop();
i--;
}else{
Print(MIN);
while(a[i]!=MIN){
sta.push(a[i]);
i++;
}
}
}
while(sta.size()){
Print(sta.top());
sta.pop();
}
printf("\n");
return 0;
}