Codeforces Global Round 19
题目传送门:Codeforces Global Round 19。
A. Sorting Parts
题意简述
给定长度为 \(n\) 的数列 \(a_1, a_2, \ldots, a_n\)。
问是否可以将数列分为一段非空前缀和一段非空后缀,将两部分分别从小到大排序后放回原位置,整个数列却并没被从小到大排序。
有多组数据。
数据范围:\(1 \le n \le {10}^4\),\(1 \le a_i \le {10}^9\)。
AC 代码
#include <cstdio>
#include <algorithm>
#define F(i, a, b) for(int i = a; i <= (b); ++i)
void Solve();
int main() {
int tests = 1;
scanf("%d", &tests);
while (tests--)
Solve();
return 0;
}
const int MN = 10005;
int n, a[MN];
void Solve() {
scanf("%d", &n);
F(i, 1, n) scanf("%d", a + i);
puts(std::is_sorted(a + 1, a + n + 1) ? "NO" : "YES");
}
如果原数列已经有序,则无论如何划分,数列都会保持有序。
否则,必然存在两元素 \(a_i, a_j\)(\(i < j\))满足 \(a_i > a_j\)。只需将它们划分在不同段内,则两段分别排序后数列仍然不会变得有序。
于是只需要判断原数列是否有序即可,使用 std::is_sorted
可以简化代码。
时间复杂度为 \(\mathcal O(n)\)。
B. MEX and Array
题意简述
设有一长度为 \(k\) 的非负整数数列 \(b_1, b_2, \ldots, b_k\)。将其划分为若干段 \([l_1, r_1], [l_2, r_2], \ldots, [l_c, r_c]\),则此划分的权值被定义为 \(\displaystyle c + \sum_{i = 1}^{c} \operatorname{mex}(\{ b_{l_i}, b_{l_i + 1}, \ldots, b_{r_i} \})\),即划分的段数加上每一段内的数的 mex 之和。定义此数列 \(b_{1 \sim k}\) 的价值为所有不同划分方式中的最大权值。
给定长度为 \(n\) 的非负整数序列 \(a_1, a_2, \ldots, a_n\),求其所有连续子串的价值之和。
有多组数据。
数据范围:\(1 \le n \le 100\),\(0 \le a_i \le {10}^9\)。
AC 代码
#include <cstdio>
#define F(i, a, b) for(int i = a; i <= (b); ++i)
void Solve();
int main() {
int tests = 1;
scanf("%d", &tests);
while (tests--)
Solve();
return 0;
}
const int MN = 105;
int n, a[MN];
void Solve() {
scanf("%d", &n);
F(i, 1, n) scanf("%d", a + i);
int ans = 0;
F(i, 1, n)
ans += (a[i] ? 1 : 2) * i * (n - i + 1);
printf("%d\n", ans);
}
首先考虑如何求出数列 \(b_1, b_2, \ldots, b_k\) 的价值。
注意到,长度为 \(\mathit{len}\) 的段中所有数的 mex 不会超过 \(len\),而每一段对划分的权值有贡献 \(1 + \operatorname{mex} \le \mathit{len} + 1\)。
又有,将这一段全部拆分为长度为 \(1\) 的段,将对划分的权值有贡献至少 \(\mathit{len}\)。而如果 \(\operatorname{mex} \ne 0\),意味着这一段内至少有一个 \(0\),于是再对划分的权值贡献至少 \(1\)。
也就是说,数列 \(b_1, b_2, \ldots, b_k\) 的价值即是全部划分为长度为 \(1\) 的段时的权值,即 \(\displaystyle \sum_{i = 1}^{k} \begin{cases} 1 & , b_i \ne 0 \\ 2 & , b_i = 0 \end{cases}\)。
令 \(c_i = \begin{cases} 1 & , b_i \ne 0 \\ 2 & , b_i = 0 \end{cases}\),则答案即为 \(c_{1 \sim n}\) 的每个子串中所有数的和之和。
我们熟知(通过和式变换)这即是 \(\displaystyle \sum_{i = 1}^{n} c_i \cdot i \cdot (n - i + 1)\)。
时间复杂度为 \(\mathcal O(n)\)。
C. Andrew and Stones
题意简述
有 \(n\)(\(n \ge 3\))堆石子排成一行,其中第 \(i\) 堆的数量为 \(a_i\)。
你需要通过一些操作将除了第 \(1\) 堆和第 \(n\) 堆以外的石子移走。每次操作,你可以:
- 选取三个下标 \(i, j, k\),满足 \(1 \le i < j < k \le n\) 且第 \(j\) 堆中至少有 \(2\) 个石子,将第 \(j\) 堆中的 \(2\) 个石子的其中一个移到第 \(i\) 堆中,另一个移到第 \(k\) 堆中。
即需保证 \(a_j \ge 2\) 并执行 \(a_j \gets a_j - 2\),\(a_i \gets a_i + 1\),\(a_k \gets a_k + 1\)。
请判断是否可以完成目标,如果可以完成,求出最少操作次数,否则报告之。
有多组数据。
数据范围:\(1 \le n \le {10}^5\),\(1 \le a_i \le {10}^9\)。
AC 代码
#include <cstdio>
#define F(i, a, b) for(int i = a; i <= (b); ++i)
void Solve();
int main() {
int tests = 1;
scanf("%d", &tests);
while (tests--)
Solve();
return 0;
}
typedef long long ll;
const int MN = 100005;
int n, a[MN];
void Solve() {
scanf("%d", &n), n -= 2;
F(i, 0, n + 1) scanf("%d", a + i);
if (n == 1 && a[1] % 2)
return puts("-1"), void();
bool ok = 0;
F(i, 1, n)
if (a[i] != 1)
ok = 1;
if (!ok)
return puts("-1"), void();
ll ans = 0;
F(i, 1, n)
ans += a[i] + (a[i] & 1);
printf("%lld\n", ans / 2);
}
首先,\(a_1\) 和 \(a_n\) 的值我们不关心,只有中间 \(n - 2\) 个数是值得考虑的,于是让 \(n\) 减去 \(2\),并且删去 \(a_1\) 和 \(a_n\) 两个数,即 \(a\) 数列变为 \(a_{1 \sim n-2}\)。
接下来考虑无解的情况:
- \(n = 1\) 且 \(a_1\) 为奇数。
- 所有的 \(a_i\) 均为 \(1\)。
这两种情况的无解性是显然的,接下来展示其他情况均有解,同时尽量减少操作次数。
其他情况即 \(n = 1\) 且 \(a_1\) 为偶数,或 \(n \ge 2\) 且至少有一个 \(a_i \ge 2\)。
我们认为一次操作将 \(2\) 个石子移到 \(1\) 和 \(n\) 号堆中是不浪费的,则可以认为每将石子移到非 \(1\) 或 \(n\) 号堆中是浪费了半步操作,我们需要减少浪费的操作数。
当 \(n = 1\) 且 \(a_1\) 为偶数时有解是显然的,而且不浪费任何一步操作。
当 \(n \ge 2\) 且至少有一个 \(a_i \ge 2\) 时,对于每个为奇数的 \(a_i\),我们都需要将至少一个石子移到其中,以将其变为偶数个,从而可以被清空,于是至少要浪费的操作数量为数列中的奇数个数。
此情况下,如果仍然存在奇数数量的石子堆,那么总是可以将数量 \(\ge 2\) 的石子堆中的 \(2\) 个石子的其中一个移动到数量为奇数的石子堆中,以浪费半步操作为代价消除一堆数量为奇数的石子堆;否则所有石子堆中石子数量均为偶数,显然可以不浪费任何一步操作清空。
于是当有解时,最少操作数为 \(\displaystyle \frac{1}{2} \left( \sum_{i = 1}^{n} a_i + \sum_{i = 1}^{n} [a_i \bmod 2 = 1] \right) = \sum_{i = 1}^{n} \left\lceil \frac{a_i}{2} \right\rceil\)。
时间复杂度为 \(\mathcal O(n)\)。
D. Yet Another Minimization Problem
题意简述
给定长度为 \(n\) 的两个正整数数列 \(a_1, a_2, \ldots, a_n\) 和 \(b_1, b_2, \ldots, b_n\)。
你可以对于任意 \(i\)(\(1 \le i \le n\))选择是否交换 \(a_i\) 和 \(b_i\)。
你需要最小化 \(\displaystyle \sum_{1 \le i < j \le n} {(a_i + a_j)}^2 + \sum_{1 \le i < j \le n} {(b_i + b_j)}^2\),求出此最小值。
有多组数据。
数据范围:\(1 \le n \le 100\),\(1 \le a_i, b_i \le 100\)。
AC 代码
#include <cstdio>
#include <algorithm>
#include <bitset>
#define F(i, a, b) for(int i = a; i <= (b); ++i)
#define dF(i, a, b) for(int i = a; i >= (b); --i)
void Solve();
int main() {
int tests = 1;
scanf("%d", &tests);
while (tests--)
Solve();
return 0;
}
const int MN = 105;
const int S = 10005;
int n;
int a[MN], b[MN];
std::bitset<S> f[MN];
void Solve() {
int ans = 0, sum = 0;
scanf("%d", &n);
F(i, 1, n) scanf("%d", a + i), ans += a[i] * a[i], sum += a[i];
F(i, 1, n) scanf("%d", b + i), ans += b[i] * b[i], sum += b[i];
ans *= n - 2;
f[0].reset();
f[0][0] = 1;
F(i, 1, n)
f[i] = (f[i - 1] << a[i]) | (f[i - 1] << b[i]);
int p = (sum + 1) / 2;
while (!f[n][p]) ++p;
ans += p * p + (sum - p) * (sum - p);
printf("%d\n", ans);
}
考虑
于是要最小化的即是 \(\displaystyle (n - 2) \left( \sum_{i = 1}^{n} a_i^2 + \sum_{i = 1}^{n} b_i^2 \right) + {\left( \sum_{i = 1}^{n} a_i \right)}^2 + {\left( \sum_{i = 1}^{n} b_i \right)}^2\)。其中第一项为定值,于是只需考虑最小化 \(\displaystyle {\left( \sum_{i = 1}^{n} a_i \right)}^2 + {\left( \sum_{i = 1}^{n} b_i \right)}^2\)。
令 \(\displaystyle S = \sum_{i = 1}^{n} a_i + \sum_{i = 1}^{n} b_i\),以及 \(\displaystyle A = \sum_{i = 1}^{n} a_i\)。只需考虑最小化 \(A^2 + {(S - A)}^2\),根据熟知结论,这即是需要让 \(A\) 尽量接近 \(S / 2\)。
枚举是否交换每对 \(a_i, b_i\) 等价于进行 0/1 背包,使用正常做法或 std::bitset
均可。
时间复杂度为 \(\mathcal O(n^2 v / w)\),其中 \(v\) 表示值域,\(w\) 为字长。
E. Best Pair
题意简述
给定长度为 \(n\) 的数列 \(a_1, a_2, \ldots, a_n\)。
令 \(\mathit{cnt}_x\) 表示数值 \(x\) 在 \(a_{1 \sim n}\) 中的出现次数。
设 \(x, y\) 为两不同的在 \(a\) 中出现过的数值,即需保证 \(\mathit{cnt}_x, \mathit{cnt}_y \ge 1\) 且 \(x \ne y\),定义数值对 \(\langle x, y \rangle\) 的权值为 \((\mathit{cnt}_x + \mathit{cnt}_y) \cdot (x + y)\)。
给出 \(m\) 对被禁止的数值对 \(\langle x_i, y_i \rangle\)(\(1 \le i \le m\))。保证给出的数值对满足 \(\mathit{cnt}_{x_i}, \mathit{cnt}_{y_i} \ge 1\) 且 \(x_i \ne y_i\) 且不会给出重复的数值对(\(\langle x, y \rangle\) 与 \(\langle y, x \rangle\) 算作相同)。
你需要求出所有未被禁止的数值对的权值的最大值。
保证至少存在一对权值有定义的未被禁止的数值对。
有多组数据。
数据范围:\(2 \le n \le 3 \times {10}^5\),\(0 \le m \le 3 \times {10}^5\),\(1 \le a_i \le {10}^9\)。
AC 代码
#include <cstdio>
#include <algorithm>
#include <vector>
#include <set>
#include <map>
#define F(i, a, b) for(int i = a; i <= (b); ++i)
void Solve();
int main() {
int tests = 1;
scanf("%d", &tests);
while (tests--)
Solve();
return 0;
}
typedef long long ll;
const int MN = 300005;
int n, m;
int a[MN];
std::vector<int> v[MN];
void Solve() {
scanf("%d%d", &n, &m);
F(i, 1, n) scanf("%d", a + i);
std::map<int, int> b;
F(i, 1, n) ++b[a[i]];
F(i, 1, n) v[i].clear();
for (auto [c, x] : b)
v[x].push_back(c);
std::vector<int> vv;
F(i, 1, n) if (!v[i].empty()) {
std::reverse(v[i].begin(), v[i].end());
vv.push_back(i);
}
std::set<std::pair<int, int>> f;
F(i, 1, m) {
int x, y;
scanf("%d%d", &x, &y);
f.insert({x, y});
}
ll ans = 0;
for (int i : vv)
for (int j : vv) if (j >= i)
for (int x : v[i]) {
bool ok = false;
for (int y : v[j]) if (x != y)
if (f.find({x, y}) == f.end() && f.find({y, x}) == f.end()) {
ans = std::max(ans, (ll)(i + j) * (x + y));
if (y == v[j][0]) ok = true;
break;
}
if (ok) break;
}
printf("%lld\n", ans);
}
首先我们有经典结论:不同的 \(\mathit{cnt}\) 的种类数只有 \(\mathcal O(\sqrt{n})\) 种。
这启发我们枚举 \(x, y\) 的 \(\mathit{cnt}\):假设枚举了 \(\mathit{cnt}_x = i\) 以及 \(\mathit{cnt}_y = j\),则枚举本身的复杂度可以控制在 \(\mathcal O(n)\) 级别。
那么,要最大化 \((i + j) \cdot (x + y)\),即是要最大化 \(x + y\),但需要满足 \(\mathit{cnt}_x = i\) 和 \(\mathit{cnt}_y = j\),并且 \(\langle x, y \rangle\) 未被禁止。
定义 \(S_i = \{ x \mid \mathit{cnt}_x = i \}\) 表示 \(\mathit{cnt}_x = i\) 的那些数值 \(x\) 构成的集合。则枚举了一对 \(i, j\) 后,我们可以最多遍历 \(m'\) 对 \(\langle x, y \rangle\),其中 \(m'\) 表示连接 \(S_i\) 与 \(S_j\) 之间的所有被禁止的数值对的数量。具体流程如下:
- 我们可以对于每个 \(i\),维护一个
std::vector
按照 \(x\) 从大到小的顺序存储 \(S_i\),在枚举的时候先从大到小枚举 \(x\),从大到小遍历 \(S_j\) 中的 \(y\),在一个std::set
中查询这一对 \(\langle x, y \rangle\) 是否是被禁止的,对于每个 \(x\),查找到第一个未被禁止的即可退出,如果最大的 \(y\) 已被匹配,则退出整个对 \(x\) 的枚举循环。
这个过程是 \(\mathcal O(1) + \mathcal O(m' \log m)\) 的。
总复杂度为 \(\mathcal O(n \log n + m \log m)\)。
F. Towers
题意简述
给定一棵有 \(n\) 个结点的树,结点编号为 \(1 \sim n\),结点 \(i\) 有权值 \(h_i\)。
你可以在结点中建任意多个信号塔,在结点 \(u\) 上建造一个信号强度为 \(e\) 的塔需要花费 \(e\) 的代价(\(e > 0\))。
对于结点 \(u\),我们称其接收到了信号,当且仅当存在两座信号塔,它们在树上的路径经过 \(u\)(包含端点),且它们的信号强度均 \(\ge h_u\)。
请求出为了让所有结点均接收到信号,需要花费的最少总代价。
数据范围:\(1 \le n \le 2 \times {10}^5\),\(1 \le h_i \le {10}^9\)。
AC 代码
#include <cstdio>
#include <algorithm>
#include <vector>
#define F(i, a, b) for(int i = a; i <= (b); ++i)
void Solve();
int main() {
Solve();
return 0;
}
typedef long long ll;
const int MN = 200005;
int n, h[MN];
std::vector<int> G[MN];
int mx[MN];
ll DFS(int u, int p) {
mx[u] = 0;
ll ret = 0;
for (int v : G[u]) if (v != p) {
ret += DFS(v, u);
mx[u] = std::max(mx[u], mx[v]);
}
ret += std::max(h[u] - mx[u], 0);
mx[u] = std::max(mx[u], h[u]);
return ret;
}
void Solve() {
scanf("%d", &n);
F(i, 1, n) scanf("%d", h + i);
F(i, 2, n) {
int x, y;
scanf("%d%d", &x, &y);
G[x].push_back(y);
G[y].push_back(x);
}
int rt = (int)(std::max_element(h + 1, h + n + 1) - h);
ll ans = 0;
int mx1 = 0, mx2 = 0;
for (int v : G[rt]) {
ans += DFS(v, rt);
if (mx[v] > mx2)
mx2 = mx[v];
if (mx2 > mx1)
std::swap(mx1, mx2);
}
ans += 2 * h[rt] - mx1 - mx2;
printf("%lld\n", ans);
}
令 \(\displaystyle h_\max = \max_{i = 1}^{n} h_i\)。显然,不会建造信号强度超过 \(h_\max\) 的信号塔,并且信号强度恰等于 \(h_\max\) 的信号塔会建造至少两座。
假设 \(h_\mathit{rt} = h_\max\),考虑将结点 \(\mathit{rt}\) 提作根。则 \(\mathit{rt}\) 的两棵不同子树内都必须建造信号强度恰等于 \(h_\max\) 的信号塔(或需要在 \(\mathit{rt}\) 自身上建造信号强度恰等于 \(h_\max\) 的信号塔)。
在考虑了这一点后,对于任意一个非 \(\mathit{rt}\) 的结点,对其的最佳“覆盖”(即选取两个路径经过它的,信号强度尽可能大的信号塔)总是有此形式:从其子树内选取一个信号强度最大的信号塔,而另一个信号塔总是可以取到那两个信号强度为 \(h_\max\) 中的某一个。于是限制即变为:对于每个结点 \(u\),其子树内必须至少建造一个信号强度 \(\ge h_u\) 的信号塔。
这时就可以进行一个简单的树形 DP,如果子树内的 \(h\) 值的最大值 \(\mathit{mx}\) 小于 \(h_u\),则选取子树中的信号塔中信号强度最大的一个,将其强度提升至 \(h_u\)(即代价增加 \(h_u - \mathit{mx}\))。
最后对于 \(\mathit{rt}\) 的处理,只需求出子树中前两大值将其提升至 \(h_\max\) 即可。
时间复杂度为 \(\mathcal O(n)\)。
G. Birthday
题意简述
你拥有 \(1, 2, \ldots, n\) 这 \(n\) 个整数。
对于每次操作,你可以选择你拥有的两个数 \(x, y\),删除这两个数,并加入 \(x + y\) 与 \(|x - y|\)。
你需要在 \(20 \cdot n\) 次操作内使这 \(n\) 个整数全部相等,并且在此基础上最小化这 \(n\) 个整数的和。
如果可以实现目标,你需要构造方案,否则报告之。
有多组数据。
数据范围:\(3 \le n \le 5 \times {10}^4\)。
AC 代码
#include <cstdio>
#include <algorithm>
#include <vector>
#include <array>
#include <bit>
#define F(i, a, b) for(int i = a; i <= (b); ++i)
void Solve();
int main() {
int tests = 1;
scanf("%d", &tests);
while (tests--)
Solve();
return 0;
}
std::vector<std::array<int, 2>> ans;
inline void add(int x, int y, int k) {
ans.push_back({x * k, y * k});
}
inline void expp(int n, int x, int k) {
while (n != x)
add(0, n, k),
add(n, n, k),
n *= 2;
}
std::vector<std::array<int, 2>> pans[8] = {
{},
{},
{},
{{1, 3}, {2, 2}},
{{1, 3}, {2, 2}},
{{1, 2}, {1, 3}, {2, 4}, {2, 6}, {4, 4}, {3, 5}, {0, 2}, {2, 2}, {0, 4}, {4, 4}},
{{1, 2}, {1, 3}, {2, 4}, {2, 6}, {4, 4}, {3, 5}, {2, 6}, {0, 4}, {4, 4}},
{{1, 2}, {3, 5}, {2, 6}, {4, 4}, {1, 0}, {1, 3}, {1, 7}, {2, 6}, {4, 4}}
};
void Solve(int n, int x, int k) {
if (n == x) {
Solve(n - 1, x, k);
return ;
}
if (n <= x / 2) {
Solve(n, x / 2, k);
F(i, 1, n - 1)
expp(x / 2, x, k);
return ;
}
if (n <= 7) {
for (auto [a, b] : pans[n])
add(a, b, k);
return ;
}
F(i, x / 2 + 1, n)
add(x - i, i, k);
// [1 ~ x-n-1], x/2, k
// [1 ~ n-x/2], x/2, 2k
int n1 = x - n - 1;
int n2 = n - x / 2;
if (n1 >= 3) {
Solve(n1, x / 2, k);
F(i, 1, n1 - 1)
expp(x / 2, x, k);
}
if (n2 >= 3) Solve(n2, x / 2, 2 * k);
expp(x / 2, x, k);
if (n1 <= 2)
F(i, 1, n1)
expp(i, x, k);
if (n2 <= 2)
F(i, 1, n2)
expp(i, x / 2, 2 * k);
if (n1 >= 3 && n2 >= 3)
add(0, x, k);
}
int n;
void Solve() {
scanf("%d", &n);
if (n == 2)
return puts("-1"), void();
int x = std::bit_ceil((unsigned)n);
if (n <= 7) {
ans = pans[n];
} else {
ans.clear();
Solve(n, x, 1);
}
add(0, x, 1);
printf("%d\n", (int)ans.size());
for (auto [a, b] : ans)
printf("%d %d\n", a, b);
}
首先,\(n = 2\) 时无解,这个样例已经告诉我们了,合理猜测其他情况均有解。
首先写个暴力跑一下 \(n = 3, 4, 5, 6, 7, 8, 9, \ldots\) 的情况,可以猜测最后需要将 \(n\) 个数全部变为大于等于 \(n\) 的最小的 \(2\) 的次幂。
这个结论的证明也很简单,考虑任意一个奇素数 \(p\),考虑拥有的整数对 \(p\) 取模后的值,可以发现每次操作不可能将不同时为 \(0\) 的两个整数变为两个 \(0\)。于是最终所有数都必须变为 \(2\) 的次幂,接下来的构造显示了总是可以达到大于等于 \(n\) 的最小的 \(2\) 的次幂。
首先,如果 \(n\) 本身就是 \(2\) 的次幂,将 \(n\) 减去 \(1\),显然与原问题等价。接下来假设 \(n\) 不是 \(2\) 的次幂,并令 \(x = 2^{\lceil \log_2 n \rceil}\) 为大于等于 \(n\) 的最小的 \(2\) 的次幂。
首先一个很自然的想法是尽量将 \(i\) 与 \(x - i\) 配对,得到 \(x\) 和 \(| 2 i - x |\)。比如:
- \(\{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 \}\) 变为
\(\{ 1, 2, 3, {\color{red} {2, 4, 6, 8}}, 8, {\color{blue} {16, 16, 16, 16}} \}\)。
于是,已得到的 \(x\) 不需要处理,而剩下的数分为三个部分:
- \(1, 2, \ldots, x - n - 1\);
- \(2, 4, \ldots, 2 \left( n - \frac{x}{2} \right)\);
- 一个落单的 \(\frac{x}{2}\)。
其中第 2 部分可以看作 \(1, 2, \ldots, n - \frac{x}{2}\) 全体乘以 \(2\),并且最终需要达到 \(x / 2\)(也被除以了 \(2\))。
当 \(n\) 足够大时,第 1, 2 部分均至少有一部分的大小 \(\ge 3\),可以递归。对于大小 \(\le 2\) 的部分,需要和落单的 \(\frac{x}{2}\) 一起处理。
在递归后,需要将已得到的整数批量操作到 \(x\)(因为可能会得到 \(x / 2^i\)),例如当 \(n = 12\) 的时候,递归 \(\{ 1, 2, 3 \}\) 得到 \(\{ 4, 4, 4 \}\) 后就需要将它们操作到 \(16\)(具体操作是简单的)。
我们对于 \(3 \le n \le 7\) 的部分打表,对于 \(n \ge 9\) 的部分,考虑至少进行了一次递归,这意味着最后一步操作必然为选取了 \(0\) 和 \(x\) 得到两个 \(x\),我们可以撤回最后一步操作,并得到一个 \(0\),有了 \(0\) 就意味着我们可以进行倍增:
- \((0, a) \to (a, a)\);
- \((a, a) \to (0, 2 a)\)。
于是我们将可能落单的 \(1, 2, 4, \frac{x}{2}\) 都倍增至 \(x\),最后补上操作 \((0, x)\) 即可。
代码实现中直接将递归过程定义为生成删去最后一步操作的操作序列,方便讨论。
时间复杂度为 \(\mathcal O(n \log n)\),操作次数可以控制在 \(\mathcal O(n \log n)\) 级别。
H. Minimize Inversions Number
题意简述
给定一个 \(1 \sim n\) 的排列 \(p_1, p_2, \ldots, p_n\)。
对于每个 \(k = 0, 1, \ldots, n\):
- 你需要在 \(p_1, p_2, \ldots, p_n\) 中取出一个长度为 \(k\) 的子序列,并将其按原顺序移动到 \(p\) 的开头。
- 你需要求出在所有选取方案中的逆序对数的最小值。
输出这 \(n + 1\) 个答案。
有多组数据。
数据范围:\(1 \le n \le 5 \times {10}^5\)。
AC 代码
#include <cstdio>
#include <algorithm>
#include <functional>
#define F(i, a, b) for(int i = a; i <= (b); ++i)
#define dF(i, a, b) for(int i = a; i >= (b); --i)
void Solve();
int main() {
int tests = 1;
scanf("%d", &tests);
while (tests--)
Solve();
return 0;
}
typedef long long ll;
const int MN = 500005;
int n, a[MN], c[MN];
int b[MN];
inline void Add(int i, int x) {
for (; i <= n; i += i & -i)
b[i] += x;
}
inline int Qur(int i) {
int s = 0;
for (; i; i -= i & -i)
s += b[i];
return s;
}
void Solve() {
scanf("%d", &n);
F(i, 1, n) scanf("%d", &a[i]);
ll ans = 0;
F(i, 1, n) b[i] = 0;
F(i, 1, n) {
int d = Qur(a[i]);
ans += i - 1 - d;
c[i] = i - 1 - 2 * d;
Add(a[i], 1);
}
F(i, 1, n) b[i] = 0;
dF(i, n, 1) {
c[i] -= 2 * Qur(a[i]);
Add(a[i], 1);
}
std::sort(c + 1, c + n + 1, std::greater<>());
printf("%lld ", ans);
for (int k = 1; k <= n; ++k) {
ans -= c[k] + (k - 1);
printf("%lld%c", ans, " \n"[k == n]);
}
}
考虑 \(k = 1\) 时的情况,即选取一个数移动到首位。
- 如果选取了 \(p_i\),逆序对数将减少 \(\displaystyle \sum_{j = 1}^{i - 1} [p_j > p_i] - \sum_{j = 1}^{i - 1} [p_j < p_i]\),即位置 \(i\) 之前的与 \(p_i\) 构成的逆序对数减去顺序对数。
- 将这个数记为 \(d_i\)。
显然,当 \(k = 1\) 时,即是选择 \(d_i\) 最大的那个移动到首位即可。
接下来考虑当 \(k \ge 2\) 时,逆序对数的变化量。可以看出,如果按从左到右的顺序将选取的子序列中的每个元素移到首位,则变化量恰好为子序列的 \(d_i\) 之和。然而如此操作后得到的排列与定义不同:选取的子序列被翻转了,于是需要将子序列翻转回来,故原序列的逆序对数将减少「选取的子序列中的顺序对数减去逆序对数」。
即,假设选取的子序列下标为 \(\{ i_1, i_2, \ldots, i_k \}\),子序列为 \(q_1, q_2, \ldots, q_k\)(\(q_i = p_{i_j}\)),则原序列的逆序对数等于 \(\displaystyle \operatorname{inv}(p_{1 \sim n}) - \left( \sum_{j = 1}^{k} d_{i_j} + \left( \left( \binom{k}{2} - \operatorname{inv}(q_{1 \sim k}) \right) - \operatorname{inv}(q_{1 \sim k}) \right) \right)\),其中 \(\displaystyle \binom{k}{2} - \operatorname{inv}(q_{1 \sim k})\) 一项即为顺序对数。
整理一下即得到需要最大化 \(\displaystyle \sum_{j = 1}^{k} d_{i_j} - 2 \operatorname{inv}(q_{1 \sim k})\)。
这时我们需要一个关键结论:对于每个逆序对 \((i, j)\)(即 \(i < j\) 且 \(p_i > p_j\)),如果 \(i\) 被选取在子序列中,则 \(j\) 也被选取在子序列中一定不更劣。我们使用反证 + 调整法证明此结论:
-
假设存在逆序对 \((i, j)\) 且 \(p_i\) 被选取而 \(p_j\) 未被选取,我们将证明存在比此种情况不更劣的情况。
-
由于满足此条件的逆序对存在,考虑选取其中 \(i, j\) 距离最小的那一对,即 \(j - i\) 最小的且满足 \(i\) 被选取而 \(j\) 未被选取的逆序对。
-
由此,在 \(i, j\) 之间不存在:
- 值在 \(p_j\) 与 \(p_i\) 之间的元素;
- 值大于 \(p_i\) 且已被选取的元素;
- 值小于 \(p_j\) 且未被选取的元素。
否则,将与 \(j - i\) 的最小性矛盾。
-
考虑将 \(i\) 从子序列中删去,并加入 \(j\),考察元素 \(p_k\) 对最终逆序对数量的贡献:
- 如果 \(k < i\) 且 \(p_k\) 被选取:\(p_k\) 与 \(p_i, p_j\) 的相对位置无变化,贡献 \(0\);
- 如果 \(k < i\) 且 \(p_k\) 未被选取:只有 \(p_k\) 介于 \(p_j\) 与 \(p_i\) 之间时,会有贡献 \(-2\),否则贡献 \(0\);
- 如果 \(k > j\) 且 \(p_k\) 未被选取:\(p_k\) 与 \(p_i, p_j\) 的相对位置无变化,贡献 \(0\);
- 如果 \(k > j\) 且 \(p_k\) 被选取:只有 \(p_k\) 介于 \(p_j\) 与 \(p_i\) 之间时,会有贡献 \(-2\),否则贡献 \(0\);
- 如果 \(i < k < j\):只有「\(p_k > p_i\) 且 \(p_k\) 被选取」或「\(p_k < p_j\) 且 \(p_k\) 未被选取」时才有贡献 \(1\),其他情况均贡献 \(-1\)。
然而前文已说明并不存在这两类(「\(p_k > p_i\) 且 \(p_k\) 被选取」或「\(p_k < p_j\) 且 \(p_k\) 未被选取」)使得贡献为正数的情况,再加上 \(i, j\) 本身从逆序对变为顺序对的 \(-1\) 贡献,于是总变化量必然为负数。
-
因为每次调整后选取的子序列的下标之和必然增加,调整法将在有限步内结束。证毕。
于是,前文需最大化的 \(\displaystyle \sum_{j = 1}^{k} d_{i_j} - 2 \operatorname{inv}(q_{1 \sim k})\) 中的第二项可改为 \(\displaystyle \sum_{j = 1}^{k} \sum_{l = i_j + 1}^{n} [p_l < p_{i_j}]\)。
令 \(\displaystyle c_i = d_i - 2 \sum_{j = i + 1}^{n} [p_j < p_i]\)。即是要最大化 \(\displaystyle \sum_{j = 1}^{k} c_{i_j}\)。
将 \(c\) 从大到小排序后逐个选取即可。
所有 \(c_i\) 的计算和最初逆序对数 \(\operatorname{inv}(a_{1 \sim n})\) 的计算均可在 \(\mathcal O(n \log n)\) 内完成。
时间复杂度为 \(\mathcal O(n \log n)\)。