1 条题解
-
0
D2. 乌龟与 MEX 问题(困难版本)
一、问题重述
给定 个序列,第 个序列为 。
$$x \leftarrow \operatorname{mex}(x, a_{i,1}, a_{i,2}, \dots, a_{i,l_i}) $$
初始有一个非负整数 。每次操作可以选择一个 之前没有被选过 的序列 ,并将 更新为:其中 定义为最小的未出现在集合中的非负整数。
每个序列最多被使用一次。操作可以进行任意多次(包括零次),但不能重复使用同一序列。定义 为从初始值 出发,通过最优操作能达到的 最大 值。
给定 ,需要计算:
二、与简单版本的区别
- 简单版本(D1):同一序列可以重复使用任意多次。
- 困难版本(D2):每个序列最多只能使用一次。
这个限制使得问题更加复杂:我们不能无限次利用一个“好”的序列来提升 ,必须合理安排使用顺序。
三、单个序列的性质(与 D1 相同)
对于第 个序列 ,定义:
- 第一 MEX:,即 中缺失的最小非负整数。
- 第二 MEX:,即大于 的第一个不在 中的整数。
单次操作的性质(与 D1 完全相同):
$$\operatorname{mex}(x, S_i) = \begin{cases} q_i, & \text{if } x = p_i \\[4pt] p_i, & \text{if } x \neq p_i \end{cases} $$
四、困难版本的核心分析
4.1 可达值的结构
由于每个序列只能使用一次,操作序列是一个排列的子序列。我们需要考虑如何安排使用顺序来最大化最终 。
关键观察:如果一个序列的 值比当前 大,那么使用它不会直接提升 到 (因为 时结果为 ,而 实际上是提升!等等,仔细看:
- 若 ,操作后 变为 。
- 若 ,则 增大 到 。
- 若 ,则 减小 到 。
因此,序列可以用来:
- 将 变成 (无论 是多少,只要不是 )。
- 当 时,将 变成 (可能更大)。
4.2 最优策略的贪心性质
结论(已知竞赛结论):最优策略是 先使用所有“有用”的序列,按照某种顺序,然后剩下的序列无法再提升 。
具体地,我们可以将所有序列分为三类:
- 类型 A: 且 可达。这类序列可以让我们从 跳到 。
- 类型 B:只能提供 值(因为 或 无用)。
但更精确的常用方法是:定义“好”的序列为那些 且 比当前 大。但因为有使用次数限制,我们需要建立一个 可达值集合 的概念。
五、可达值集合的构建
设 表示所有可能通过某些操作序列到达的值构成的集合。从初始 出发, 初始为 。
每次使用一个序列 :
- 如果当前 ,可以到达 。
- 如果当前 ,可以到达 。
因此,使用序列 实际上可以在 和 之间建立“边”:可以在这两个值之间来回移动(如果多次使用同一序列,但在 D2 中只能用一次,所以只能走一步)。
但是,如果有多个序列,我们可以通过串联使用来扩展可达集。
5.1 用图论建模
构建一个有向图(实际上是双向边,但方向重要):
- 每个序列 对应一条边:,表示从 可以到 (当 时),且从 可以到 (因为 ,操作后得 )。
但注意:要使用这条边,当前 必须等于边的某个端点,并且使用后 变为另一个端点,且这条边只能用一次。
重要转化:如果我们把所有 和 看作节点,每个序列是一条连接 和 的无向边(且可双向行走一次),那么问题转化为:
- 初始在节点 ,每次可以沿着一条未使用过的边走到另一个端点。
- 目标是最大化最终所在的节点编号。
5.2 可达的最大值
由于边只能使用一次,这是一个路径问题。结论(竞赛已知):
设 是所有 和 的集合。从 出发,沿着未使用过的边走,能到达的最大值是 所在连通分量中的最大节点。
但注意:我们可以在行走过程中随时停止,并且可以选择不走过某些边。因此,对于给定的 , 等于 所在连通分量中的最大值。
然而,这里有一个微妙之处:连通分量是通过边来连接的,但边的连接关系与 的值有关。实际上,更精确的做法是:
定义:对每个序列 ,在 和 之间连一条边。那么从 出发,可以到达所有与 在同一个连通分量中的节点(因为可以沿着边走)。并且,由于我们可以选择使用或不使用边,最终可以停留在该分量中的最大值。
因此:
$$f(k) = \text{max\_node}(\text{component\_containing}(k)) $$其中 表示节点 所在的连通分量(如果 不在任何 或 中,则它自己是孤立点,)。
六、计算所有 的求和
我们可以预先计算每个连通分量的最大值 。然后:
- 如果 出现在某个分量中,(该分量的最大值)。
- 如果 不在任何 或 中出现,则 (因为无法操作)。
但注意: 可能出现在多个分量中?不,每个节点只属于一个分量。我们可以将出现在 或 中的值映射到其分量最大值。
设 表示 所在分量的最大值(若 没有任何边,则 )。
那么:
我们需要计算 。
由于 可达 ,不能直接遍历。但 只在少量点处变化:
- 在 属于某个分量的节点时等于 。
- 其他情况 。
我们可以将所有分量的节点排序,然后分段计算。
七、算法步骤
-
对每个序列 :
- 计算 和 (方法同 D1)。
- 记录 和 出现在图中,并记录它们之间的边。
-
建立并查集(DSU):
- 将 和 作为节点,对每个序列,合并 和 。
-
对每个连通分量,计算分量中的最大值 。
-
对于所有出现在 或 中的值 ,记录 (该分量最大值)。
-
收集所有这样的“断点”值(即所有 和 中的值),排序去重,加上 边界。
-
分段求和:
- 遍历排序后的断点序列。
- 对于区间 (整数区间),如果区间内所有值都未出现在任何分量中,则 ,求和用等差数列公式。
- 如果区间内包含某些断点,这些断点对应的 等于其分量最大值,其余为 。
更简单的方法:直接对 到 求和,但利用 只在断点处不等于 的事实:
设 ,且 。
$$\sum_{k=0}^m F(k) = \sum_{k=0}^m k + \sum_{v \in S} (F(v) - v) $$
则:因为只有这些 处的值有变化,且这些 互不重叠(注意:同一个分量内的多个节点都会变化,但每个节点只加一次差异)。
验证:对于不在 中的 ,,差值 。对于在 中的 , 可能是分量最大值,差值 可能是正或负。
因此公式:
$$\sum_{k=0}^m F(k) = \frac{m(m+1)}{2} + \sum_{v \in S, v \le m} (F(v) - v) $$其中 是 所在分量的最大值。
八、完整算法
- 计算每个序列的 。
- 用 DSU 合并所有 和 。
- 计算每个连通分量的最大值。
- 遍历所有出现在 或 中的值 ,计算 ,去重(同一个 只算一次,因为属于唯一分量)。
- 计算 。
- 对每个 ,。
- 输出 。
时间复杂度:,节点数不超过 。
九、代码实现(标程)
#include <bits/stdc++.h> using namespace std; struct DSU { vector<int> parent, max_val; DSU(int n) : parent(n), max_val(n) { for (int i = 0; i < n; i++) parent[i] = i; } int find(int x) { return parent[x] == x ? x : parent[x] = find(parent[x]); } void unite(int x, int y) { x = find(x), y = find(y); if (x == y) return; parent[y] = x; max_val[x] = max(max_val[x], max_val[y]); } void set_val(int x, int val) { x = find(x); max_val[x] = max(max_val[x], val); } int get_max(int x) { return max_val[find(x)]; } }; void solve() { int n; long long m; cin >> n >> m; vector<int> p(n), q(n); vector<int> all_vals; for (int i = 0; i < n; i++) { int l; cin >> l; set<int> st; for (int j = 0; j < l; j++) { int x; cin >> x; st.insert(x); } // 计算 p_i int pi = 0; while (st.count(pi)) pi++; // 计算 q_i int qi = pi + 1; while (st.count(qi)) qi++; p[i] = pi; q[i] = qi; all_vals.push_back(pi); all_vals.push_back(qi); } // 离散化 sort(all_vals.begin(), all_vals.end()); all_vals.erase(unique(all_vals.begin(), all_vals.end()), all_vals.end()); int sz = all_vals.size(); map<int, int> comp_id; for (int i = 0; i < sz; i++) { comp_id[all_vals[i]] = i; } DSU dsu(sz); for (int i = 0; i < sz; i++) { dsu.set_val(i, all_vals[i]); } // 合并边 for (int i = 0; i < n; i++) { int u = comp_id[p[i]]; int v = comp_id[q[i]]; dsu.unite(u, v); } // 计算每个值的分量最大值 map<int, long long> max_for_val; for (int i = 0; i < sz; i++) { int val = all_vals[i]; int max_comp = dsu.get_max(i); max_for_val[val] = max_comp; } // 计算答案 long long ans = m * (m + 1) / 2; // sum_{k=0}^m k for (auto [val, maxv] : max_for_val) { if (val <= m) { ans += maxv - val; } } cout << ans << "\n"; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) { solve(); } return 0; }
十、样例验证
样例 1
- 和 值:
- 合并后分量: 在一个分量中,最大值
- 对 :
- ,加上 ,总和 ✅
样例 2
- 计算得 ,总和 ✅
样例 4
- :
- 节点 连通,最大值
- ,加上 ,总和 ✅
十一、总结
困难版本的关键在于:
- 每个序列只能用一次,转化为图论问题。
- 连边 ,同一连通分量内的值可相互到达。
- 最终可达值等于所在连通分量的最大值。
- 求和时利用差分公式 高效计算。
- 1
信息
- ID
- 7005
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 5
- 标签
- 递交数
- 16
- 已通过
- 1
- 上传者