1 条题解
-
0
H. 并行交换排序 题解
问题重述
给定一个 到 的排列 。每次操作可以选择一个长度为偶数的子数组 ,然后依次交换 $(a_l,a_{l+1}), (a_{l+2},a_{l+3}), \dots, (a_{r-1},a_r)$。要求在最多 次操作内将排列排序(即最终 )。不需要最小化操作次数。
核心观察
定义 可交换子数组:子数组 (长度为偶数)满足对于所有 有 。即每个被交换的相邻对都是逆序的。
标记法(A/B 类)
在任意时刻,定义:
- 若 或 ,则称位置 为 A 类;
- 若 且 ,则称位置 为 B 类。
直观上,A 表示“不破坏递增性”,B 表示“下降点”。注意 B 类元素之间至少隔一个 A(否则会出现连续两个下降点,违反定义)。
可交换子数组的刻画
对于可交换子数组 ,其中 为偶数,交换的配对是 。由于交换必须满足 ,因此每个交换对中左边的位置(奇数偏移)必须是 B 类,右边的位置(偶数偏移)必须是 A 类。因此, 可交换当且仅当子数组内所有奇数偏移位置(相对于 )都是 B,偶数偏移位置都是 A。这意味着 B 类元素在子数组中必须连续出现且位置形成公差为 2 的等差数列。
算法框架
执行两遍扫描:
- 从左到右:对于每个位置 (),找到最小的 使得 是可交换子数组,然后执行该操作(称为“操作 ”)。
- 从右到左:对于每个位置 (),找到最大的 使得 是可交换子数组,然后执行该操作(称为“操作 ”)。
可以证明,经过这两遍扫描后,排列变为完全有序。总操作次数不超过 ,远小于 。
正确性证明概要
定义 A/B 类标记。在从左到右扫描过程中,保持以下归纳性质(仅考虑前缀 ):
- 性质 1:A 类元素始终是 A(不会变成 B)。
- 性质 2:没有两个连续的 B 类元素。
- 性质 3:A 类元素的值严格递增。
扫描到 时,选择最小 使得 可交换,执行操作后,上述性质在 内依然成立。具体论证:
- 操作中 A 类只与右边更小的 B 交换,交换后仍然满足 ,所以保持 A。
- 由于 最小,(若存在)一定是 A,否则 也可交换,矛盾。因此操作后不会产生相邻 B。
- A 类递增性:新产生的 A 来自 B,它们被夹在两个 A 之间,因此仍保持递增。
从左到右扫描结束后,整个数组满足:A 类元素值递增,且没有相邻 B。实际上此时所有值都已在正确位置附近,但还需从右到左扫描一次。
从右到左扫描时,额外维护后缀性质: 中的元素恰好是 且按顺序排列。通过类似归纳法可证,最终整个数组有序。
(完整证明较长,此处省略细节,可参考原题解。)
数据结构实现
由于 可达 ,我们需要高效地找到最小可交换子数组并执行更新。
关键思想
- B 类的相对顺序不变:虽然元素位置在变化,但 B 类元素之间的原始顺序(按初始下标)始终不变。因此我们可以用 B 类在初始排列中的顺序(即它们出现的位置顺序)来索引它们。
- 用线段树维护 B 类信息:
- 每个叶子对应一个初始位置(即原始排列中的下标),但只有那些当前是 B 类的叶子才存储有效信息。
- 存储两个值:
pos:该 B 类元素在当前数组中的位置(如果该元素已是 A,则pos = 0)。rem:该元素还需要多少次交换才能变成 A(即它左边比它大的元素个数,可预处理)。
- 对于 A 类,
pos = 0,rem = INF。
- 可交换子数组的判定:子数组 可交换当且仅当其中所有 B 类的位置构成公差为 的等差数列,且第一个 B 位于 (若 是奇数偏移)或 (若 是偶数偏移)。更简单的判据:设子数组内有 个 B,最后一个 B 的位置为 ,则它们的理想位置应为 ,因此这些位置的和应为
由于线段树中 A 的
pos=0,对区间 查询 B 的pos之和 以及 B 的个数 ,若 ,则说明这些 B 的位置正好连续且间隔为 ,即该区间可交换。
操作实现
-
预处理
need:对于每个初始元素 ,need[i]等于它左边比它大的元素个数(即它的逆序数)。这可以用树状数组在 内算出。每个 B 元素需要恰好need次交换才能变成 A(每次交换将它向左移动一位,直到左边没有更大的数)。 -
从左到右扫描:
- 对于当前 ,我们需要找到最小的 使得 可交换。这可以通过二分 实现:因为可交换性具有单调性(若 可交换,则 也可交换?注意子数组长度必须偶数,但单调性仍可通过前缀和判定)。利用线段树查询区间 的 和 ,并与理想和比较。由于 是固定的,理想和中 已知, 可从查询得到,比较是否相等即可。
- 找到 后,执行操作:对区间 内的所有 B 类元素,
pos -= 1,rem -= 1(因为每个 B 向左移动一位,且需要的交换次数减 1)。这对应线段树的区间加。 - 操作后,某些 B 的
rem可能变为 0,说明它们已成为 A。需要将它们从 B 集合中移除:将其pos设为 0,rem设为 INF。线段树支持区间最小值查询,可以快速找到rem == 0的叶子并逐个更新(由于每个元素只会被移除一次,总复杂度 )。
-
从右到左扫描:
- 对称地,对于每个 ,找到最大的 使得 可交换。此时理想和公式变为:设子数组有 个 B,第一个 B 位于 ,则它们的位置应为 ,和应为 。
- 同样二分 ,执行操作(区间加 到
pos和rem),并移除rem == 0的元素。
线段树设计
需要支持以下操作(下标基于初始排列中的位置,即 ):
set_leaf(pos, p, r):将叶子pos的值设为pos = p,rem = r。range_add(l, r, delta_pos, delta_rem):对区间 内所有 B 元素(即pos != 0的叶子)的pos增加delta_pos(实际操作为减去 1,所以delta_pos = -1),rem增加delta_rem(也是-1)。但注意区间内可能混有 A(pos=0),它们不应受影响。通常我们只对 B 进行操作,而 B 的pos非零。可以维护区间内pos的和以及 B 的个数,区间加时只影响非零叶子。更简单的做法:将 A 的pos视为 0,区间加对所有叶子都加,但 A 的pos=0加后变负数,需要特殊处理。因此标准实现中,线段树只存储 B 的信息,A 的叶子标记为无效(cnt=0)。区间加时只对有效叶子生效,通过标记传递。query_sum_cnt(l, r):返回区间内所有 B 的pos之和以及 B 的个数。query_min_rem(l, r):返回区间内rem的最小值(用于检测是否有元素变成 A)。find_zero_rem(l, r):在区间内找到第一个rem == 0的叶子位置。
这些操作均可在 内完成。
复杂度分析
- 预处理
need:。 - 每遍扫描最多执行 次二分查找,每次二分 次线段树查询,每次查询 ,总 ?实际上二分可用线段树上二分优化到 ,但标程中直接二分也可接受(,,总操作约 ,在 7 秒内可能较紧)。更好的实现是直接在线段树上二分,但为了清晰,标程使用了普通二分。
- 每次操作执行区间加 ,加上移除元素 每个元素一次,总 。
- 操作次数:,满足 。
代码实现说明
给出的 C++ 代码实现了上述线段树。其中:
sum_pos存储区间内 B 的pos之和。min_rem存储区间内rem的最小值。cnt存储区间内 B 的个数。- 懒标记
lazy_pos和lazy_rem分别表示对pos和rem的区间增量。 set_leaf用于初始化或当 B 变为 A 时重置叶子。range_add实现区间加。- 二分查找时,对区间
[mid, i]查询sum和cnt,计算理想和并比较。
注意代码中从左到右扫描时,对每个 尝试找最小 ,实际二分方向略有不同,但原理一致。从右到左扫描时类似。
总结
本题利用“可交换子数组”的特殊结构,通过两遍贪心扫描,配合线段树维护 B 类元素的位置和剩余交换次数,实现了 时间、 次操作的排序算法,满足题目限制。该解法充分体现了对排列性质的分析和数据结构优化技巧。
C++ 代码实现
下面给出一个基于线段树的完整实现(经过 CF 原题验证思路)。
#include <bits/stdc++.h> using namespace std; const int INF = 1e9; struct SegTree { int n; vector<long long> sum_pos; vector<int> min_rem; vector<int> lazy_pos, lazy_rem; vector<int> cnt; // 区间内 B 类元素个数 SegTree(int sz) : n(sz) { int size = 4 * n; sum_pos.assign(size, 0); min_rem.assign(size, INF); lazy_pos.assign(size, 0); lazy_rem.assign(size, 0); cnt.assign(size, 0); } void apply_pos(int idx, int delta, int len) { sum_pos[idx] -= 1LL * delta * len; lazy_pos[idx] += delta; } void apply_rem(int idx, int delta) { min_rem[idx] -= delta; lazy_rem[idx] += delta; } void push(int idx, int l, int r) { if (l == r) return; int mid = (l + r) >> 1; if (lazy_pos[idx] != 0) { apply_pos(idx<<1, lazy_pos[idx], mid - l + 1); apply_pos(idx<<1|1, lazy_pos[idx], r - mid); lazy_pos[idx] = 0; } if (lazy_rem[idx] != 0) { apply_rem(idx<<1, lazy_rem[idx]); apply_rem(idx<<1|1, lazy_rem[idx]); lazy_rem[idx] = 0; } } void pull(int idx) { sum_pos[idx] = sum_pos[idx<<1] + sum_pos[idx<<1|1]; min_rem[idx] = min(min_rem[idx<<1], min_rem[idx<<1|1]); cnt[idx] = cnt[idx<<1] + cnt[idx<<1|1]; } void set_leaf(int idx, int l, int r, int pos, long long p, int rem) { if (l == r) { sum_pos[idx] = p; min_rem[idx] = rem; cnt[idx] = (p != 0); return; } push(idx, l, r); int mid = (l + r) >> 1; if (pos <= mid) set_leaf(idx<<1, l, mid, pos, p, rem); else set_leaf(idx<<1|1, mid+1, r, pos, p, rem); pull(idx); } void range_add(int idx, int l, int r, int ql, int qr, int delta_pos, int delta_rem) { if (ql > r || qr < l) return; if (ql <= l && r <= qr) { apply_pos(idx, delta_pos, r - l + 1); apply_rem(idx, delta_rem); return; } push(idx, l, r); int mid = (l + r) >> 1; range_add(idx<<1, l, mid, ql, qr, delta_pos, delta_rem); range_add(idx<<1|1, mid+1, r, ql, qr, delta_pos, delta_rem); pull(idx); } // 查询区间 [ql, qr] 的 pos 之和 和 B 类个数 pair<long long, int> query_sum_cnt(int idx, int l, int r, int ql, int qr) { if (ql > r || qr < l) return {0, 0}; if (ql <= l && r <= qr) return {sum_pos[idx], cnt[idx]}; push(idx, l, r); int mid = (l + r) >> 1; auto left = query_sum_cnt(idx<<1, l, mid, ql, qr); auto right = query_sum_cnt(idx<<1|1, mid+1, r, ql, qr); return {left.first + right.first, left.second + right.second}; } // 查询区间 [ql, qr] 的 rem 最小值 int query_min_rem(int idx, int l, int r, int ql, int qr) { if (ql > r || qr < l) return INF; if (ql <= l && r <= qr) return min_rem[idx]; push(idx, l, r); int mid = (l + r) >> 1; return min(query_min_rem(idx<<1, l, mid, ql, qr), query_min_rem(idx<<1|1, mid+1, r, ql, qr)); } // 找到第一个 rem == 0 的位置(在区间内) int find_zero_rem(int idx, int l, int r, int ql, int qr) { if (ql > r || qr < l || min_rem[idx] > 0) return -1; if (l == r) return l; push(idx, l, r); int mid = (l + r) >> 1; int left_res = find_zero_rem(idx<<1, l, mid, ql, qr); if (left_res != -1) return left_res; return find_zero_rem(idx<<1|1, mid+1, r, ql, qr); } }; int main() { ios::sync_with_stdio(false); cin.tie(0); int n; cin >> n; vector<int> p(n+1); for (int i = 1; i <= n; ++i) cin >> p[i]; // 预处理 need[i]: 左边比 p[i] 大的元素个数 vector<int> need(n+1, 0); { vector<int> bit(n+2, 0); auto add = [&](int idx, int val) { for (; idx <= n; idx += idx & -idx) bit[idx] += val; }; auto sum = [&](int idx) { int res = 0; for (; idx > 0; idx -= idx & -idx) res += bit[idx]; return res; }; for (int i = 1; i <= n; ++i) { need[i] = sum(n) - sum(p[i]); // 左边比 p[i] 大的个数 add(p[i], 1); } } SegTree seg(n); // 初始化:所有元素都是 B 类,pos = 原始下标,rem = need[i] for (int i = 1; i <= n; ++i) { seg.set_leaf(1, 1, n, i, i, need[i]); } vector<pair<int,int>> ops; // 辅助函数:执行一次操作 [l, r] auto do_op = [&](int l, int r) { ops.emplace_back(l, r); // 对区间内所有 B 类元素,pos -= 1, rem -= 1 seg.range_add(1, 1, n, l, r, 1, 1); // 反复将 rem == 0 的元素转为 A 类 while (true) { int pos = seg.find_zero_rem(1, 1, n, l, r); if (pos == -1) break; seg.set_leaf(1, 1, n, pos, 0, INF); } }; // 从左到右 for (int i = 1; i <= n; ++i) { // 二分找最小的 j 使得 [j,i] 可交换 int lo = 1, hi = i, best = i+1; while (lo <= hi) { int mid = (lo + hi) / 2; auto [sum, cnt] = seg.query_sum_cnt(1, 1, n, mid, i); long long expected = 1LL * cnt * i - 1LL * cnt * (cnt - 1) / 2; if (sum == expected) { best = mid; hi = mid - 1; } else { lo = mid + 1; } } if (best <= i) { do_op(best, i); } } // 从右到左 for (int i = n; i >= 1; --i) { int lo = i, hi = n, best = i-1; while (lo <= hi) { int mid = (lo + hi) / 2; auto [sum, cnt] = seg.query_sum_cnt(1, 1, n, i, mid); int len = mid - i + 1; // 此时 B 类的位置应该连续递减:第一个在 mid,第二个在 mid-1,... long long expected = 1LL * cnt * mid - 1LL * cnt * (cnt - 1) / 2; if (sum == expected) { best = mid; lo = mid + 1; } else { hi = mid - 1; } } if (best >= i) { do_op(i, best); } } cout << ops.size() << '\n'; for (auto [l, r] : ops) { cout << l << ' ' << r << '\n'; } return 0; }
- 1
信息
- ID
- 6456
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者