题解:洛谷 P3184 Counting Haybales

【题目来源】

洛谷:P3184 [USACO16DEC] Counting Haybales S - 洛谷

【题目描述】

Farmer John 刚刚将他的 \(N\) 个干草捆(\(1 \leq N \leq 100,000\))安排在他农场的一维道路上的各个位置。为了确保它们之间的间距合适,请帮助他回答 \(Q\) 个查询(\(1 \leq Q \leq 100,000\)),每个查询询问在道路的特定区间内有多少个干草捆。

【输入】

第一行包含 \(N\)\(Q\)

第二行包含 \(N\) 个不同的整数,每个整数的范围在 \(0 \ldots 1,000,000,000\) 之间,表示每个位置上有一个干草捆。

接下来的 \(Q\) 行每行包含两个整数 \(A\)\(B\)\(0 \leq A \leq B \leq 1,000,000,000\)),表示一个查询,询问在 \(A\)\(B\)(包括 \(A\)\(B\))的区间内有多少个干草捆。

【输出】

你需要输出 \(Q\) 行。对于每个查询,输出其对应区间内的干草捆数量。

【输入样例】

4 6
3 2 7 5
2 3
2 4
2 5
2 7
4 6
8 10

【输出样例】

2
2
3
4
1
0

【核心思想】

  1. 问题分析:给定 \(N\) 个干草捆的位置和 \(Q\) 个区间查询,求每个区间内的干草捆数量。坐标范围达 \(10^9\),但 \(N, Q \leq 10^5\)。本质上是离散化 + 树状数组问题:将大坐标范围压缩到有限空间,用树状数组高效统计区间和。

  2. 算法选择

    • 离散化:将所有出现过的坐标(干草捆位置和查询端点)排序去重,映射到 \(1 \sim M\) 的连续整数
    • 树状数组(Fenwick Tree):支持 \(O(\log M)\) 的单点更新和区间查询
  3. 关键步骤

    • 读取数据:读入 \(N, Q\)\(N\) 个干草捆位置,\(Q\) 个查询区间
    • 收集坐标:将所有干草捆位置和查询的左右端点加入 alls 数组
    • 离散化sort + unique 去重,用 lower_bound 映射到 \(1 \sim M\)
    • 建树状数组:对每个干草捆位置 add(pos, 1)
    • 处理查询:对每个查询 \([l, r]\),输出 query(r) - query(l-1)
  4. 时间/空间复杂度

    • 时间复杂度:\(O((N+Q) \log (N+Q))\),离散化排序 + 树状数组操作
    • 空间复杂度:\(O(N+Q)\),离散化数组和树状数组
  5. 离散化 + 树状数组的核心思想

    • 坐标压缩:将 \(10^9\) 范围的大坐标映射到 \(O(N+Q)\) 级别的小坐标,避免开巨大数组
    • 树状数组的高效性:单点更新和前缀查询均为 \(O(\log M)\),区间查询通过两个前缀差实现
    • 查询端点的必要性:查询区间 \([A, B]\) 的端点也需加入离散化,否则无法定位
    • 离线处理:所有查询先读入再统一处理,适合批量查询场景
    • 适用于大坐标范围、区间统计、离线查询类问题

【算法标签】

普及- #树状数组

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 100005 * 3;  // 最大数组大小(考虑离散化后的可能大小)
typedef pair<int, int> PII;  // 定义pair类型,用于存储查询区间

int n, q;              // n: 数组长度, q: 查询次数
int a[N];              // 原始数组
int tr[N];             // 树状数组,用于统计离散化后的值出现次数
vector<int> alls;      // 离散化数组,存储所有需要离散化的值
vector<PII> queries;   // 存储所有查询区间

/**
 * 计算lowbit:获取x的最低位的1
 * @param x 输入数值
 * @return x的最低位的1所代表的值
 */
int lowbit(int x)
{
    return x & -x;  // 利用补码性质
}

/**
 * 树状数组单点更新操作
 * @param x 更新位置(离散化后的位置)
 * @param c 增加的值
 */
void add(int x, int c)
{
    // 树状数组标准更新操作,向上更新所有相关节点
    // 注意:这里使用了固定的N作为上限,可能存在问题
    for (int i = x; i <= N; i += lowbit(i))
        tr[i] += c;
}

/**
 * 树状数组前缀和查询操作
 * @param x 查询位置(离散化后的位置)
 * @return 前x个位置的和
 */
int query(int x)
{
    int res = 0;
    // 树状数组标准查询操作,向下累加所有相关节点
    for (int i = x; i; i -= lowbit(i))
        res += tr[i];
    return res;
}

/**
 * 离散化查找:在离散化数组alls中查找x的位置
 * @param x 要查找的值
 * @return x在离散化数组中的索引(从1开始)
 */
int find(int x)
{
    // 使用二分查找在alls中找到x的位置
    return lower_bound(alls.begin(), alls.end(), x) - alls.begin() + 1;
}

int main()
{
    // 输入数组长度和查询次数
    cin >> n >> q;

    // 读入原始数组,并收集需要离散化的值
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        alls.push_back(a[i]);  // 将数组元素加入离散化列表
    }

    // 读入查询区间,并收集查询端点用于离散化
    for (int i = 1; i <= q; i++)
    {
        int l, r;
        cin >> l >> r;
        queries.push_back({l, r});  // 存储查询区间
        alls.push_back(l);          // 将左端点加入离散化列表
        alls.push_back(r);          // 将右端点加入离散化列表
    }

    // 离散化处理
    sort(alls.begin(), alls.end());  // 排序
    alls.erase(unique(alls.begin(), alls.end()), alls.end());  // 去重

    // 使用树状数组统计离散化后的值
    for (int i = 1; i <= n; i++)
    {
        int pos = find(a[i]);  // 将原始值映射到离散化位置
        add(pos, 1);          // 在树状数组中对应位置加1
    }

    // 处理每个查询
    for (auto t : queries)
    {
        // 将查询区间端点离散化
        int l = find(t.first);   // 左端点离散化
        int r = find(t.second);  // 右端点离散化

        // 计算区间和:query(r) - query(l-1)
        cout << query(r) - query(l - 1) << endl;
    }

    return 0;
}

【运行结果】

4 6
3 2 7 5
2 3
2 4
2 5
2 7
4 6
8 10
2
2
3
4
1
0
posted @ 2026-08-17 09:47  团爸讲算法  阅读(6)  评论(0)    收藏  举报