JOISC2017 切符の手配题解
形式化题意:
给定正整数 和 个三元组 ,你有一个序列 ,初始所有元素为 。
你需要对序列 进行 次操作,对于第 次操作,你需要:
- 选择任意一个整数 。
- 将所有 的 加上 。
- 将所有 的 加上 。
请你最小化 的值。
思路:
推性质神仙题。
这道题,某些人通过猜结论,调着调着就过拍了,然后交上去过了。
不得不说,这道题的性质太妙了,感觉只能通过打表看出来……
:
我们不妨先抛开 的情况,只考虑 时如何做。这样我们将问题转化成,初始每个区间都未反转,我们需要反转一些区间,使得操作后 尽可能地小。这里,我们定义反转一个区间 为,将其对全集 取补,变为集合 。
记多重集 为我们选出的需要反转的区间的集合,下文我们同时称 为一个反转方案。
性质 :答案具有单调性。我们记答案的上界为 。如果存在一种反转方案,使得 ,则对于所有 ,也存在一种反转方案使得 。
这说明,我们可以尝试用二分答案解决这个问题。
性质 :若 中的所有区间之并为空,则这种反转方案一定不优。
证明:显然,此时存在区间 ,满足 ,如果我们同时反转这两个区间,我们讨论反转后产生的贡献:
- 所有 的 不变。
- 所有 的 会加上 。
由于反转后没有 会减少,所以这种反转方案不优。
也就是说,如果这 个区间存在一个子集有交,则存在一种最优方案,使得存在下标 ,其中区间 为 中的所有区间之并。
我们尝试二分答案 。接下来我们讨论如何判定 是否合法。
我们记 表示所有区间不反转时,所有操作进行完后 的值, 表示对于当前集合 ,将 中的区间反转后,所有操作进行完后 的值。记 ,则 。由于 ,我们得到 。也就是说,要使得 合法,必须存在一个 ,使得通过其推导出的 满足
此时我们能够发现,如果我们确定了 和 ,则我们可以确定所有 的下界,即每个下标至少需要被 中的多少个区间覆盖。这时,找出一个满足条件的 也就变得容易了。
至此,我们可以通过枚举 和 的值,然后贪心地每次将 取得尽量小。具体地,我们计算出每个 的下界,然后依次遍历 ,尝试将 累加到下界(累加不到则当前 不合法),每次取区间时,考虑从左端点小于等于 的区间中贪心地选出右端点最大的加入 。遍历完后,只需检查 是否不小于下界即可。这样我们就设计出了一个判定 是否合法的朴素算法,外层再套一个二分即可得出答案。
注意,因为 ,所以对于任意 , 同时需要满足 ,即 。也就是说,当 时我们可直接判定 不合法。如果我们枚举 时不从下界 开始,并且在判定 是否合法时,仅仅只通过判定 是否达到下界验证 的合法性,则算法的正确性无法保证。(想一想,为什么)
对于 的情况,二分答案的时间为 ,枚举 的时间为 ,贪心算法的时间为 ,则总时间复杂度为 。
对于 的情况,记 ,则二分答案的时间为 ,枚举 的时间为 ,贪心算法的时间不变(在代码中非常自然地改一下即可),则总时间复杂度为 。
这里给出贪心算法的代码:
// 所有变量名与之前约定的记号含义相同 // 所有三元组 (li,ri,ci) 都存储在 vec[li] 中 // 保证 zt 从下界 max(pi) - x 开始枚举 bool valid(long long x, int t, long long zt) { for (int i = t + 1; i <= n; i ++) { z[i] = 0; } std::priority_queue<Interval> q; long long zi = 0; for (int i = 1; i <= t; i ++) { long long lim = (p[i] + zt - x + 1) >> 1; for (int j = 0; j < vec[i].size(); j ++) { if (vec[i][j].r > t) { q.push(vec[i][j]); } } while (q.size() && zi < lim) { Interval iter = q.top(); q.pop(); int delta = std::min(lim - zi, (long long) iter.c); zi += delta; z[iter.r] -= delta; if (iter.c -= delta) { q.push(iter); } } if (zi < lim) { return false; } } z[t] = zi; for (int i = t + 1; i <= n; i ++) { if (((z[i] += z[i - 1]) << 1) < p[i] + zt - x) { return false; } } return true; }
其中 Interval
类和变量 vec
的定义如下:
class Interval { public: int l, r, c; bool operator < (const Interval& rhs) const { return r < rhs.r; } }; std::vector<Interval> vec[maxn];
比较明显的是,如果略去枚举 的时间,总时间复杂度为 ,在该题目限制下可以接受。最优的 只有 种?或是说,我们只需要枚举 个 ?
:
贪心算法的时间复杂度看起来已经不可优化了,我们尝试挖掘更多的性质,优化枚举 的时间。
性质 :存在一种最优方案,满足 或 。
证明:我们假设命题不成立,并尝试推导出矛盾。根据条件,当前最优方案满足 。
首先我们证明,当前方案中不存在区间 使得 。
实际上,假设存在这样的区间,我们考虑将 从 中删除,即不翻转 。记 为删除 后的多重集 , 为删除 后的 。我们分析 与 的关系:
- 对于所有 ,。
- 对于所有 ,。
显然, 满足 ,则 。为了保证 的最优性,我们只好让 ,由此可推导出 ,进而得到 ,此与假设矛盾。因此,当前方案中不存在这样的区间。同时,我们可以得知当前方案中, 中一定存在一个左端点为 的区间和一个右端点为 的区间(其中 ,且这两个区间不同)。
现在,我们考虑以下调整算法:
- 如果当前 或者已满足 ,结束调整。
- 否则,我们从 中分别选择一个左端点为 的区间和一个右端点为 的区间,然后将它们从 中删除。显然,这样调整后的方案仅仅只对于所有 的 增加了 ,其它的 不增,不劣于最优方案。然后我们回到步骤 1。
我们对当前方案进行以上调整算法,然后得到新的多重集 。
若当前方案 仍然有 ,一方面,我们容易知道 满足 ;另一方面,我们在之前已经证明,若当前方案满足 ,则不存在区间 使得 ,因此 。综合这两方面,我们得知 。
所以,我们仍然从 中分别选择一个左端点为 的区间和一个右端点为 的区间,然后将它们从 中删除。容易知道这样的调整一定不劣于最优方案。调整后 ,此与假设矛盾。
综上所述,命题成立。
这说明我们不需要大范围地枚举 了。若当前位置 存在最优方案满足 ,我们只需要判定 和 两种情况中有其中一个合法即可。
总时间复杂度降至 。
:
既然我们优化掉了枚举 的时间,那么 是不是也可以像 一样,不需要枚举呢?我们随便猜一个结论然后跑一遍,可以发现:
性质 :令 ,若存在一种最优方案 满足 ,则 同时满足 。
证明:仍然考虑反证。若存在最优方案 满足 ,则我们可推导出 ,因此 中至少存在一个区间没有覆盖到 ,即 。我们得到
注意到,,所以我们尝试对上述不等式做变换得到
此与假设条件 矛盾。因此命题成立。
至此,我们又将 的范围缩小到 。
既然都到这一步了,我们继续猜测,是不是我们只要让 满足 就能够找到满足之前性质的最优解(找不到则 为最优方案之一)?
性质 :若存在一种最优方案 满足 ,则 同时满足 。
证明:类似性质 的证明,我们考虑反证。令 ,若当前最优方案 满足 ,且存在 满足 ,则我们有
综合上述不等式可得
此与假设条件 矛盾。因此命题成立。
据性质 ,我们可推断出,若存在一种最优方案 满足 ,则对于所有 的 , 都相同(想一想,为什么)。所以,我们只需要令 为集合 的其中一个元素,在二分时对于 和 两种情况分别跑一遍贪心算法,即可做到判断当前答案 是否大于等于最优解。
总时间复杂度降至 ,足以通过此题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现