P3341 [ZJOI2014] 消棋子 解题报告
P3341 [ZJOI2014] 消棋子 解题报告
这是一道有趣的模拟题。题目要求我们解决两个问题:一是计算给定的一系列操作能消掉多少对棋子;二是找出一个能消掉最多棋子的方案。下面我们来分步解析这道题的思路和解法。
核心思路:如何高效地“看”到棋子?
游戏的核心操作是:从一个空格子出发,朝两个方向看,找到最近的棋子。棋盘可能非常大(\(r, c\) 可达 \(10^5\)),我们不可能真的去一个格子一个格子地遍历。
这里,题解采用了一个非常高效的数据结构:std::set
(有序集合)。
-
为每一行、每一列建立索引:
- 我们创建
row_set[i]
,一个集合,用来存放第i
行所有棋子的信息(主要是列号和颜色)。 - 同样,我们创建
column_set[j]
,存放第j
列所有棋子的信息(主要是行号和颜色)。
- 我们创建
-
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
个操作,一步步模拟即可。
模拟步骤:
- 初始化:使用
reset()
函数,将所有棋子的初始位置分别存入对应的row_set
和column_set
中。 - 遍历操作:对于给出的每一个操作,例如在
(x, y)
点,选择d1
和d2
两个方向:- 首先要判断
(x, y)
是不是一个空格子。如果row_set[x]
中存在一个列号为y
的棋子,说明这里不是空的,操作非法,直接跳过。 - 调用
get
函数,分别获取d1
和d2
方向上遇到的第一个棋子。 - 判断这两个棋子是否存在,并且颜色是否相同。
- 如果条件满足,说明这是一次成功的消除。我们将计数器
answer
加一,并调用erase
函数将这对棋子从row_set
和column_set
中彻底删除,因为它们已经被消掉了。
- 首先要判断
- 输出结果:遍历完所有
m
个操作后,输出answer
的值。
问题二:寻找最优解(贪心策略)
要消掉最多的棋子,我们不能漫无目的地尝试。一个很自然的想法是:如果有一对棋子能够被消除,我们就立刻消除它。这是一种贪心策略。但关键在于,消除一对棋子后,棋盘格局发生了变化,可能会创造出新的消除机会。这种连锁反应正是解题的核心。
贪心算法与连锁反应:
-
潜在的消除点:对于任意一对同色棋子,只有在特定的几个空格点操作,才有可能消除它们。
- 共行:棋子在
(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)
才可能同时看到它俩。
- 共行:棋子在
-
处理连锁反应 (Chain Reaction):题解精妙地使用了一个队列
q
来处理这种连锁反应。- 初始扫描:我们首先遍历
1
到n
每一种颜色,检查它们是否在当前棋盘上能够被消除。题解中的insert
函数(可以理解为try_eliminate
)就负责这个检查。如果颜色p
可以被消除,就执行消除操作,并将颜色编号p
推入队列q
中。 - 处理队列:只要队列
q
不为空,就说明刚刚有棋子被消除了,我们需要检查这是否引发了新的机会。- 从队列中取出一个颜色
i
。 - 找到颜色
i
的两个棋子被消除前的位置,比如pos_a
和pos_b
。这两个位置现在是空格了。 - 从
pos_a
和pos_b
这两个新的空格子出发,向上下左右四个方向看,看看紧邻的棋子是哪些。 - 对于每一个看到的邻居棋子(比如颜色为
j
),我们再次调用insert(j)
,尝试消除颜色j
。如果成功,j
也会被加入队列,等待下一轮检查。
- 从队列中取出一个颜色
- 结束:当队列变空时,意味着棋盘上再也没有可以消除的棋子了,连锁反应结束。此时记录下的所有成功操作,就是我们找到的一套能消除最多棋子的方案。
- 初始扫描:我们首先遍历
算法流程总结:
- 初始化:重置棋盘状态,建立
set
索引。 - 贪心消除:对每种颜色
i
(从 1 到n
),调用insert(i)
尝试进行一次消除。如果成功,将i
加入队列。 - 循环处理连锁反应:
- 当队列不为空,取出队首颜色
i
。 - 在其留下的两个空格处,检查所有相邻的棋子
j
。 - 对每个
j
,再次调用insert(j)
尝试消除。
- 当队列不为空,取出队首颜色
- 输出结果:输出记录下来的操作总数和具体操作。
通过这种“贪心 + 队列处理连锁反应”的机制,我们能够系统性地发现并执行所有可能的消除操作,从而得到一个消除数量最大化的方案。