模拟赛1

题解

A

一个字母放在某个按键的第 d 个位置,就会贡献 频数 * d。所有按键提供的可用位置只和深度有关:容量为 a_i 的按键提供深度 1,2,...,a_i 各一个位置。

为了让总贡献最小,应当把出现频数最大的字母放到最小的深度上。若 p_i >= p_jd_i > d_j,交换这两个字母后答案减少 (p_i-p_j)(d_i-d_j),所以高频配浅位置一定不劣。

做法:若 sum a_i < n 则无解;否则将频数降序排序,将容量升序排序,按深度依次统计还有多少个按键能放这个深度,把高频字母依次放进去。

复杂度 O((n+m)log(n+m))

B

要求排列满足 |p_i-i| <= 2。从左到右决定第 i 位时,只可能用到数 i-2i+2,因此只需要用一个 5 位二进制状态记录这些数是否已经使用。

转移时枚举给第 i 位填哪个未用的数。填完后窗口右移一格,右移前 i-2 必须已经被使用,否则它以后再也没有机会出现。中间位置的转移完全相同,用 32 状态矩阵快速幂处理 n 很大的情况。

复杂度 O(32^3 log n)

C

1. 把题目看成拼矩形

一个数对 (a,b) 可以理解为一个矩形:

  • a 是宽。
  • b 是高。
  • 权值 w(a,b)=a*b 就是面积。

两个运算也正好对应两种拼接方式:

  • X: (a,b) X (c,d) = (a+c, max(b,d)),相当于左右并排拼起来,宽相加,高取最大值。
  • Y: (a,b) Y (c,d) = (max(a,c), b+d),相当于上下叠起来,宽取最大值,高相加。

所以题目等价于:每个叶子可以选择一个小矩形,每个内部节点可以选择横拼或竖拼,问最后最小面积是多少。

2. 什么是 Pareto 前沿

Pareto 前沿也可以叫“非劣方案集合”。

如果有两个方案:

  • 方案 A 是 (x1,y1)
  • 方案 B 是 (x2,y2)

并且 x1 <= x2y1 <= y2,那么方案 A 宽不比 B 大,高也不比 B 大。此时 B 无论放到后面的任何运算里,都不可能比 A 更好,因为 XY 对宽高都是单调的:输入的宽高变小,输出的宽高不会变大。

这种情况下,我们说 B 被 A 支配,可以直接删掉。

举个例子:

(2,10), (4,6), (5,8), (7,3)

其中 (5,8)(4,6) 支配,因为 4 <= 56 <= 8,所以 (5,8) 永远没必要保留。

但是 (2,10)(4,6) 谁也不能删:前者更窄,后者更矮,各有优势。(7,3) 也不能删,因为它虽然更宽,但是高度更小。

删掉所有被支配的方案后,剩下的就是 Pareto 前沿。本题里我们把前沿按 x 从小到大排序。这样保留下来的 y 一定是严格递减的:

x 越大,y 必须越小,否则后面的点就会被前面的点支配。

3. 每个子表达式维护什么

对每个子表达式,我们不只保存一个最优数对,而是保存它所有可能结果的 Pareto 前沿。

为什么不能只保存面积最小的一个?

因为当前面积小,不代表以后一定好。例如:

(1,100) 面积 100
(10,10) 面积 100

它们面积一样,但和别的矩形横拼、竖拼时表现不同。更窄或更矮的方案都可能在后面变成最优,所以要保留所有“没有被明显淘汰”的方案。

对于一个叶子,也就是一个变量的可选集合,先把输入给出的所有数对做一次 Pareto 化,删掉被支配的选择。

对于一个运算符,弹出左右两个子表达式的前沿,把它们合并成新的前沿。

整个后缀表达式用栈处理:

  1. 遇到变量 i,把第 i 个变量的前沿压栈。
  2. 遇到运算符 0,弹出右子树和左子树,合并后压回栈。
  3. 最后栈顶就是整个表达式的前沿,在里面取最小 x*y

4. 如何 Pareto 化

给出一堆候选数对后,做法如下:

  1. x 升序排序。
  2. 如果 x 相同,只保留 y 最小的那个。
  3. 从左到右扫描,维护目前见过的最小 y
  4. 当前点的 y 如果不小于之前最小的 y,它就被前面的某个点支配,删掉。
  5. 否则保留它。

例如候选点排序后是:

(2,10), (4,6), (5,8), (7,3)

扫描到 (5,8) 时,前面已经有 (4,6),宽更小、高更小,所以删掉。最后得到:

(2,10), (4,6), (7,3)

5. 合并两个前沿时为什么不能暴力

