P11191 「KDOI-10」超级演出 解题报告
P11191 「KDOI-10」超级演出 解题报告
1. 题目解读与分析
首先,我们来深入理解题目的核心规则。这是一个在有向无环图(DAG)上进行的游戏。
- 场景: 一个包含 \(n\) 个点 \(m\) 条边的有向无环图。节点1是唯一的终点(舞台),其余节点(\(2, \dots, n\))是候场室,每个候场室初始都有一个剧团。
- 行动: 按给定的命令序列 \(a_1, \dots, a_k\) 对候场室发布出场命令。
- 出场条件: 对候场室 \(u\) 的命令有效,必须满足两个条件:
- \(u\) 的剧团还在候场室(未出场)。
- 存在一条从 \(u\) 到舞台 \(1\) 的路径,这条路径上所有中间的候场室都已空(即它们的剧团都已成功出场)。
- 目标: 对于 \(q\) 次独立的询问,每次给出一个区间 \([l, r]\),我们需要计算如果只执行 \(a_l, \dots, a_r\) 这一系列命令,最终会有多少剧团留在候场室。
由于每次询问都是独立的,且数据规模很大,为每次询问都进行暴力模拟是不可行的。我们必须找到一个更高效的预处理和查询方法。
2. 核心思路:将问题转化为依赖关系
问题的关键在于“出场”这个行为的依赖性。一个剧团 \(u\) 能否出场,取决于它下游某个或某些剧团是否已经出场。我们可以将这个依赖关系量化。
我们换一个角度思考:对于第 \(i\) 个命令 \(a_i\)(在 \(1, \dots, k\) 的完整序列中),它要成功执行,需要满足什么样的前提?
这个前提是,它所依赖的路径上的所有剧团,都必须收到过成功的出场命令,并且这些命令的时间点都必须在 \(i\) 之前。为了让条件最容易满足,它会选择那条“前提最不苛刻”的路径。
我们可以为每个命令 \(a_i\) 预计算一个“前提值”,记作 \(w_i\)。
\(w_i\) 的定义:要让第 \(i\) 个命令 \(a_i\) 在某个询问区间
[l, r]
中成功,这个区间的起始点l
必须满足l <= w_i
。换句话说,\(w_i\) 是保证 \(a_i\) 能够成功执行所需要的最晚的那个前置命令的起始时刻。
如果对于一个询问 \([l, r]\),我们想知道命令 \(a_i\)(其中 \(l \le i \le r\))是否有效,只需要检查它的前提条件是否被满足,即 \(l \le w_i\)。
只要我们能预计算出所有 \(w_i\),问题就变得清晰多了。
3. 如何计算前提值 \(w_i\)
我们按顺序(\(i\) 从 1 到 \(k\))来计算每个 \(w_i\)。
-
基础情况:如果候场室 \(a_i\) 能直接到达舞台 1(即存在边 \(a_i \to 1\)),它不依赖任何其他剧团。它的成功只取决于它自己被命令。因此,它的前提就是它自己,我们定义 \(w_i = i\)。
-
递推情况:如果 \(a_i\) 不能直接到舞台 1,它必须经过某个下游的候场室 \(v\)。为了让 \(a_i\) 能出场,它需要至少一条通往舞台的路径是通畅的。假设它选择经过 \(v\),而 \(v\) 在 \(i\) 之前最后一次被成功命令的前提是 \(w_j\),那么 \(a_i\) 的成功就间接依赖于 \(w_j\)。
由于 \(a_i\) 可以选择任意一条通畅的路径,它会选择那个“前提条件最容易满足”的路径。“容易满足”意味着对起始点 \(l\) 的约束最宽松,即 \(l \le W\) 中的 \(W\) 尽可能小。但这里是“或”的关系,比如可以走依赖前提 \(W_1\) 的路,或者走依赖前提 \(W_2\) 的路,只要满足 \(l \le W_1\) 或 \(l \le W_2\) 即可,这等价于 \(l \le \max(W_1, W_2)\)。因此,我们需要取所有下游路径中,那个最苛刻的前提。
我们可以得到 \(w_i\) 的计算公式:
\(w_i = \max_{v \in \text{后继}(a_i)} \{ w_j \}\),其中 \(j\) 是在 \(i\) 之前对 \(v\) 的最后一次命令的索引。为了方便计算,我们用一个数组
rev[u]
来记录节点u
最后一次被命令时计算出的 \(w\) 值。公式简化为:
w[i] = max(rev[v])
,其中v
是a[i]
的所有直接后继。
加速计算:根号分治
直接按上述方法计算,若某个节点度数很大,复杂度会很高。这里可以使用根号分治优化:
- 设定阈值 \(B\)(如 \(\sqrt{m}\))。
- 小度节点 (度数 < B):后继少,直接遍历后继,使用
rev
数组计算 \(w_i\)。 - 大度节点 (度数 ≥ B):后继多,反向更新。我们为每个大度节点 \(p\) 维护一个值
tw[p]
,记录其所有后继中最大的rev
值。每当计算完一个 \(w_i\) (针对节点 \(a_i\)),我们就去更新 \(a_i\) 的所有大度前驱的tw
值。当需要计算一个大度节点自身的 \(w\) 值时,直接取tw
即可。
4. 解答询问:二维数点问题的转化与求解
预处理完所有 \(w_i\) 后,对于每个询问 \([l, r]\),问题转化为:
统计在 \(a_l, \dots, a_r\) 中,有多少个不同的候场室 \(u\),满足存在某个 \(i \in [l, r]\) 使得 \(a_i=u\) 且 \(w_i \ge l\)。
这是一个典型的二维数点问题,可以用离线处理 + 树状数组(BIT) 高效解决。
4.1 转化为二维平面上的点
我们可以将每个命令 \(a_i\) 想象成一个二维平面上的点 \(P_i(i, w_i)\):
- x 轴 代表命令的时间
i
。 - y 轴 代表命令的前提值
w_i
。
一个询问 [l, r]
就是要统计满足下面条件的点的种类数(不同候场室):
- \(l \le i \le r\) (点的 x 坐标在查询区间内)
- \(l \le w_i\) (点的 y 坐标满足前提)
这个问题的难点在于如何处理“种类数”,即对同一个候场室的多个满足条件的点只计数一次。
4.2 离线扫描线 + 树状数组
这正是“离线扫描线”算法的用武之地。
- 离线处理:将所有 \(q\) 个询问存储起来,不立即回答。将它们按右端点
r
从小到大排序。 - 扫描与处理:我们从 \(i=1\) 到 \(k\) 遍历命令序列,可以想象一条竖直的“扫描线”从左向右移动。当扫描线移动到位置
i
时,我们处理第i
个命令,并回答所有右端点 \(r=i\) 的询问。 - 树状数组维护“活跃点”:为了处理“种类数”的限制,我们只关心每个候场室最后一次被命令的信息。我们用树状数组来维护这些“活跃点”的
w
值分布。- 更新:当扫描线处理到第 \(i\) 个命令
a[i]
时,其对应的点是(i, w[i])
。- 如果候场室
a[i]
之前出现过(假设其上一个活跃点的w
值为old_w
),说明旧的活跃点(j, old_w)
(\(j<i\))已过时。我们在树状数组的old_w
位置减 1,移除它的贡献。 - 然后,我们将新的活跃点
(i, w[i])
加入。在树状数组的w[i]
位置加 1。 - 我们用一个
lst[u]
数组记录每个候场室u
当前活跃点的w
值。
- 如果候场室
- 查询:当扫描线移动到 \(r\) 时,我们回答所有右端点为 \(r\) 的询问 \((l, r)\)。
- 此时,树状数组里存储了所有在 \(1 \dots r\) 时间内出现的候场室的、最新的
w
值信息。 - 我们要查询的是,这些活跃点中有多少个满足
w >= l
。 - 这在树状数组上是一个范围查询:
总活跃点数 - w值小于l的点数
。 - 具体公式为:
ask(k) - ask(l-1)
(其中ask(x)
查询w <= x
的点的数量,k
是w
的最大可能值)。 - 这个结果就是成功出场的剧团数量。
- 最终答案为:总剧团数 \((n-1)\) - 成功出场数。
- 此时,树状数组里存储了所有在 \(1 \dots r\) 时间内出现的候场室的、最新的
- 更新:当扫描线处理到第 \(i\) 个命令
5. 算法流程总结
-
预处理
w
数组:- 遍历图,根据度数将节点分为大度和小度。
- 从 \(i=1\) 到 \(k\) 遍历命令序列 \(a\)。
- 使用根号分治策略,结合
rev
(记录节点最新w值) 和tw
(为大度节点服务) 数组,计算出每个 \(w_i\)。
-
回答询问:
- 将所有询问按右端点 \(r\) 排序。
- 初始化一个树状数组和记录节点最新
w
值的lst
数组。 - 从 \(i=1\) 到 \(k\) 进行扫描:
a. 处理命令a[i]
:更新树状数组,将a[i]
的旧贡献(如果存在)移除,加入w[i]
的新贡献,并更新lst[a[i]]
。
b. 处理所有右端点 \(r=i\) 的询问(l, i)
:利用树状数组计算出场剧团数,公式为当前总贡献数 - ask(l-1)
。
c. 计算剩余剧团数(n-1) - 出场数
,并存储答案。 - 按原始顺序输出所有答案。
该算法通过巧妙的预处理和问题转化,将复杂的模拟过程变成了一个高效的、可离线处理的二维数点问题,最终通过树状数组在优秀的复杂度内解决了问题。