一、题目大意
给我们一个长度为n数列,k次查询,每次查询出数列中相邻子元素之和的绝对值最接近 t 的区间,输出区间和的绝对值,输出区间起点left和区间终点right(闭区间)
二、解题思路
我们要求的是总和最接近t的子区间,那么就认为是从 [1,n] 的区间内,去掉前左边的一部分,去掉右边的一部分,使得最终的子区间求和最接近t。
假设去掉的左半部分为[1,left],去掉的右半部分为[right,N],那么[left+1,right-1]为有效序列。
但是暴力枚举所有的left和right一定会超时
我们可以事先打个表计算出从数组从第n位到第i位的后缀和suffix[i],然后把后缀和数组排序,这样只需要根据每次的left利用二分查找找到最优的right,让left从0开始循环到n计算一次,就可以得到最优解,复杂性O(nlogn)可行
这个根据left去找right的过程,其实也很简单,假设 [1,left]区间的和为L,[right,N]区间的和为R,所有数组元素的总和为sum 那么本题目就是要使得 sum - L - R 接近t,那么我们设 sum - L - R = t,
然后求出 R = sum - L - t,这样只需要从后缀和数组里去找到最接近 sum - L - t 的值即可
需要注意的是,如果 left + 1 = right,那么代表有效序列为空,这种情况要排除;还需要考虑下当left=0,right=1时,有效序列也为空,也需要排除。
但是lower_bound找到的是第一个大于 sum - L - t 的值,所以要找最接近的,最好是把相邻的几个位置都判断下,然后又考虑到相邻的位置可能是 left+1和right重合,设lower_bound找到的位置为idx,那么就直接把二分定位的位置idx,idx-1,idx-2,idx+1,idx+2都判断下,即可解题
然后本题目说的是绝对值接近t,那么存在一种子区间接近-t的情况,这种情况的话,二分查找到right会小于left,例如N=10,right=6,left=8,那么有效序列为[1,10]-[6,10]-[1,8]=-[6,8],那其实就是 6到8的子序列的和取负值最接近t,那么这种情况需要更新结果为 right到left
其余的情况更新结果为 left+1到right-1即可
然后更新区间的同时,也要更新此时保存的区间和,abs(sum-L-R)
结果的初始值设置为 [1,N]和abs(sum)即可
三、代码
#include <iostream>
#include <algorithm>
using namespace std;
// first代表数值,second代表下标
typedef pair<int, int> P;
P suffixSum[100007]; // 后缀和
int n, k, t, arr[100007], sum, ansLeft, ansRight, ans;
void calc(P right, int left, int currentSum)
{
// [left+1,right.second-1]不能为空集
if (left + 1 == right.second || left == 0 && right.second == 1)
{
return;
}
// 有效的值=[1,n]-[1,left]-[right,n]=currentSum-[right.second,n]=currentSum-right.first
int validVal = abs(currentSum - right.first);
int distance = abs(t - validVal);
if (distance < abs(ans - t))
{
ans = validVal;
//[1,left]都被去掉了,[right.second,N]也被去掉了left>right.second
if (left >= right.second)
{
ansLeft = right.second;
ansRight = left;
}
else
{
ansLeft = left + 1;
ansRight = right.second - 1;
}
}
}
void solve()
{
//[left+1,right.second-1]为有效范围
int currentSum = sum, idx = 0;
for (int left = 0; left <= n; left++)
{
currentSum -= arr[left];
idx = lower_bound(suffixSum, suffixSum + n + 1, P(currentSum - t, -1)) - suffixSum;
P right = suffixSum[idx];
// 二分定位点的当前,前一个,前一个再前一个,后一个,后一个再后一个,这五个点需要考虑
// 当前点
calc(right, left, currentSum);
if (idx - 1 >= 0)
{
right = suffixSum[idx - 1];
calc(right, left, currentSum);
}
if (idx - 2 >= 0)
{
right = suffixSum[idx - 2];
calc(right, left, currentSum);
}
if (idx + 1 < n)
{
right = suffixSum[idx + 1];
calc(right, left, currentSum);
}
if (idx + 2 < n)
{
right = suffixSum[idx + 2];
calc(right, left, currentSum);
}
}
}
void inputAndSolve()
{
sum = 0, arr[0] = 0;
for (int i = 1; i <= n; i++)
{
scanf("%d", &arr[i]);
sum += arr[i];
}
for (int i = n; i >= 1; i--)
{
suffixSum[i].second = i;
suffixSum[i].first = arr[i];
if (i != n)
{
suffixSum[i].first += suffixSum[i + 1].first;
}
}
suffixSum[0].second = -1;
suffixSum[0].first = 0x3f3f3f3f;
sort(suffixSum, suffixSum + n + 1);
for (int i = 1; i <= k; i++)
{
scanf("%d", &t);
ansLeft = 1, ansRight = n, ans = abs(sum);
solve();
printf("%d %d %d\n", ans, ansLeft, ansRight);
}
}
int main()
{
while (true)
{
scanf("%d%d", &n, &k);
if (n == 0 && k == 0)
{
break;
}
inputAndSolve();
}
return 0;
}