假设左前沿大小是 s,右前沿大小是 t。如果枚举左右所有组合,要做 s*t 次。

表达式有最多 2000 个叶子,内部节点也接近 2000 个。如果每次都平方枚举,容易爆掉。

所以需要利用前沿的有序性质:按 x 升序时,y 严格递减。

6. 快速生成所有有用的 X 候选

先考虑横拼 X

(a,b) X (c,d) = (a+c, max(b,d))

最后的高度只可能来自左边某个高度,或者右边某个高度,因为它是 max(b,d)

假设我们指定最后高度不能超过 h。那么左边只能选高度 <= h 的方案,右边也只能选高度 <= h 的方案。

在这些合法方案中,为了让横拼后的宽度最小,左边应该选“高度 <= h 时宽度最小”的方案,右边也一样。

于是对于每个可能的高度 h,最多只需要产生一个最优横拼候选:

(左边在高度 <= h 时的最小宽度 + 右边在高度 <= h 时的最小宽度, h)

为什么只枚举前沿中出现过的高度就够?

因为 X 的最终高度是 max(b,d),一定等于某个参与方案的高度,也就是某个前沿点的高度。其他高度不会产生新的关键结果。

由于前沿按 x 升序、y 降序排列,可以用双指针随着 h 增大维护“第一个高度 <= h 的点”。这个点就是当前条件下宽度最小的点。

7. 快速生成所有有用的 Y 候选

竖拼 Y 完全对称:

(a,b) Y (c,d) = (max(a,c), b+d)

最后的宽度只可能来自左边某个宽度,或者右边某个宽度。

假设指定最后宽度不能超过 w。左边和右边都只能选宽度 <= w 的方案。为了让总高度最小,两边都应该选“宽度 <= w 时高度最小”的方案。

于是对于每个可能的宽度 w,产生一个候选:

(w, 左边在宽度 <= w 时的最小高度 + 右边在宽度 <= w 时的最小高度)

同样只需要枚举左右前沿里出现过的宽度,并用双指针维护当前最优高度。

8. 合并流程总结

合并左前沿 L 和右前沿 R

  1. 枚举 LR 中出现过的所有高度,生成必要的 X 候选。
  2. 枚举 LR 中出现过的所有宽度,生成必要的 Y 候选。
  3. 把这些候选放在一起做 Pareto 化。
  4. 得到当前子表达式的新前沿。

这样每次合并生成的候选数量大约是 |L|+|R|,而不是 |L|*|R|

9. 正确性要点

第一,删掉被支配方案是安全的。这里把“支配”写成一个偏序:

(x1,y1) <= (x2,y2)  当且仅当  x1 <= x2 且 y1 <= y2

如果 (x1,y1) <= (x2,y2),那么 (x1,y1) 支配 (x2,y2)

下面证明被支配的矩形永远可以删。考虑任意第三个矩形 (u,v)

  • X 中宽是相加,高是取最大值。
  • Y 中宽是取最大值,高是相加。

若把 (x2,y2) 换成 (x1,y1),则:

X:
(x1+u, max(y1,v)) <= (x2+u, max(y2,v))

Y:
(max(x1,u), y1+v) <= (max(x2,u), y2+v)

也就是说,XY 都保持这个偏序关系。一个矩形如果一开始已经被另一个矩形支配,那么无论之后和多少个矩形继续运算,它得到的最终结果也会被对应结果支配,面积不可能更小。

因此,任意子表达式里被支配的矩形都不可能通向全局最优答案,可以删掉。这个结论也可以看成对表达式树往上的归纳:叶子处删除安全;如果左右子表达式的删除安全,那么经过单调的 X/Y 合并后仍然安全。

第二,X 候选没有漏。

任意一个横拼结果的高度是 h=max(b,d)。在高度 <=h 的所有左方案中选最小宽、在高度 <=h 的所有右方案中选最小宽,得到的横拼结果宽度不大于原方案,高度不超过 h。所以原方案要么被这个候选支配,要么等价于这个候选。保留这些候选后再 Pareto 化,不会漏掉有用结果。

第三,Y 候选同理。

任意一个竖拼结果的宽度是 w=max(a,c)。在宽度 <=w 的所有左方案中选最小高、在宽度 <=w 的所有右方案中选最小高,得到的竖拼结果高度不大于原方案,宽度不超过 w。因此也不会漏。

最后,根节点前沿包含了所有可能最优结果的代表,答案就是其中最小的 x*y

10. 状态数为什么有上界

题目保证最终最优答案在 32 位整数范围内,记:

M = 2^31 - 1

所有数对的坐标都是正整数,而且之后的 X/Y 运算只会让宽和高不减小:

  • X 后宽变成 a+c >= a,高变成 max(b,d) >= b
  • Y 后宽变成 max(a,c) >= a,高变成 b+d >= b

