Luogu 10046 [CCPC 2023 北京市赛] 哈密顿
注意到边权的形式是 \(|a_i - b_j|\) 的形式,要同时考虑到正负,但这明显是不想看到的。
结合题目要求的是边权和最大值,那么一个方法就是把 \(|a_i - b_j|\) 转化为最大值的形式去维护。
于是可以考虑拆分为 \(\max\{a_i - b_j, b_j - a_i\}\)。
于是可以转化为,任意钦定 \(a_i, b_i\) 前的正负号,但要求对于边 \(i\to j\),\(a_i, b_j\) 的符号需要相反,求带符号的 \(a_i, b_i\) 之和的最大值。
于是这个时候考虑切换关注的元素,从边转为符号。
即现在考虑,如何分配符号是合法的(存在合法的哈密顿路径),下记两个相邻的符号分别代表对于点 \(i\),\(a_i, b_i\) 的符号:
-
首先可以少关注几种情况。
如果每个点的情况为 \(+-\),那么任意一个哈密顿回路都是合法的;同理,如果每个点的情况都为 \(-+\),任意一个哈密顿回路都是合法的。
-
接下来考虑如果既有 \(+-\) 也有 \(-+\) 的情况。
手玩能够发现如果只有这两种情况是不存在合法的哈密顿回路的,因为需要有一个从 \(+-\) 到 \(-+\) 的“桥梁”。
根据定义知道这个“桥梁”也就是 \(--\)。
但是同时,\(-+\) 也需要能到达 \(+-\),所以也需要“桥梁”\(++\)。同时因为应当满足 \(a_i\) 部分 \(+\) 的数量等于 \(b_i\) 部分 \(-\) 的数量(另一面也是相同的,但是互为补集,所以满足一个即可),可以推出 \(++\) 的数量是与 \(--\) 的数量相等的。
同时如果去尝试去掉一些元素,能发现其实没有 \(+-\) 或 \(-+\) 或都没有的情况下也是存在哈密顿回路的。
梳理一下,可以得到结论:当符号分配存在数量相同的 \(++\) 和 \(--\) 时,剩余的符号可以任意分配为 \(+-\) 和 \(-+\)。
分析完可能的情况,接下来考虑求解最大值。
首先对于第一种情况,只涉及两种小情况,可以直接线性扫一遍求出。
于是着重来考虑第二种情况。
首先注意到结论中对于 \(++\) 和 \(--\) 的限制只是“存在”。
于是一个想法是先令其不存在,然后通过调整使其存在。
首先如果不存在 \(++\) 和 \(--\),那么对于每个点就是选择 \(+-\) 或者 \(-+\)。
那么此时没有关于这个的限制,所以一定会选择最大值。
于是直接钦定 \(a_i\ge b_i\),那么此时 \(i\) 的贡献其实就是 \(a_i - b_i\)。
那么如果 \(i\) 的符号变为了 \(++\),贡献就变为 \(a_i + b_i\),对应的差值就是 \(2b_i\);类似的,如果 \(i\) 的符号变为了 \(--\),贡献就变为了 \(-a_i - b_i\),对应的差值就是 \(-2a_i\)。
于是可以直接考虑把 \(2b_i\) 和 \(-2a_i\) 扔到两个大根堆 \(\operatorname{Q}_1, \operatorname{Q}_2\) 里。
那么只要当两个堆的堆顶之和 \(> 0\),那么此时把这两个 \(+-\) 更换为 \(++, --\),贡献肯定越大。
于是可以直接更换符号,加上堆顶之和并弹出这两个元素继续看后面还能不能换。
首先对于用堆的正确性是比较显然的:如果选了 \(2b_j\),但是存在 \(2b_i > 2b_j\) 没被选,把 \(j\) 替换为 \(i\) 一定更优,即选取的一定是从大到小排序的前缀。
其次就是一个问题:为什么不会出现一个 \(i\) 同时 \(a_i, b_i\) 都被换掉从 \(+-\) 变成 \(-+\) 的情况?
这个可以分讨一下 \(i\) 的情况:
- 此时两个堆堆顶的两个元素恰好为 \(2b_i, -2a_i\),注意到 \(a_i\ge b_i\),所以 \(2b_i - 2a_i\le 0\),一定不会更换符号。
- 此时一个堆堆顶为 \(2b_i\),\(-2a_i\) 还在另一个堆中。
令此时另一个堆的堆顶为 \(-2a_j\),那么此时应该有 \(2b_i - 2a_j > 0\) 也就是 \(b_i > a_j\)。
观察这个堆的形式,发现对于 \(b\) 来说,每一次的堆顶一定会越来越小。
所以假设堆顶为 \(-2a_i\) 时另一个元素为 \(2b_k\),那么有 \(2b_i \ge 2b_k\),又因为 \(a_i\ge b_i\),所以 \(2b_k - 2a_i\le 2b_i - 2a_i\le 0\),一定不会更换 \(a_i\) 和 \(b_k\) 的符号。
接下来贪心的正确性就说明完了,但是注意此时还有一个小情况没有解决:
刚刚的贪心都是基于第一次更换一定更优,一定会在贪心过程中产生 \(++, --\) 的情况的。
但是如果第一次更换就不优又怎么处理?
这是更为简单的,因为选取任何 \(i, j\) 更换符号都不优,这说明只会更换一次符号(更换更多次一定更劣)。
那么就可以直接枚举符号更换为 \(++\) 的 \(i\),那么此时贪心的,符号更换为 \(--\) 的 \(j\) 一定是满足 \(i\not = j\) 中 \(a_j\) 最小的 \(j\)。
预处理出 \(a\) 中的最小值和次小值即可线性维护。
时间复杂度瓶颈在贪心所需要的堆上,时间复杂度 \(\mathcal{O}(n\log n)\)。
感觉思路写出来有点太抽象了,其实懂了的话代码是很好写的!
#include<bits/stdc++.h>
using ll = long long;
constexpr int maxn = 1e5 + 10;
int n;
ll a[maxn], b[maxn];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%lld%lld", &a[i], &b[i]);
ll sum1 = 0, sum2 = 0;
for (int i = 1; i <= n; i++) sum1 += a[i] - b[i], sum2 += b[i] - a[i];
ll ans = std::max(sum1, sum2);
for (int i = 1; i <= n; i++) {
if (a[i] < b[i]) std::swap(a[i], b[i]);
}
ll sum = 0;
for (int i = 1; i <= n; i++) sum += a[i] - b[i];
std::priority_queue<ll> q1, q2;
for (int i = 1; i <= n; i++) q1.push(b[i]), q2.push(-a[i]);
if (q1.top() + q2.top() > 0) {
while (! q1.empty() && q1.top() + q2.top() > 0) {
sum += (q1.top() + q2.top()) * 2ll, q1.pop(), q2.pop();
}
ans = std::max(ans, sum);
} else {
ll mn = 1e9, cmn = 1e9;
for (int i = 1; i <= n; i++) {
a[i] < mn ? (cmn = mn, mn = a[i]) : (cmn = std::min(cmn, a[i]));
}
for (int i = 1; i <= n; i++) {
ans = std::max(ans, sum + b[i] * 2ll - (a[i] == mn ? cmn : mn) * 2ll);
}
}
printf("%lld\n", ans);
return 0;
}
浙公网安备 33010602011771号