OJ: poj

题目 ID: 2566

标签: 双指针

日期: 2025-11-26 20:04

https://vjudge.net/problem/POJ-2566#author=DeepSeek_zh

这是一道非常经典的算法题目(对应 POJ 2566 - Bound Found)。

针对你的核心疑问:“数组中有负数,打破了普通滑动窗口的单调性,如何使用双指针?

答案是:不要直接在原数组上跑双指针,而是在“排序后的前缀和数组”上跑双指针。

核心解题思路

1. 转换问题:前缀和 (Prefix Sum)

原数组 AA 中含有负数,导致区间伸缩时和的大小变化不可预测。但是,任何一个子区间 [l,r][l, r] 的和都可以表示为两个前缀和的差:

Sum(l,r)=P[r]P[l1]Sum(l, r) = P[r] - P[l-1]
其中 P[i]P[i] 是前 ii 个元素的和,P[0]=0P[0] = 0

题目要求找到 Sum(l,r)t|Sum(l, r)| \approx t,等价于找到两个前缀和 P[x]P[x]P[y]P[y],使得:

P[x]P[y]t|P[x] - P[y]| \approx t

问题就转变成了在一个单调数组上,查找最ABCA-B \approx C的数值.

2. 制造单调性:排序 (Sorting)

虽然原数组有负数导致前缀和数组 PP 也是乱序的,但如果我们把 PP 数组按照数值大小从小到大排序,情况就变了。

设排序后的前缀和数组为 SS。此时 SS 是单调递增的。 对于 SS 中的任意两个元素 S[right]S[right]S[left]S[left](其中 right>leftright > left),它们的差 diff=S[right]S[left]diff = S[right] - S[left] 必定是 非负的

这就构造出了双指针所需的单调性:

  • 如果 diff<tdiff < t:我们需要更大的差值,所以 右指针 right 向右移(找一个更大的数减去当前的左边)。
  • 如果 diff>tdiff > t:差值太大了,我们需要更小的差值,所以 左指针 left 向右移(找一个更大的被减数,缩短两者距离)。

3. 算法流程

  1. 计算前缀和:生成包含 (N+1)(N+1) 个元素的数组(包含初始的0),存储结构为 {value, original_index}
  2. 排序:按照 value 对前缀和数组进行升序排序。
  3. 双指针扫描
    • 初始化 left = 0, right = 1
    • 计算 diff = S[right].val - S[left].val
    • 检查 diff 与目标 t 的接近程度,更新全局最优解(记录最小差值、以及对应的原始下标)。
    • 移动指针
      • diff < tright++
      • diff > tleft++
      • diff == t:直接找到完美解,跳出。
      • 注意:若 left == right,强制 right++ 以保持区间非空。

4. 还原下标

排序打乱了原始下标,但我们在结构体里保存了 original_index。 假设找到的最优对是 S[left]S[left]S[right]S[right],它们的原始下标分别是 id1id_1id2id_2

  • 区间起点 l=min(id1,id2)+1l = \min(id_1, id_2) + 1
  • 区间终点 u=max(id1,id2)u = \max(id_1, id_2)

C++ 代码实现

cpp
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;
typedef long long ll;

const int N = 100005; // 题目范围 1e5,稍微开大一点
const ll INF = 1e18;  // 足够大的初始值
int n, k;

struct Node {
    ll val;
    int id;
    // 重载小于号,sort 时自动调用
    bool operator<(const Node &other) const {
        return val < other.val;
    }
} p[N]; // 全局大数组

ll _abs(ll t) { return t < 0 ? -t : t;}

void work(ll t) {
    // 初始化答案状态
    ll min_gap = INF;
    ll ans_sum = 0;
    int ans_l = 0, ans_r = 0;

    // 双指针初始化
    int l = 0, r = 1;

    // 4. 双指针扫描单调数组
    // 只要 r 没越界,并且 l 也没越界 (实际上 r 会先触顶)
    while (r <= n && l <= n) {
        ll diff = p[r].val - p[l].val;
        ll gap = _abs(diff - t);

        // 尝试更新最优解
        if (gap < min_gap) {
            min_gap = gap;
            ans_sum = diff;
            ans_l = p[l].id;
            ans_r = p[r].id;
        }

        // 剪枝:如果已经完美匹配,无需继续
        if (gap == 0) break;

        // 移动策略
        if (diff < t) r++;      // 差小了 -> 扩大右边界
        else l++;               // 差大了 -> 扩大左边界

        // 撞车保护:区间长度至少为 1
        if (l == r) r++;
    }

    // 输出结果
    // 题目要求输出区间 [min_id + 1, max_id]
    if (ans_l > ans_r) swap(ans_l, ans_r);
    cout << ans_sum << " " << ans_l + 1 << " " << ans_r << "\n";
}

int main() {
    // 关流加速,竞赛必备
    ios::sync_with_stdio(false);
    cin.tie(0);

    while (cin >> n >> k && (n || k)) {

        // 1. 初始化并构建前缀和
        // p[0] 永远是 0,代表空前缀
        p[0] = {0, 0};

        for (int i = 1; i <= n; ++i) {
            int x;
            cin >> x;
            // 直接利用上一个位置累加,不需要原数组 a[]
            p[i].val = p[i - 1].val + x;
            p[i].id = i;
        }

        // 2. 排序
        // 注意范围是 [0, n],所以要 sort 到 p + n + 1
        sort(p, p + n + 1);

        // 3. 处理 k 次查询
        while (k--) {
            ll t;
            cin >> t;
            work(t);

        }
    }
    return 0;
}

复杂度分析

  1. 前缀和计算: O(N)O(N)
  2. 排序: O(NlogN)O(N \log N)
  3. 查询处理:
    • 每次查询使用双指针遍历一次数组,复杂度 O(N)O(N)
    • 总共有 KK 次查询。
    • 查询总复杂度 O(K×N)O(K \times N)
  4. 总复杂度: O(NlogN+K×N)O(N \log N + K \times N)
    • 鉴于 N=100,000N=100,000,如果 KK 很大,这可能会超时。但在本题(POJ 2566)的数据范围内,KK 比较小,或者测试用例较弱,此解法是标准正解。

总结

这就是处理带负数区间和问题的通解: 前缀和 + 排序 + 双指针。 它把原本无序的区间和问题,转化为了有序数组上的两数之差问题。