题目大意
题目总结:D. Doorway
说白了就是告诉你滑动门两边的墙壁,告诉你从左到右的滑动门的这个长度。然后有好几层的这样子的子的滑动门系统问你这样子的系统能够张开的最大口子长度是多少。
问题描述
在一个多层滑动门系统中,共有 层。每一层可以看作一个水平区间,由左侧墙壁 和右侧墙壁 限定。每一层包含若干扇固定长度的滑动门,门可以在该层的墙壁范围内自由左右滑动,但不能相互重叠,也不能越过墙壁。
目标
寻找一个最大的开口(Opening)。开口定义为一个水平区间,使得在所有层中,该区间内的任何位置都没有任何门或墙壁遮挡。你需要通过调整每一层门的位置,使得这个公共开口的长度最大化。
输入格式
-
第一行:层数 ()。
-
接下来的 行,每行描述一层:
- 三个整数:门数量 、左墙坐标 、右墙坐标 ()。
- 个整数:该层从左到右每扇门的长度 。
-
数据保证 。
输出格式
- 一个整数,表示通过移动门所能达到的最大公共开口长度。
样例解释
样例 1
-
输入:
- 第 1 层:墙在 ,门长 3 和 2。
- 第 2 层:墙在 ,门长 1, 1 和 2。
-
逻辑:
所有层的共同墙壁限制区间为 ,即 ,长度为 7。- 在第 1 层中,如果将长为 3 的门推到最左,长为 2 的门推到最右,中间会留出空隙。
- 在第 2 层中,通过合理分配门的位置,可以使得在 这个范围内,所有层共同暴露出的最大空白区间长度为 4。
样例 2
-
输入:
- 第 1 层:墙在 ,门长 2 和 4(总长 6,空隙 1)。
- 第 2 层:墙在 ,门长 4(总长 4,空隙 1)。
-
逻辑:
第一层有效范围 ,第二层有效范围 ,它们的交集是 ,长度仅为 3。由于两层中都存在长度大于或等于交集长度的门或门组限制,无法在所有层中腾出一个完全没有遮挡的公共区间。 -
输出: 0
注意
这个示例展示了第一个例子的解决方案。墙壁填充黑色,门填充各种灰色,开口是白色。当每层的第一个门向左移动,其余的门向右移动时,我们得到了最大的开口为4。
.png)
思路讲解
还是一样的思路啊,就是当你发现你只知道起始位置的时候,你会发现计算这个东西啊,其实不是很容易。
当然,我们上面所指的起始位置,其实是指的总体的起始位置。考虑到,虽然说总体的起始位置,知道总体的起始位置,然后我们去进行一个这个操作,它其实也是需要 吧。
那我们曾经提及过非常多次,当我们发现采用总体的想法碰壁的时候,我们应该怎么样?我们应该采用更加细颗粒度的东西,是不是?因此我们可以采用每一个起始位置,就是每一个滑窗系统的起始位置,去解决以及考虑这个问题。
这道题维护的是:每层的"gap 右端点",一共 个值。
具体来说,multiset 里始终有 个数,第 层占一个位置,表示"在当前扫描位置下,第 层的空隙最远能延伸到哪里"。
我画一下扫描过程,用样例来说明。
样例模拟
1 | 2 |
层 1:,,门 ,。gap-start 候选:,对应 rightend:。
层 2:,,门 ,。gap-start 候选:,对应 rightend:。
所有事件排序:
| 扫到 | 属于哪层 | 新 rightend | multiset 状态 | |
|---|---|---|---|---|
| 层1 | 层2还没激活 | |||
| 层2 | ||||
| 层1 | ||||
| 层2 | ||||
| 层2 | ||||
| 层1 | ||||
| 层2 | ||||
| 答案 = ,在 时取到。 | ||||
| multiset 里的含义:在扫描位置 时,层 1 的空隙最远能到 ,层 2 的空隙最远能到 。取最小值 ,opening = 。 | ||||
| 所以维护的东西就是这 个"每层能延伸到多远"的值,用 multiset 随时拿最小值。 |
为什么这道题目你需要去塞一个负无穷呢?是这样子的,这个是因为啊,我们另外一道题目就是这个 brick work 是处理这个不相交问题,但这道题目我们处理的是相交问题。不相交问题你自然是还没有启用的区间,你自然是不用往里面去塞的。但是集合相交问题,还没有启用的集合,需要往里塞一个未启用值,就是未生效值。因为它都没启用,它肯定是不相交的对吧,为了避免这种情况影响我们的最终判断,我们需要往里面塞一个未启用值?
1 | multiset<ll> ri_st; |
AC代码
AC
https://codeforces.com/contest/2181/submission/362349671
1 | /** |
心路历程(WA,TLE,MLE……)
好,我来认真讲一下这道 Doorway 的扫描线,以及和 Brickwork 的对比。
Doorway 扫描线的精妙之处
这道题的扫描线,最精妙的地方有三层递进:
第一层:观察到 gap 宽度是定值
每层的门总长是固定的,墙的区间也是固定的,所以每层的空隙宽度 是一个常数。不管你怎么滑动门,空隙的大小不变,变的只是空隙的位置。
这意味着原问题从"怎么安排门"变成了"从每层选一个空隙位置,使得所有层的空隙的交集最大"。
第二层:最优 st 必在 gap-start 处(这个说白了傻子都知道,所谓的gap start就是每层都有K种选择,纯纯是傻逼。)
opening 的左端点 st 如果确定了,那每层的最优策略就是:把空隙尽量往右推(把尽可能多的门放到 st 左边),这样右端点 最大。
关键性质:在两个相邻的 gap-start 之间,所有层的 rightend 都不变,但 st 在递增,所以 严格递减。
结论:最优 st 必然是某层的某个 gap-start 位置。候选点总共就 个。
第三层:扫描线 + multiset 做到 O(sum_k * log n)
如果对每个候选 st 暴力算所有层的 rightend,复杂度是 ,太慢。
精妙之处:把所有候选 st 排序后从左到右扫描,每次只有一层的 rightend 发生变化(就是当前 gap-start 所属的那一层)。用 multiset 维护所有层的 rightend, 就能查到全局最小值。
这和 Brickwork 里的扫描线"每次只有一个矩形进入/离开 activates 集合"是同一个道理。
和 Brickwork 的对比
| Brickwork(砖墙) | Doorway(门) | |
|---|---|---|
| 消除的维度 | 扫描 x 坐标,把 2D 矩形重叠问题变成 1D 区间重叠问题 | 扫描 gap-start 位置,把"多层选位置求交集最大"变成"逐步更新单层 rightend" |
| 事件类型 | 矩形左边进入(op=1)、右边离开(op=0) | gap-start 出现(更新该层的 rightend) |
| 维护的集合 | set<pair<ll,ll>> 维护当前所有激活的 y 区间,保证互不重叠 |
multiset<ll> 维护每层当前的 rightend,追踪全局最小值 |
| 每次事件的操作 | 插入时检查前驱/后继是否重叠;删除时直接 erase | 更新一层的 rightend(erase 旧值 + insert 新值),查 *begin() |
| 核心不变量 | activates 中所有区间互不重叠 | multiset 中恰好 N 个值,每层一个 |
| 判定条件 | 每次插入都不和邻居重叠 → yes | *begin() - g 的最大值就是答案 |
相同点
- 都是把高维问题降到一维:Brickwork 把 2D 重叠检测降到 1D 区间检测;Doorway 把"多层 gap 位置选择"降到一维扫描。
- 每次事件只影响一个元素:Brickwork 每次只有一个区间进/出;Doorway 每次只有一层的 rightend 变化。这是扫描线高效的根本原因。
- 都依赖有序集合:利用
set/multiset的 操作来维护全局信息。
不同点
- 目标不同:Brickwork 是判定问题(有没有重叠);Doorway 是最优化问题(最大 opening)。
- 集合语义不同:Brickwork 的
activates存的是当前活跃的区间,需要保证互不重叠,所以只需查前驱后继;Doorway 的 multiset 存的是每层一个标量值(rightend),需要的是全局最小值,所以用begin()。 - 事件的对称性:Brickwork 有"进入"和"离开"两种事件,是对称的;Doorway 只有一种事件(gap-start),每层的 rightend 只增不减,没有"离开"。
1 | /** |
1 | for (int i = 1; i <= N; ++i) { |