POJ 2566 Bound Found 尺取法+二分

一、题目大意

给我们一个长度为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;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值