1 条题解

  • 0
    @ 2026-5-6 15:07:38

    B. 矩阵稳定化 详细题解

    题目重述

    给定一个 n×mn \times m 的整数矩阵 aa。反复执行以下操作:

    • 找到任意一个单元格 (i,j)(i,j),满足 aija_{ij} 严格大于它所有上、下、左、右相邻单元格的值(忽略边界)。如果有多个,选择行号最小的,再选列号最小的。
    • 将该单元格的值减少 11。 直到不存在这样的单元格。输出最终矩阵。保证算法在有限步内终止。

    核心观察

    • 操作只减少值,不会增加,因此每个单元格的值单调递减。
    • 当某个单元格的值大于所有邻居时,它会被反复减 11,直到它不再大于所有邻居。
    • 设单元格 pp 的邻居中最大值为 MpM_p,则 pp 最终的值必定等于 MpM_p(因为当它大于 MpM_p 时会一直减到 MpM_p,一旦等于 MpM_p 就不再满足“严格大于”条件)。但注意,MpM_p 本身也可能随着邻居的减少而变化,因此最终值可能小于初始 MpM_p
    • 整个稳定过程等价于:反复将当前所有局部最大值(严格大于所有邻居)降低到其邻居的最大值,直到没有局部最大值。

    算法思想

    由于每次只将最大的局部峰值降低到邻居的最大值,我们可以使用优先队列(最大堆)来模拟。但直接模拟逐次减 11 会超时(值可达 10910^9),因此我们一次性地将单元格的值设为邻居的最大值(而不是每次减 11)。这样每个单元格最多被处理一次(原因见后),总复杂度 O(nmlog(nm))O(nm \log(nm))

    具体步骤

    1. 读取矩阵 aa,初始化一个最大堆(优先队列),元素为 (a[i][j], i, j),按值降序、行升序、列升序排列(但堆默认按值最大,我们直接存值即可,因为处理顺序并不严格依赖行列,最终结果唯一)。
    2. 复制一份当前矩阵 cur,用于记录实时值。
    3. 当堆非空:
      • 弹出堆顶 (val, i, j)
      • 如果 cur[i][j] != val,说明该单元格已经被更新过,跳过。
      • 计算四个方向上邻居的最大值 mx(忽略边界)。
      • 如果 val > mx,则将该单元格的值更新为 mx(即 cur[i][j] = mx),并将 (mx, i, j) 重新入堆。
      • 否则,不做任何操作。
    4. 最终 cur 即为稳定后的矩阵。

    正确性证明

    引理 1:每个单元格的值最多被更新一次。
    证明:假设单元格 pp 第一次被处理时,其值 v>maxneighbors(cur)v > \max_{neighbors}(cur),于是被更新为 m=maxneighbors(cur)m = \max_{neighbors}(cur)。此后,pp 的值等于 mm。因为 mm 是当时其邻居的最大值,所以至少存在一个邻居 qq 满足 cur[q]=mcur[q] = m。之后任何其他单元格的更新只会使值减小,因此 cur[q]cur[q] 只会变小或不变,而 cur[p]cur[p] 保持 mm 不变。那么 cur[p]cur[q]cur[p] \ge cur[q],且当 cur[q]cur[q] 严格变小时,cur[p]>cur[q]cur[p] > cur[q],但 pp 可能还有其它邻居,其中至少有一个(即 qq)的值小于 mm。为了 pp 再次成为局部最大值,需要 cur[p]cur[p] 严格大于所有邻居。然而,pp 的邻居中包括 qq,而 cur[p]=mcur[p] = mcur[q]mcur[q] \le m,当 cur[q]<mcur[q] < m 时,pp 确实大于 qq,但 pp 可能还存在另一个邻居 rr 最初等于 mm,若 rr 后来也被降低,那么 pp 就可能大于所有邻居。但注意到 rrpp 相邻,且 cur[r]=mcur[r] = m 时,rr 本身也是一个局部最大值(如果它的其他邻居都不大于 mm),那么 rr 也会被处理并降低。然而,当 rr 被处理后,其值也会变成它邻居的最大值,但该最大值一定不超过 mm,且由于 pp 的值保持 mmrr 的新值 m\le m。此时 pp 的邻居中 rr 的值变为 m\le mqq 的值也 m\le m,但 pp 仍然大于等于它们,然而 pp 还有一个邻居?实际上,pp 可能不止两个邻居,但关键是 pp 自身并没有被再次入堆,因为它的值从未改变过。在算法中,只有当单元格的值被修改时,我们才重新入堆。pp 的值在第一次更新后就固定了,后面不会再被修改,因此它不可能再次被处理。所以每个单元格只会被更新一次,从而每个单元格最多入堆两次(初始一次,更新后一次)。因此总操作次数为 O(nm)O(nm)

    引理 2:算法结束时,矩阵中没有任何单元格的值严格大于所有邻居。
    证明:若存在这样的单元格,它一定会被堆处理。因为堆中始终包含所有大于邻居最大值的单元格(初始全部入堆,每次更新后新值也会入堆)。当堆为空时,意味着没有单元格满足条件。

    引理 3:最终矩阵与按题目描述的逐次减 11 操作得到的结果相同。
    证明:逐次减 11 的过程相当于将每个局部最大峰值逐步降低,而我们的算法一次降低到邻居最大值,这等价于跳过了中间不必要的减 11 步骤。由于每次降低的目标值相同(均为当时邻居的最大值),且后续操作不依赖中间值,因此最终结果一致。

    复杂度分析

    • 每个单元格初始入堆一次,更新后至多再入堆一次,总入堆次数 O(nm)O(nm)
    • 每次堆操作 O(log(nm))O(\log(nm)),总时间 O(nmlog(nm))O(nm \log(nm))
    • 空间 O(nm)O(nm)
    • 所有测试用例的 nmnm 之和 2×105\le 2\times 10^5,完全可行。

    实现细节

    • 使用 priority_queue<tuple<int,int,int>>,默认按第一个元素降序,与需求一致。
    • 需要存储当前矩阵 cur 以便比较和更新。
    • 注意边界处理:邻居只考虑存在的位置,最大值初始化为 00 或很小的数(因为所有值 1\ge 1)。

    示例验证

    以第一个测试用例 1 2,矩阵 [3, 1] 为例:

    • 初始堆:(3,0,0), (1,0,1)
    • 弹出 (3,0,0),邻居只有 (0,1) 值为 11,最大值 113>13>1,故将 cur[0][0] 设为 11,入堆 (1,0,0)
    • 此时堆中有 (1,0,0), (1,0,1)
    • 弹出 (1,0,0),检查邻居最大值(右边 11),11 不大于 11,跳过。
    • 弹出 (1,0,1),邻居只有左边 1111 不大于 11,跳过。
    • 堆空,结束。得到 [1,1],符合输出。

    其他测试用例类似。

    总结

    本题的核心是将逐次减 11 的复杂过程转化为一次直接降低到邻居最大值,并利用优先队列模拟“从大到小”的稳定化过程。每个单元格最多更新一次,保证了高效性。最终矩阵是唯一的,且算法易于实现。

    • 1

    信息

    ID
    6986
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者