模拟赛1
题解
A
一个字母放在某个按键的第 d 个位置,就会贡献 频数 * d。所有按键提供的可用位置只和深度有关:容量为 a_i 的按键提供深度 1,2,...,a_i 各一个位置。
为了让总贡献最小,应当把出现频数最大的字母放到最小的深度上。若 p_i >= p_j 但 d_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-2 到 i+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 <= x2 且 y1 <= y2,那么方案 A 宽不比 B 大,高也不比 B 大。此时 B 无论放到后面的任何运算里,都不可能比 A 更好,因为 X 和 Y 对宽高都是单调的:输入的宽高变小,输出的宽高不会变大。
这种情况下,我们说 B 被 A 支配,可以直接删掉。
举个例子:
(2,10), (4,6), (5,8), (7,3)
其中 (5,8) 被 (4,6) 支配,因为 4 <= 5 且 6 <= 8,所以 (5,8) 永远没必要保留。
但是 (2,10) 和 (4,6) 谁也不能删:前者更窄,后者更矮,各有优势。(7,3) 也不能删,因为它虽然更宽,但是高度更小。
删掉所有被支配的方案后,剩下的就是 Pareto 前沿。本题里我们把前沿按 x 从小到大排序。这样保留下来的 y 一定是严格递减的:
x 越大,y 必须越小,否则后面的点就会被前面的点支配。
3. 每个子表达式维护什么
对每个子表达式,我们不只保存一个最优数对,而是保存它所有可能结果的 Pareto 前沿。
为什么不能只保存面积最小的一个?
因为当前面积小,不代表以后一定好。例如:
(1,100) 面积 100
(10,10) 面积 100
它们面积一样,但和别的矩形横拼、竖拼时表现不同。更窄或更矮的方案都可能在后面变成最优,所以要保留所有“没有被明显淘汰”的方案。
对于一个叶子,也就是一个变量的可选集合,先把输入给出的所有数对做一次 Pareto 化,删掉被支配的选择。
对于一个运算符,弹出左右两个子表达式的前沿,把它们合并成新的前沿。
整个后缀表达式用栈处理:
- 遇到变量
i,把第i个变量的前沿压栈。 - 遇到运算符
0,弹出右子树和左子树,合并后压回栈。 - 最后栈顶就是整个表达式的前沿,在里面取最小
x*y。
4. 如何 Pareto 化
给出一堆候选数对后,做法如下:
- 按
x升序排序。 - 如果
x相同,只保留y最小的那个。 - 从左到右扫描,维护目前见过的最小
y。 - 当前点的
y如果不小于之前最小的y,它就被前面的某个点支配,删掉。 - 否则保留它。
例如候选点排序后是:
(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:
- 枚举
L和R中出现过的所有高度,生成必要的X候选。 - 枚举
L和R中出现过的所有宽度,生成必要的Y候选。 - 把这些候选放在一起做 Pareto 化。
- 得到当前子表达式的新前沿。
这样每次合并生成的候选数量大约是 |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)
也就是说,X 和 Y 都保持这个偏序关系。一个矩形如果一开始已经被另一个矩形支配,那么无论之后和多少个矩形继续运算,它得到的最终结果也会被对应结果支配,面积不可能更小。
因此,任意子表达式里被支配的矩形都不可能通向全局最优答案,可以删掉。这个结论也可以看成对表达式树往上的归纳:叶子处删除安全;如果左右子表达式的删除安全,那么经过单调的 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. 复杂度
设一次合并的两个前沿大小分别为 s 和 t。
本做法生成 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_bound 在 O(log n) 内统计一段圆弧中的点数。圆弧跨过 0 时拆成两段即可。
题目保证 t = 0 和 t = 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)
当 t 从 0 增加到 T 时,y 在圆环上扫过从 x - S 到 x 的一段弧。最后一次相遇,对应的是从
begin = (x - S) mod L
开始,顺时针方向遇到的第一个顺时针幽灵初始位置。
因此做法是:
- 在排好序的顺时针位置数组中,二分找到第一个
>= begin的点; - 如果不存在,就取数组第一个点,表示跨过了
0; - 计算它到
begin的顺时针距离dist; - 若
dist < S,说明这次相遇发生在(0, T]内,逆时针幽灵最终颜色变成这只顺时针幽灵的初始颜色; - 否则说明没有相遇,颜色保持初始颜色。
算法步骤
- 读入所有变色龙。
- 分别保存:
- 所有编号按位置排序的数组
order; - 顺时针编号数组和位置数组;
- 逆时针编号数组和位置数组。
- 所有编号按位置排序的数组
- 对每个幽灵按初始位置排名枚举:
- 用二分统计它与反方向幽灵的相遇次数;
- 根据相遇次数计算它最终对应哪个输入编号;
- 计算幽灵最终位置和方向;
- 若它是逆时针幽灵,再二分找到最后一次影响它颜色的顺时针幽灵。
- 按输入编号输出答案。
正确性证明
引理 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)。

浙公网安备 33010602011771号