洛谷 P1503 鬼子进村:树状数组与二分查找的经典应用

题目描述

县城里有 n 个用地道相连的房子排成一条直线,第 i 个房子只与第 i-1 和 i+1 个相连。特别地,第 1 个房子只和第 2 个联通,第 n 个房子只和第 n-1 个联通。

有 m 条消息依次传来:

  • D x:鬼子将 x 号房子摧毁了,地道被堵上
  • R:村民们将鬼子上一个摧毁的房子修复了
  • Q x:有一名士兵被围堵在 x 号房子中,询问能到达的房子数量

数据范围:1 ≤ n, m ≤ 5×10⁴

问题分析

核心观察

对于未被摧毁的 x 号房子,士兵能到达的范围是连续的,由左右两边最近的被摧毁房子决定。

设左边最近的被摧毁房子位置为 L(L < x),右边最近的被摧毁房子位置为 R(R > x),则能到达的范围是 (L+1) 到 (R-1),能到达的房子数为 R – L – 1

问题转化

原问题转化为:

  • 维护一个集合 S,存储所有被摧毁的房子
  • 对于查询 x,需要找到 S 中 x 的前驱(小于 x 的最大值)和后继(大于 x 的最小值)
  • 答案 = 后继 – 前驱 – 1

算法设计

为什么使用树状数组?

树状数组(Fenwick Tree)是一种高效的数据结构,用于维护数组前缀和的动态查询和更新。

  • 单点更新 O(log n):修改一个元素的值
  • 区间查询 O(log n):查询前缀和
  • 可以通过二分查找实现「连续完好的区间长度」查询

核心技巧:不直接找被摧毁的房子,而是找 x 左右连续完好的房子数量。

核心操作:查询连续完好区间

使用二分查找分别寻找左右两边连续完好的区间长度。

查询左边连续完好房子数

// 查询 x 左边连续完好的房子数
int find_left(int x) {
    int l = 1, r = x, ans = x;
    while (l <= r) {
        int mid = (l + r) >> 1;
        // 检查 [mid, x] 区间是否全部完好
        if (query(mid, x) == x - mid + 1) {
            ans = mid;
            r = mid - 1;  // 向左收缩,寻找更左边的起点
        } else {
            l = mid + 1;  // 不全部完好,向右收缩
        }
    }
    return x - ans + 1;  // 左边连续完好数
}

查询右边连续完好房子数

// 查询 x 右边连续完好的房子数
int find_right(int x) {
    int l = x, r = n, ans = x;
    while (l <= r) {
        int mid = (l + r) >> 1;
        // 检查 [x, mid] 区间是否全部完好
        if (query(x, mid) == mid - x + 1) {
            ans = mid;
            l = mid + 1;  // 向右扩大,寻找更右边的终点
        } else {
            r = mid - 1;  // 不全部完好,向左收缩
        }
    }
    return ans - x + 1;  // 右边连续完好数
}

二分查找的正确性

定义函数 f(mid) = [区间[mid, x]是否全部完好]

  • 若 f(mid) = true,则对于任意 mid' > mid,区间[mid', x] 是 [mid, x] 的子区间,必然也全部完好
  • 若 f(mid) = false,则对于任意 mid' < mid,区间[mid', x] 包含区间[mid, x],必然也存在被摧毁房子

因此 f(mid) 具有单调性,存在一个分界点 M,使得当 mid ≥ M 时 f(mid)=true,当 mid < M 时 f(mid)=false。二分查找可以找到最小的 mid 使得 f(mid)=true。

完整代码实现

#include <iostream>
#include <cstdio>
using namespace std;

const int N = 50010;

int n, m;          // 房子数、消息数
int bad[N];       // 标记房子是否被摧毁,1=已摧毁
int tr[N];        // 树状数组,存储完好的房子数
int stk[N], top;  // 栈,记录摧毁顺序用于 R 操作

// lowbit 函数:返回 x 二进制的最低位 1
int lowbit(int x) {
    return x & -x;
}

// 树状数组单点更新
void add(int x, int c) {
    for (; x <= n; x += lowbit(x))
        tr[x] += c;
}

// 树状数组前缀和查询
int sum(int x) {
    int res = 0;
    for (; x; x -= lowbit(x))
        res += tr[x];
    return res;
}

// 区间查询:[l, r] 区间内完好的房子数
int query(int l, int r) {
    if (l > r) return 0;
    return sum(r) - sum(l - 1);
}

// 查询 x 左边连续完好的房子数
int find_left(int x) {
    int l = 1, r = x, ans = x;
    while (l <= r) {
        int mid = (l + r) >> 1;
        if (query(mid, x) == x - mid + 1) {
            ans = mid;
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    return x - ans + 1;
}

// 查询 x 右边连续完好的房子数
int find_right(int x) {
    int l = x, r = n, ans = x;
    while (l <= r) {
        int mid = (l + r) >> 1;
        if (query(x, mid) == mid - x + 1) {
            ans = mid;
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    return ans - x + 1;
}

int main() {
    scanf("%d%d", &n, &m);
    
    // 初始化树状数组:所有房子都是完好的
    for (int i = 1; i <= n; i++)
        add(i, 1);

    while (m--) {
        char op[2];
        int x;
        scanf("%s", op);
        
        if (op[0] == 'D') {  // 摧毁操作
            scanf("%d", &x);
            if (!bad[x]) {
                bad[x] = 1;
                add(x, -1);      // 完好数减 1
                stk[++top] = x;  // 入栈
            }
        } else if (op[0] == 'R') {  // 修复操作
            if (top > 0) {
                x = stk[top--];   // 取出上一个被摧毁的房子
                bad[x] = 0;
                add(x, 1);       // 完好数加 1
            }
        } else if (op[0] == 'Q') {  // 查询操作
            scanf("%d", &x);
            if (bad[x]) {
                printf("0\n");  // 士兵在被摧毁的房子中
            } else {
                int l = find_left(x);   // 左边连续完好数
                int r = find_right(x);  // 右边连续完好数
                printf("%d\n", l + r - 1);  // 减去重复计算的 x
            }
        }
    }
    return 0;
}

复杂度分析

操作 时间复杂度 空间复杂度
单点修改(add) O(log n) O(n)
区间查询(query) O(log n)
二分查找 O(log² n)
总复杂度 O(m log² n)

对于 n, m ≤ 50000,log₂50000 ≈ 16,总操作数约为 50000 × 256 ≈ 1.28×10⁷,完全可以接受。

关键要点总结

  1. 栈的应用:维护被摧毁房子的顺序,确保 R 操作修复的是「最近」被摧毁的房子
  2. 树状数组:高效维护「完好房子数」,支持 O(log n) 的单点更新和区间查询
  3. 二分查找:利用单调性快速定位连续完好区间的边界
  4. 答案公式:left_good + right_good – 1(x 被重复计算)

样例验证

输入:

7 9
D 3
D 6
D 5
Q 4
Q 5
R
Q 4
R
Q 4

处理过程:

操作      状态(被摧毁)  查询4号结果  说明
初始      {}              -           所有房子完好
D 3      {3}             -           摧毁3号
D 6      {3,6}           -           摧毁6号
D 5      {3,5,6}         -           摧毁5号
Q 4     {3,5,6}         1           4号只能到达4号本身
Q 5     {3,5,6}         0           5号已被摧毁
R        {3,6}           -           修复5号
Q 4     {3,6}           2           4号可到达4,5
R        {3}             -           修复6号
Q 4     {3}             4           4号可到达4,5,6,7

输出:1 0 2 4

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