[TK] Blocks 单调栈

题目描述

给出 \(N\) 个正整数 \(a[1..N]\) ,再给出一个正整数 \(k\) ,现在可以进行如下操作:每次选择一个大于 \(k\) 的正整数 \(a[i]\) ,将 \(a[i]\) 减去 \(1\) ,选择 \(a[i-1]\)\(a[i+1]\) 中的一个加上 \(1\) 。经过若干次操作后,问最大能够选出多长的一个连续子序列,使得这个子序列的每个数都不小于 \(k\) .

初步分析

其实这个题与下述题目等效:

给出 \(N\) 个正整数 ( \(a[i]-k\) ) 组成的序列,序列前 \(i\) 项和记为 \(s[i]\) ,求满足 \(s[i]> s[j]\)\(i> j\)\(j-i\) 的最大值.

我们来思考一下为什么两个问题会相等.

容易想到,只有平均数大于等于 \(k\) 的子序列经过操作后能够使其每个数都不小于 \(k\),等效于在 \(a[i]-k\) 构成的序列中求最大的和为非负的区间. 而区间和又等于 \(s[j]-s[i-1]\) ,即 \(s[j]-s[i-1]> 0\),整理得 \(s[j]> s[i-1]\),此时区间长度 \(j-i+1\) 即为答案.

因此,我们现在的问题就转化为,如何求这样的区间最大值.

区间最值

首先放出引理:\(s[i_{1}] < s[j],\ s[i_{2}]< s[j],\ i_{1} < i_{2} \le j\),那么 \(i_{2}\) 一定不是最优解.

根据此引理,我们定义有可能为最优解的条件:符合条件,且其左方没有符合条件的数.

所以,如果我们从左边遍历,遇到第一个符合条件的数,那么符合该条件的数只有它可能是最优解,所以我们直接改到下一个条件遍历.

总结出如下遍历步骤:

  • 设定初始条件为 \(s[j]=0\).
  • 从左向右尝试放入前缀和.
  • 当前前缀和满足 \(s[i] > s[j]\),放入,并将 \(s[i]\) 作为新的条件.
  • 不满足,忽略.

前缀和有正有负,那么为什么初始条件是 \(s[j]=0\) 呢. 这是为了防止将正的前缀和放进去. 因为实际上正的前缀和可以直接作为答案区间,不需要进行这步流程,我们把它放进去反而会跑错.

注意到此步骤可以用栈实现:

s.push(0);
for(int i=1;i<=n;++i){
	sum[i]=sum[i-1]+a[i]-k;
	if(sum[s.top()]>sum[id]){
		s.push(id);
	}
}

我们通过上述步骤,将栈内的内容维护成了一个严格单调递减的序列的下标,这样的话,只要满足 \(i < j\),就有 \(s[i]<s[j]\).

接下来我们来考虑如何求最大区间.

由上述引理,我们倒序遍历全部前缀和 \(s[i]\) , 设栈顶前缀和为 \(s[top]\),那么我们可以遍历到满足 \(s[top]<s[i]\) 的最后一个数,此时区间长度即为 \(i-top\).

代码实现

stack<long long> s;
long long a[1000001],sum[1000001];
void push(int id){
	if(sum[s.top()]>sum[id]){
		s.push(id);
	}
}
long long calc(int n){
	long long ans=0;
	for(int i=n;i>=1;--i){
		while(!s.empty()&&sum[i]-sum[s.top()]>=0){
			ans=max(ans,i-s.top());
			s.pop();
		}
	}
	return ans;
}
int main(){
	int n,m,k;
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		cin>>a[i];
	}
	for(int i=1;i<=m;++i){
		cin>>k;
		s.push(0);
		for(int i=1;i<=n;++i){
			sum[i]=sum[i-1]+a[i]-k;
			push(i);
		}
		s.push(n);
		cout<<calc(n)<<" ";
		while(!s.empty()){
			s.pop();
		}
	}
}
posted @ 2024-02-22 11:55  HaneDaniko  阅读(12)  评论(0编辑  收藏  举报