【接近】——【单调队列】NKOJ _P3545
问题描述
对于一个数字序列A,并且有若干询问。对于每个询问,要求求出一段在序列A中非空 的连续段使得这一段数字的总和的绝对值尽量接近P。
输入格式
第一行2个数N、T,表示序列的长度和询问的个数。
接下来一行N个整数,表示A序列。 接下来T行,每行一个数P表示询问。
样例输入 | 样例输出 |
---|---|
5 1 -10 -5 0 5 10 3 | 5 2 2 |
6 2 -2 5 3 0 -3 -4 1 6 | 1 1 6 6 1 3 |
【数据范围】
30%的数据 1<=N<=1,000。
60%的数据 1<=N<=10,000。
100%的数据 1<=N<=100,000,A 序列中数字绝对值<=10,000,T<=100,询问的 数字<=109
对于这道题,我最开始想的是预处理出所有的连续的总和,对于每个询问二分查找,但显然预处理就是O(N2)的时间复杂度,gg。
观察题目,这道题最特殊的一点就是 ”使得这一段数字的总和的绝对值尽量接近P。“
一般处理区间和都是用的前缀和的形式,及Sum[i,j]_Sum[j]-Sum[i-1] 但这道题让我们求的是abs(Sum[i,j]_Sum[j]-Sum[i-1]) 再联想数据范围显然我们需要一个不多于O(NT)的算法,每一次操作O(N),于是很自然地想到了单调队列。
但是前缀和数组怎么维护单调性呢?又注意到此题求的是一段区间总和的绝对值 所以我们直接把前缀和数组进行排序,因为对于两个前缀和差——Sum[i],Sum[j],它们的相对大小关系对答案并无影响,只要它们的差的绝对值足够接近P即可。
所以我们直接对Sum数组从大到小排序,然后从小到大枚举i,当我们讨论到i时,对于任意一对(i,j)如果Sum[j]-Sum[i]>=P,那么我们记下j与i对答的贡献,然后删除掉j(因为Sum数组从大到小排序,所以**i之后的Sum[k]数组(i>K)与j的差值Sum[j]-Sum[k]>=Sum[j]-Sum[i])**然后只用讨论一次差值小于P的答案即可。因为每个Sum数组只会进队一次,所以时间复杂度为O(N),总时间复杂度O(NT).
详见代码:
#include<stdio.h>
#include<bits/stdc++.h>
#define H 100005
using namespace std;
struct node{int v,id;}Sum[H];
int N,T,P;
bool cmp(node a,node b){
if(a.v==b.v)return a.id<b.id;
return a.v>b.v;
}
void Work(){
int L,R,ans,Cha;
L=R=Cha=1234567890;
deque<int>Q;
for(int i=0;i<=N;i++){
//
while(!Q.empty()&&Sum[Q.front()].v-Sum[i].v>=P){
int k=Q.front();
int A=min(Sum[k].id,Sum[i].id);
int B=max(Sum[k].id,Sum[i].id);
int C=Sum[k].v-Sum[i].v;
if(C-P<=Cha){
if(C-P==Cha){
if(A==L)R=min(R,B);
else if(A<L)L=A,R=B;
}
else ans=C,L=A,R=B;
Cha=C-P;
}
Q.pop_front();
}//讨论差大于等于P的情况
Q.push_back(i);
if(Q.size()!=1){
int k=Q.front();
int A=min(Sum[k].id,Sum[i].id);
int B=max(Sum[k].id,Sum[i].id);
int C=Sum[k].v-Sum[i].v;
if(P-C<=Cha){
if(P-C==Cha){
if(A==L)R=min(R,B);
else if(A<L)L=A,R=B;
}
else ans=C,L=A,R=B;
Cha=P-C;
}
}//讨论差值小于等于P的情况
}
printf("%d %d %d\n",ans,L+1,R);
}
int main(){
scanf("%d%d",&N,&T);
for(int i=1;i<=N;i++){
int x;scanf("%d",&x);
Sum[i].v=Sum[i-1].v+x;
Sum[i].id=i;
}
sort(Sum,Sum+1+N,cmp);//数组一定要从[0,N]排序,因为会有从[1,i]的连续区间的情况
while(T--){
scanf("%d",&P);
Work();
}
}