P3341 [ZJOI2014] 消棋子 解题报告


P3341 [ZJOI2014] 消棋子 解题报告

这是一道有趣的模拟题。题目要求我们解决两个问题:一是计算给定的一系列操作能消掉多少对棋子;二是找出一个能消掉最多棋子的方案。下面我们来分步解析这道题的思路和解法。

核心思路:如何高效地“看”到棋子?

游戏的核心操作是:从一个空格子出发,朝两个方向看,找到最近的棋子。棋盘可能非常大(\(r, c\) 可达 \(10^5\)),我们不可能真的去一个格子一个格子地遍历。

这里,题解采用了一个非常高效的数据结构:std::set (有序集合)。

  1. 为每一行、每一列建立索引

    • 我们创建 row_set[i],一个集合,用来存放第 i 行所有棋子的信息(主要是列号和颜色)。
    • 同样,我们创建 column_set[j],存放第 j 列所有棋子的信息(主要是行号和颜色)。
  2. set 的优势

    • 有序性set 内部的元素是自动排序的。row_set[i] 里的棋子按列号从小到大排序,column_set[j] 里的棋子按行号从小到大排序。
    • 快速查找:利用 set 的有序性,我们可以使用 lower_bound (查找第一个不小于某值的元素) 等方法,非常快速地定位到某个坐标旁边最近的棋子,时间复杂度为 \(O(\log k)\),其中 \(k\) 是该行/列的棋子数。

举个例子:要在第 x 行第 y 列的空格向右('R')看,我们只需要在 row_set[x] 中查找第一个列号大于 y 的棋子即可。同理,向上('U')看,就在 column_set[y] 中查找第一个行号小于 x 的棋子。题解中的 get 函数就是封装了这个逻辑。

问题一:模拟给定的操作

这是比较直接的一部分。我们只需要严格按照题目给出的 m 个操作,一步步模拟即可。

模拟步骤

  1. 初始化:使用 reset() 函数,将所有棋子的初始位置分别存入对应的 row_setcolumn_set 中。
  2. 遍历操作:对于给出的每一个操作,例如在 (x, y) 点,选择 d1d2 两个方向:
    • 首先要判断 (x, y) 是不是一个空格子。如果 row_set[x] 中存在一个列号为 y 的棋子,说明这里不是空的,操作非法,直接跳过。
    • 调用 get 函数,分别获取 d1d2 方向上遇到的第一个棋子。
    • 判断这两个棋子是否存在,并且颜色是否相同。
    • 如果条件满足,说明这是一次成功的消除。我们将计数器 answer 加一,并调用 erase 函数将这对棋子从 row_setcolumn_set 中彻底删除,因为它们已经被消掉了。
  3. 输出结果:遍历完所有 m 个操作后,输出 answer 的值。

问题二:寻找最优解(贪心策略)

要消掉最多的棋子,我们不能漫无目的地尝试。一个很自然的想法是:如果有一对棋子能够被消除,我们就立刻消除它。这是一种贪心策略。但关键在于,消除一对棋子后,棋盘格局发生了变化,可能会创造出新的消除机会。这种连锁反应正是解题的核心。

贪心算法与连锁反应

  1. 潜在的消除点:对于任意一对同色棋子,只有在特定的几个空格点操作,才有可能消除它们。

    • 共行:棋子在 (r, c1)(r, c2),那么消除点一定在 (r, y)c1 < y < c2
    • 共列:棋子在 (r1, c)(r2, c),那么消除点一定在 (x, c)r1 < x < r2
    • 不同行列:棋子在 (r1, c1)(r2, c2),它们构成一个矩形。只有在另外两个顶点 (r1, c2)(r2, c1) 才可能同时看到它俩。
  2. 处理连锁反应 (Chain Reaction):题解精妙地使用了一个队列 q 来处理这种连锁反应。

    • 初始扫描:我们首先遍历 1n 每一种颜色,检查它们是否在当前棋盘上能够被消除。题解中的 insert 函数(可以理解为 try_eliminate)就负责这个检查。如果颜色 p 可以被消除,就执行消除操作,并将颜色编号 p 推入队列 q 中。
    • 处理队列:只要队列 q 不为空,就说明刚刚有棋子被消除了,我们需要检查这是否引发了新的机会。
      • 从队列中取出一个颜色 i
      • 找到颜色 i 的两个棋子被消除前的位置,比如 pos_apos_b。这两个位置现在是空格了。
      • pos_apos_b 这两个新的空格子出发,向上下左右四个方向看,看看紧邻的棋子是哪些。
      • 对于每一个看到的邻居棋子(比如颜色为 j),我们再次调用 insert(j),尝试消除颜色 j。如果成功,j 也会被加入队列,等待下一轮检查。
    • 结束:当队列变空时,意味着棋盘上再也没有可以消除的棋子了,连锁反应结束。此时记录下的所有成功操作,就是我们找到的一套能消除最多棋子的方案。

算法流程总结

  1. 初始化:重置棋盘状态,建立 set 索引。
  2. 贪心消除:对每种颜色 i (从 1 到 n),调用 insert(i) 尝试进行一次消除。如果成功,将 i 加入队列。
  3. 循环处理连锁反应
    • 当队列不为空,取出队首颜色 i
    • 在其留下的两个空格处,检查所有相邻的棋子 j
    • 对每个 j,再次调用 insert(j) 尝试消除。
  4. 输出结果:输出记录下来的操作总数和具体操作。

通过这种“贪心 + 队列处理连锁反应”的机制,我们能够系统性地发现并执行所有可能的消除操作,从而得到一个消除数量最大化的方案。

posted @ 2025-07-15 18:18  surprise_ying  阅读(12)  评论(0)    收藏  举报