【带权并查集+贪心+构造】codeforces 1244 G. Running in Pairs
题目
https://codeforces.com/contest/1244/problem/G
题意
输入两个正整数 \(n,k(1 \leq n \leq 10^6, 1 \leq k \leq n^2)\)。
你需要构造两个 \(1 \text{~} n\) 的排列 \(p, q\),在满足 \(sum = \sum_{i=1}^{n}{max(p_i,q_i)} \leq k\) 的条件下将 \(sum\) 最大化。
最后,若存在这样子的两个排列 \(p, q\),则输出两个排列 \(p, q\),每个排列占一行,行内每个元素以空格隔开;否则输出 \(-1\) 代表不存在。
题解
不妨假设排列 \(p, q\) 初始状态均为 \(1, 2, ..., n\)。因为加法满足交换律,所以我们可以直接令排列 \(p\) 不变,仅对排列 \(q\) 进行调整。
易知在 \(q\) 不进行任何调整时 \(sum\) 可以取得最小值 \(\sum_{i=1}{n}i\),所以若 \(k > \sum_{i=1}^{n}\) 则无解,直接输出 \(-1\),否则必定有解。无论如何对排列 \(q\) 进行调整,对于集合 \(\{ x \mid x \ge \lfloor \frac{n}{2} \rfloor \}\) 中的所有元素至少出现一次在 \(sum\) 的累加过程中。基于此性质,若想使得 \(sum\) 变大,可以让 \(\{x, x \}(x < \lfloor \frac{n}{2} \rfloor)\) 和 \(\{y, y\}(y \ge \lfloor \frac{n}{2} \rfloor)\) 交换为 \(\{x, y \}\) 和 \(\{y, x \}\),这会使得 \(sum = sum + y - x\)。此时,有的小伙伴可能会担心若我先使用了特定的数 \(y\),下次不能再使用 \(y\) 了,这有没有可能使得答案变差?答案是不会的,因为加法满足交换律,所以
等价于
所以,我们可以从小到大遍历 \(p\) 的每一个满足 \(\{ x \mid x < \lfloor \frac{n}{2} \rfloor \}\) 的元素,并按顺序为之贪心的选择一个最大可配对的数(即未使用过),遍历过程中若无论再执行任何操作都会使得 \(sum > k\),则提前退出循环。在遍历结束后(包括提前退出循环的情况),将剩余未使用的数从小到大依次分配给排列 \(q\) 剩余的所有位置即可。
对于代码的编写,可以使用带权并查集维护一个大小为 \(n + 1\) 的集合 \(dsu\),初始化为 \(dsu[i] = i\),代表在大于等于 \(i\) 的所有数中最小未使用的数,代表元使用并集中的最大值。此外,再使用 \(dsuSz\) 维护每一个集合的大小,这样子就可以通过代表元(即集合内最大值)快速求出比当前集合小的最大集合的代表元。每当用完一个代表元以后,只需将其和其之后的第一个元素所在集合合并为并集即可。
参考代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
constexpr int N = 1e6 + 7;
int T, n;
ll k;
int ans[N];
int dsuSz[N];//带权值就取消注释dsuSz
int dsu[N];//初始化就调用iota
int find(int x) { return x == dsu[x] ? x : dsu[x] = find(dsu[x]); }
bool merge(int x, int y, bool elderAsFather = true) { int fx = find(x), fy = find(y); if (fx == fy) return false;
if (elderAsFather) {// 较大者作为父亲节点
if (fx < fy) dsu[fx] = fy, dsuSz[fy] += dsuSz[fx]; else dsu[fy] = fx, dsuSz[fx] += dsuSz[fy];
} else {// 较小者作为父亲节点
if (fx < fy) dsu[fy] = fx, dsuSz[fx] += dsuSz[fy]; else dsu[fx] = fy, dsuSz[fy] += dsuSz[fx];
}
return true;
}
/*前n项和公式:从第 l 项到第 r 项*/
ll preNSum(ll l, ll r) { return (r - l + 1) * (r + l) >> 1; }
int main() {
ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
cin >> n >> k;
iota(dsu + 1, dsu + 2 + n, 1);
fill(dsuSz + 1, dsuSz + 2 + n, 1);
ll sum = preNSum(1, n);// 计算前 n 项和,这是 sum 最小的情况
if (sum > k) {// 不存在满足不大于 k 的情况
cout << -1 << '\n';
return 0;
}
ll diff = k - sum;// 还可以增大的数的总大小
int m = n / 2, w = 0;
for (int i = 1; i <= m && diff > 0LL; ++ i) {
int j = min(1LL * n, diff + i);// 当前剩余可增大的总大小为 diff,因此 i 可以配对的最大数为 i + diff,但不可能超过 n
int fa = find(j);// 找到 j 的代表元,即大于等于 j 的最小未使用的数
int idx = fa > j || fa > n ? fa - dsuSz[fa] : fa;// 如果已经超出最大可增幅范围,只能使用比 j 小且最大的未使用的数
ans[w ++] = idx;// 保存答案
diff -= idx - i;// 可增大的总大小要扣去 idx - i
merge(idx, idx + 1);// 由于 idx 已经使用了,因此要指向其之后的最相邻的未使用的数,由于并查集的代表元是最大下标,所以只需要与后一个数合并为一个集合即可
}
for (int i = 1; i <= n; ++ i) {
i = find(i);// 找到未使用的数
if (i <= n) ans[w ++] = i;
}
cout << k - diff << '\n';
for (int i = 1; i <= n; ++ i) cout << i << ' ';
cout << '\n';
for (int i = 0; i < n; ++ i) cout << ans[i] << ' ';
return 0;
}
浙公网安备 33010602011771号