题外话
- 差一题就能拿一等奖了,有点残念
- 当时试过了线段树/树状数组/优先队列写法,但是都没过
- 这题网上我没找到题解,估计也只能用单调队列实现吧(猜的)
难度
− 11 249 -\frac{11}{249} −24911
题意
N N N 堆礼物围成一个环,顺时针编号依次从 1 到 N N N,每堆礼物个数 A i A_i Ai
现在你要选择连续的一堆礼物,礼物个数至少是 M M M个。
选择礼物的花费为:这连续一堆礼物中数量最大值的一堆的数量 减去 这堆礼物中数量最小值的这堆的数量
正式表达:给定 1 ≤ i ≤ j ≤ N 1\le i\le j\le N 1≤i≤j≤N,使得 ∑ k = i j A k ≥ M \sum_{k=i}^jA_k\ge M ∑k=ijAk≥M
求出 min { max j k = i A k − min j k = i A k } \min\{\underset{k=i}{\overset{j}{\max}}A_k-\underset{k=i}{\overset{j}{\min}}A_k\} min{
k=imaxjAk−k=iminjAk}
数据范围
1 ≤ N ≤ 1 0 6 1\le N\le 10^6 1≤N≤106
A i ≤ 1 0 12 A_i\le 10^{12} Ai≤1012
思路
- 易得,把 [ 1 , N ] [1,N] [1,N]的环 拓展成 [ 1 , 2 N ] [1,2N] [1,2N]的链之后,可以通过尺取来求出所有满足条件的 i 、 j i、j i、j
- 这时候,题目变成了:求下标范围为 [ i , j ] [i,j] [i,j] 中的最小值和最大值
当时很单纯的我以为就线段树随便搞搞就行了,但是发现时间 O ( N log N ) O(N\log N) O(NlogN)可能是够的,但是内存 128 M 128M 128M 怎么都压不下来,导致一直运行错误,现在才发现其实就是 M L E MLE MLE了。 - 普通的线段树或者刚刚学的ST表的内存需求是 O ( N log N ) O(N\log N) O(NlogN)
(当然线段树可能因为我用的板子很挫),最普通的 O ( N ) O(N) O(N) 内存 2 e 6 ∗ 8 / 1024 / 1024 = 15 M 2e6*8/1024/1024=15M 2e6∗8/1024/1024=15M了,带一些常数的算法都会很难通过。。
单调队列就出现了。
-
这玩意儿可以在滑动窗口的情况下,快速算出这一段窗口的最值。有多呢?
-
滑窗是 O ( N ) O(N) O(N)的,每一步滑窗,摊还时间复杂度:处理 O ( 1 ) O(1) O(1),查询 O ( 1 ) O(1) O(1)。
-
综合时间复杂度:严格 O ( N ) O(N) O(N),空间复杂度: O ( N ) O(N) O(N)
-
怎么用滑窗实现 求范围内最值 呢?
- 可以使用双端队列
deque<type>
,也可以使用一个简单的数组模拟。(吉首这题用STL卡常,建议使用数组模拟)。
- 可以使用双端队列
单调队列的操作实现
【以求最小值为例】
-
首先理解这句话:“单调队列里面存储的元素是原数组的下标”
-
添加元素
- 窗口向右,会添加新的元素。
- 如果新的数字比队列尾更大,那肯定不要它;反过来,如果队列的尾比新的数字还要大,那么队列尾就去掉,直到队列空掉或者遇到一个比新数字还小的数字。
- 接下来,把新的数字的下标加进队列尾。
-
删除元素
- 因为是滑窗,肯定窗口左端有些值会离开窗口,因此我们希望删掉它。
- 但是不能这样考虑。我们考虑:窗口左端点为 s t st st,我们需要删掉所有在窗口左端点还左边的非法元素。
- 如果队列头储存的元素(即下标)小于 s t st st(即窗口左端点),那么他们是非法元素,丢掉他们。
-
查询窗口内元素的最值:
- 队列头储存的下标所指向的元素即为最值。
-
为什么要这么处理?
- 其实可以这样快速理解:双端队列的头存储目前的最值的下标。
- 但是由于窗口滑动,窗口中的其他值也有可能成为某段区间的最值。
- 我们把其他可能的元素按顺序从头到尾进行了排列,并且因为添加元素时附带删掉了一些不可能成为答案的元素。
- 另外,删除元素的时机就是,队列头也就是目前的最值已经过期(在窗口外了)然后再删掉。
这样,这题其实已经完成了。细节代码处理见下吧。
核心代码
/*
_ __ __ _ _
| | \ \ / / | | (_)
| |__ _ _ \ V /__ _ _ __ | | ___ _
| '_ \| | | | \ // _` | '_ \| | / _ \ |
| |_) | |_| | | | (_| | | | | |___| __/ |
|_.__/ \__, | \_/\__,_|_| |_\_____/\___|_|
__/ |
|___/
*/
const int MAX = 2e6+50;
const ll INF = 2e18;
ll aa[MAX];
ll Q[MAX],R[MAX]; /// Q:最小值双端队列 R:最大值双端队列
int main(){
int T;scanf("%d",&T);
while(T--){
int n;ll m;
scanf("%d%lld",&n,&m);
for(int i = 1;i <= n;++i)scanf("%lld",&aa[i]),aa[i+n]=aa[i];
int st = 1,ed = 1; /// 窗口端点
int st1 = 1,ed1 = 1,st2 = 1,ed2 = 1; /// 双端队列的端点
ll now = aa[1];
Q[st1] = 1;R[st1] = 1;
ll ans = INF;
while(1){
while(ed + 1 <= n * 2 && now < m && ed - st + 1 + 1 <= n){
/// 添加元素处理
ed++;
now += aa[ed];
while(ed1 - st1 >= 0 && aa[Q[ed1]] >= aa[ed])ed1--;
while(ed2 - st2 >= 0 && aa[R[ed2]] <= aa[ed])ed2--;
Q[++ed1] = ed;
R[++ed2] = ed;
}
if(now < m)break;
ans = min(ans,aa[R[st2]] - aa[Q[st1]]); /// 查询
now -= aa[st];
st++;
while(ed1 - st1 >= 0 && Q[st1] < st)st1++; /// 删除元素处理
while(ed2 - st2 >= 0 && R[st2] < st)st2++;
}
if(ans == INF)printf("-1\n");
else printf("%lld\n",ans);
}
return 0;
}
补充
- 可能不用 S T L STL STL 的代码对于初次阅读的体验比较差,这里补一份只用 S T L STL STL 的题
- 上一篇的用 S T ST ST 表实现 R M Q RMQ RMQ 的题目,用单调队列再次实现一下:质量检测 | 洛谷 P2251
单调队列代码如下:
可以看到,直接 S T L STL STL 的代码量并不多,但是应该思考下为什么这样。
/*
_ __ __ _ _
| | \ \ / / | | (_)
| |__ _ _ \ V /__ _ _ __ | | ___ _
| '_ \| | | | \ // _` | '_ \| | / _ \ |
| |_) | |_| | | | (_| | | | | |___| __/ |
|_.__/ \__, | \_/\__,_|_| |_\_____/\___|_|
__/ |
|___/
*/
const int MAX = 2e6+50;
int aa[MAX];
deque<int>Q;
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;++i)scanf("%d",&aa[i]);
for(int i = 1;i <= n;++i){
while(Q.size() && i-Q.front() >= m)Q.pop_front();
while(Q.size() && aa[Q.back()] >= aa[i])Q.pop_back();
Q.push_back(i);
if(i >= m)printf("%d\n",aa[Q.front()]);
}
return 0;
}