所以,如果某个中间矩形 (x,y) 已经满足 x*y > M,那么它继续往上合并后面积只会更大,不可能成为最优答案的一部分。代码中可以直接丢掉这种状态。

于是每个被保留的状态都满足:

x*y <= M

同时,一个前沿里的状态按 x 升序排列时,y 必须严格递减。设某个前沿有 k 个状态:

(x1,y1), (x2,y2), ..., (xk,yk)

其中:

x1 < x2 < ... < xk
y1 > y2 > ... > yk

因为 x_i 是正整数且严格递增,所以:

x_i >= i

因为 y_i 是正整数且严格递减,所以从第 i 个位置到最后还有 k-i+1 个不同的正整数高度,因此:

y_i >= k-i+1

又因为每个状态都满足 x_i*y_i <= M,所以对任意 i 都有:

i * (k-i+1) <= M

取中间位置,例如 i = floor((k+1)/2),左边大约是 k^2/4,得到:

k^2 / 4 <= M
k <= 2 * sqrt(M)

也就是说,每个 Pareto 前沿的状态数是 O(sqrt(M)) 的。对本题:

2 * sqrt(2^31 - 1) < 100000

所以一个子表达式不会保留无限多状态。这就是只维护不被偏序支配的矩形后,状态数复杂度成立的原因。

另外,因为每个叶子的坐标不超过 2000,一个含有 cnt 个叶子的子表达式宽和高都不会超过 2000*cnt,所以状态数也不会超过 2000*cnt。实际状态数满足:

O(min(2000*cnt, sqrt(M)))

11. 复杂度

设一次合并的两个前沿大小分别为 st

本做法生成 O(s+t) 个候选,再排序并 Pareto 化,复杂度约为:

O((s+t) log(s+t))

K=O(sqrt(M)) 是单个前沿的状态数上界,则每个合并点最多处理 O(K) 个候选。表达式有 n-1 个运算符,因此总时间复杂度可以写成:

O(n * K log K)

其中本题 K < 100000。相比直接枚举所有组合的 O(s*t),这里每次合并只处理线性数量的关键候选。

栈中同时存在的若干前沿对应互不相交的子表达式。若用更细的上界 2000*cnt 来估计,同一时刻所有栈内状态数总和不超过 O(2000*n),因此空间可以控制在可接受范围内。

D

核心观察一:把碰撞看成“幽灵穿过”

考虑一只顺时针变色龙 A,颜色为 x,和一只逆时针变色龙 B,颜色为 y 相遇。

真实规则是:

  • A 变为逆时针,颜色仍为 x
  • B 变为顺时针,颜色也变为 x

如果只看圆环上有哪些运动轨迹,而暂时不关心“输入中的第几只”是谁,那么这等价于:

  • 顺时针轨迹继续顺时针走;
  • 逆时针轨迹继续逆时针走;
  • 两个真实编号在这两条轨迹之间交换。

也就是说,坐标和方向可以用“幽灵变色龙直接穿过对方”的模型计算;真实变色龙的编号会在每次相遇时沿圆环顺序发生交换。

颜色的变化也能在幽灵模型里描述:

  • 顺时针幽灵的颜色永远保持初始颜色;
  • 逆时针幽灵每次遇到一个顺时针幽灵,就会被改成那个顺时针幽灵的颜色;
  • 因此逆时针幽灵最终颜色只取决于 T 之前最后一次遇到的顺时针幽灵。

核心观察二:相遇次数只需要二分统计

一只顺时针幽灵和一只逆时针幽灵的相对速度是 2。令

S = 2T
full = S / L
rest = S % L

对于任意一只顺时针幽灵:

  • 每只逆时针幽灵在完整的相对绕圈中都会与它相遇 full 次;
  • 还会额外相遇一次,当且仅当那只逆时针幽灵的初始位置在它顺时针方向长度 rest 的弧段内。

所以顺时针幽灵的相遇次数为:

full * 逆时针总数 + 顺时针方向 rest 弧段内的逆时针初始点数

同理,对于一只逆时针幽灵:

full * 顺时针总数 + 逆时针方向 rest 弧段内的顺时针初始点数

把顺时针初始位置、逆时针初始位置分别排序,就能用 lower_bound / upper_boundO(log n) 内统计一段圆弧中的点数。圆弧跨过 0 时拆成两段即可。

题目保证 t = 0t = T 时没有两只变色龙在同一位置,因此端点处不会产生需要额外讨论的冲突。

核心观察三:用相遇次数恢复真实编号

