十滴水

用有序映射维护有水格子,用最小堆按编号处理爆炸,并在爆炸时更新当前前驱和后继。

OJ: shumeng

题目 ID: CSP202403D

难度:普及+/提高-

标签:模拟有序集合优先队列

日期: 2026-07-31 16:21

形式化题目

一维网格 1c1 \sim c 中部分格子有 141 \sim 4 滴水。共 nn 次操作,每次给指定格子加一滴水:

  • 若某格子达到 55 滴,它立刻清空,并让当前时刻它左右两侧最近的有水格子各加一滴水;
  • 多个格子同时待爆炸时,按位置从小到大依次爆炸,且新产生的爆炸按相同规则加入待爆序列。

每次操作结束后输出仍有水的格子数。

思路

这题是带连锁反应的模拟,核心要支持两类操作:找出当前格子左右最近的有水格子,以及按位置顺序处理待爆格子

朴素做法:有序数组暴力模拟

先看一个直觉做法:把所有有水的格子放进排序数组,每次爆炸用二分找位置、删掉它再给相邻元素加水。

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:39
 */
// brute.cpp:小数据暴力解,用有序数组保存有水格子,二分查找相邻格子。
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int c, m, operation_count;
    cin >> c >> m >> operation_count;
    vector<pair<int, int> > water; // water[i] = {位置, 水滴数},按位置升序排列
    for (int i = 0; i < m; i++) {
        pair<int, int> current;
        cin >> current.first >> current.second;
        water.push_back(current);
    }
    sort(water.begin(), water.end());

    while (operation_count--) {
        int position;
        cin >> position;
        vector<int> exploding; // 待爆炸的位置列表
        // 找到指定位置并加一滴水
        for (int i = 0; i < (int)water.size(); i++) {
            if (water[i].first == position) {
                water[i].second++;
                if (water[i].second >= 5) exploding.push_back(position);
                break;
            }
        }

        while (!exploding.empty()) {
            sort(exploding.begin(), exploding.end()); // 最左的格子先爆
            int current = exploding[0];
            exploding.erase(exploding.begin());

            // 二分找到当前格子在数组中的下标
            vector<pair<int, int> >::iterator current_it = lower_bound(
                water.begin(), water.end(), make_pair(current, -1));
            if (current_it == water.end() || current_it->first != current
                    || current_it->second < 5) continue; // 已被之前的爆炸清空

            int index = current_it - water.begin();
            water.erase(water.begin() + index);

            // 左右相邻的有水格子各加一滴水
            if (index - 1 >= 0) {
                water[index - 1].second++;
                if (water[index - 1].second >= 5) exploding.push_back(water[index - 1].first);
            }
            if (index < (int)water.size()) {
                water[index].second++;
                if (water[index].second >= 5) exploding.push_back(water[index].first);
            }
        }

        cout << water.size() << '\n';
    }

    return 0;
}

这个做法每步的数组删除是 O(m)O(m) 的,只适合小数据,但能帮助验证连锁反应的规则。

主解:map 维护相邻 + 小根堆维护爆炸顺序

map<位置, 水滴数> 保存所有有水格子,有序键天然给出“左右相邻的有水格子”。一次爆炸时,先记住当前格子的前驱和后继迭代器,再删除当前格子,这样取到的正是爆炸瞬间两侧最近的格子。

待爆炸位置放进小根堆,保证每次处理编号最小的格子。堆里可能出现已被之前爆炸清空的过期位置,弹出时检查它是否仍在 map 中且水滴数 5\ge 5,不满足就跳过。

关键性质:一个格子爆开后从有水集合中消失,本轮不会再被重新加入,所以所有爆炸的总次数是 O(m)O(m) 的。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:39
 */
#include <bits/stdc++.h>
using namespace std;

map<int, int> water;  // water[pos] 表示位置 pos 当前的水滴数,只保存有水的格子
priority_queue<int, vector<int>, greater<int> > exploding; // 待爆炸位置,小根堆保证最左的先爆

// 给位置 pos 加一滴水,达到 5 滴就加入待爆堆
void add_water(int pos) {
    water[pos]++;
    if (water[pos] >= 5) exploding.push(pos);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int c, m, operation_count;
    cin >> c >> m >> operation_count;
    for (int i = 0; i < m; i++) {
        int pos, cnt;
        cin >> pos >> cnt;
        water[pos] = cnt;
    }

    while (operation_count--) {
        int position;
        cin >> position;
        add_water(position);

        // 处理连锁爆炸,直到没有待爆格子
        while (!exploding.empty()) {
            int current = exploding.top();
            exploding.pop();
            map<int, int>::iterator it = water.find(current);
            if (it == water.end() || it->second < 5) continue; // 已被之前的爆炸清空

            // 先记住左右相邻的有水格子,再删除当前格子
            map<int, int>::iterator left = it;
            bool has_left = (left != water.begin());
            if (has_left) --left;
            map<int, int>::iterator right = it;
            ++right;
            bool has_right = (right != water.end());
            water.erase(it);

            if (has_left) add_water(left->first);
            if (has_right) add_water(right->first);
        }

        cout << water.size() << '\n';
    }

    return 0;
}

复杂度

mm 为初始有水格子数,nn 为操作数。

  • 时间:每个格子至多爆炸一次,每次 map 查找与更新 O(logm)O(\log m),总时间复杂度 O((n+m)logm)O((n + m) \log m)
  • 空间:map 中最多 mm 个格子,空间复杂度 O(m)O(m)

总结

模拟连锁反应时要把握两个关键数据结构:有序集合表达“最近的有水格子”,优先队列表达“多个待爆格子按最左优先”。删除当前节点后再取邻居,能避免把已经清空的格子当成传播目标;堆中残留的过期元素通过合法性检查丢弃即可。