题目大意
给定一组包含n个整数的数列和k个询问,求取一个子串,使得该***连续子串***的***和的绝对值***最接近t
注:暴力求解必定超时
解题思路
- 由于要求取一个连续子串的部分和,这让我们想到了记录前缀和sum的方式来在O(1)内求得任意子串的和。
- 对于子串问题的处理,为了避免暴力双重循环,我们往往使用尺取法(双指针法),就像是在字符串匹配问题时的那样。当然,其他的方法(如DP)也经常用来处理子串问题,最经典的有最长上升子序列
- 在使用双指针法时,要求序列具有一定的单调性。也正是因为序列具有单调性,才是我们能固定左端点,移动右端点,到达一定情况时,则可以移动左端点,从未有过任何回溯,从而形成节约。
- 这里我们显然不能将原序列sort后进行取尺操作,这会丧失原序列的位置信息,不满足子串条件。
- 结合前缀和,我们可以用结构体Node记录前缀和sum和位置idx,然后进行sort,然后对其进行取尺操作。结构体数组sum保证了递增性,我们可以不重复地尽可能地获取到最接近t的元素和。
坑点
- 由于sum单调递增,所以s=sum[j].s-sum[i].s求得的必为正数,由于sort后idx被打乱,故事实上可能是前减后、也可能是后减前,综合来看s就是子串部分和的绝对值。
- 部分和sum[j].s-sum[i].s对应的是原始索引是l=sum[j].i和r=sum[i].i,不是j和i。
- 部分和sum[j].s-sum[i].s是原始序列a[i+1]+…+a[j],注意起点是i+1。
- 在双指针移动中,可能导致i、j重合,这是需要人为地将j向后移动。注意,鉴于这一点,循环判断条件要写为i<=j && j<=n 而非i<j && j<=n。
- 当部分和s等于t时,此时已最接近t,循环可以直接退出。当然,程序中可能有多个s等于t的时候,但题目中对此没有要求,我们简单地处理为“一旦遇到这种情况就 结束循环”。
AC代码
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#include <cstdio>
using namespace std;
struct Node
{
int s;
int i;
friend bool operator<(const Node & a,const Node & b){return a.s<b.s;}
};
const int maxn=1e5+10;
const int INF=1e9+10;
int n,k;
Node sum[maxn];
int main()
{
while(~scanf("%d %d",&n,&k) && n && k)
{
sum[0].s=0,sum[0].i=0;
for(int i=1;i<=n;i++)
{
int a;scanf("%d",&a);
sum[i].s=sum[i-1].s+a;
sum[i].i=i;
}
sort(sum,sum+n+1);
while(k--)
{
int t;scanf("%d",&t);
int total,min_dif=INF;
int l,r;
int i=0,j=1;
while(i<=j && j<=n)
{
int s=sum[j].s-sum[i].s;//以i为起点的序列和,随j增大而增大!!
int dif=abs(s-t);
if(dif<min_dif)
{
min_dif=dif,l=sum[i].i,r=sum[j].i,total=s;//注意l、r是原序列的索引!!!不是这里的i、j!
}
if(s<t) j++;//还可以更接近
else if(s>t) i++;//已经不可能更接近了
else break;//已经一致了
if(i==j) j++;//用于避免i、j重合
}
if(l>r) swap(l,r);
printf("%d %d %d\n",total,l+1,r);
}
}
return 0;
}