把所有变色龙按初始位置从小到大排序,得到 order[0..n-1]

在幽灵模型中,幽灵直接穿过;在真实模型中,每次相遇相当于两只真实编号交换轨迹。因此:

  • 一只顺时针幽灵每相遇一次,它携带的真实编号在圆环排序中向后移动一位;
  • 一只逆时针幽灵每相遇一次,它携带的真实编号在圆环排序中向前移动一位。

若某个幽灵初始排名为 i,相遇次数为 k

  • 顺时针幽灵最终对应的输入编号是 order[(i + k) mod n]
  • 逆时针幽灵最终对应的输入编号是 order[(i - k) mod n]

于是我们枚举每个幽灵,算出它最终落到哪个真实编号上,再把这个幽灵的最终位置、颜色、方向填给该编号。

幽灵最终位置很直接:

顺时针: (p + T) mod L
逆时针: (p - T) mod L

幽灵最终方向就是它自己的方向。

逆时针幽灵的最终颜色

顺时针幽灵颜色不变。

对一只初始位置为 x 的逆时针幽灵,设 S = 2T。一只初始位置为 y 的顺时针幽灵会在时刻 t 与它相遇,当且仅当:

y = x - 2t (mod L)

t0 增加到 T 时,y 在圆环上扫过从 x - Sx 的一段弧。最后一次相遇,对应的是从

begin = (x - S) mod L

开始,顺时针方向遇到的第一个顺时针幽灵初始位置。

因此做法是:

  1. 在排好序的顺时针位置数组中,二分找到第一个 >= begin 的点;
  2. 如果不存在,就取数组第一个点,表示跨过了 0
  3. 计算它到 begin 的顺时针距离 dist
  4. dist < S,说明这次相遇发生在 (0, T] 内,逆时针幽灵最终颜色变成这只顺时针幽灵的初始颜色;
  5. 否则说明没有相遇,颜色保持初始颜色。

算法步骤

  1. 读入所有变色龙。
  2. 分别保存:
    • 所有编号按位置排序的数组 order
    • 顺时针编号数组和位置数组;
    • 逆时针编号数组和位置数组。
  3. 对每个幽灵按初始位置排名枚举:
    • 用二分统计它与反方向幽灵的相遇次数;
    • 根据相遇次数计算它最终对应哪个输入编号;
    • 计算幽灵最终位置和方向;
    • 若它是逆时针幽灵,再二分找到最后一次影响它颜色的顺时针幽灵。
  4. 按输入编号输出答案。

正确性证明

引理 1:幽灵穿过模型与真实模型的运动位置集合相同。

每次相遇前,两只变色龙方向相反;相遇后,两者都改变方向。若忽略编号,相当于原顺时针轨迹继续顺时针,原逆时针轨迹继续逆时针。速度都为 1,所以所有时刻的位置集合与“直接穿过”完全一致。

引理 2:顺时针幽灵颜色不变,逆时针幽灵颜色等于最后一次遇到的顺时针幽灵颜色。

一次相遇中,原顺时针者颜色不变,原逆时针者变成顺时针者颜色。放到幽灵轨迹上看,就是顺时针轨迹的颜色继续保持,并在相遇时覆盖逆时针轨迹的颜色。所以顺时针幽灵颜色始终不变,逆时针幽灵只需要看最后一次被哪个顺时针幽灵覆盖。

引理 3:相遇次数公式正确。

顺逆两个幽灵的相对速度为 2,在时间 T 内相对路程为 S = 2T。每绕完整一圈长度 L,它们相遇一次,所以贡献 S / L 次;剩余长度 S mod L 内,只有位于对应方向剩余弧段里的反方向幽灵会再贡献一次。二分统计的正是这些点,因此相遇次数正确。

引理 4:相遇次数能正确确定真实编号。

在圆环上相邻的两条相反方向轨迹相遇时,真实编号交换轨迹。对顺时针幽灵来说,每次相遇都会把它最终对应的编号沿初始位置排序向后推进一位;对逆时针幽灵则向前推进一位。取模后得到的 order[(i +/- k) mod n] 正是该幽灵在真实模型中对应的输入编号。

由引理 1 可得位置和方向计算正确;由引理 2 可得颜色计算正确;由引理 3 和引理 4 可得每个幽灵被填入的输入编号正确。因此算法输出的每只输入编号对应变色龙在 t = T 的位置、颜色、方向均正确。

复杂度

排序复杂度 O(n log n)

每只变色龙做常数次二分,复杂度 O(n log n)

总时间复杂度 O(n log n),空间复杂度 O(n)

posted @ 2026-09-08 13:25  Zhenggeek  阅读(5)  评论(0)    收藏  举报