Codeforces Round 1111 (Div. 2)
没有 E 和 F。
A. Zero Sum
考虑 +1 和 -1 数量的差值,每次操作会让差值 +4, -4 或者不变,操作目标是让差值等于 0。
如果初始差值模 4 不等于 0,后续操作就不可能让差值等于 0;反之,一定存在让差值等于 0 的操作序列,要细究的话策略很好想,题目也只让判断是否可行,就不展开了。
B. Yet Another Constructive
首先考虑无解情形,由于所有元素必须为正整数,显然 \(m < k\) 时无解(就算 k 个元素全 1,加起来都比 m 大了)。
接下来考虑如何构造题意要求的数组,一个很 trivial 的想法是把第一个数填得很大,后面全填 1,这样能让第一个子数组的和取到 \(m\),而再往后滑动窗口的时候,和就不可能够到 m。至于第一个数填多少也很简单,简单计算可得 \(m - (k - 1)\)。
C. Inversion of a Subsequence
操作限制翻译过来就是:每次取反操作里,子序列中至少要有一个 \(1\)。
显然一次操作就可以把所有 \(0\) 变成 \(1\),接下来就是分类讨论了:
- \(a\) 和 \(b\) 数组完全一致:不用修改,答案为 0
- \(a\) 和 \(b\) 数组不一致,且 \(a\) 中不存在 \(1\):答案为 -1
- \(a\) 和 \(b\) 数组不一致,且 \(a\) 中存在 \(1\),有一个及以上的 \(1\) 要变成 \(0\):答案为 1
- \(a\) 和 \(b\) 数组不一致,且 \(a\) 中存在 \(1\),没有 \(1\) 要变成 \(0\),但存在 \(0\) 要变成 \(1\):答案为 2
想清楚之后对着写 if-else 条件判断就可以了。
D1. XOR Sorting (Easy Version)
这个思路不很好,想的时候路走窄了,虽然能过 D1,但是不好推广到 D2,不过还是记一下。
考虑如何以最小的代价将 \(a[i]\) 挪到 \(a[j]\)。trivial 的想法是直接一步交换它俩,但是或许有更省的方法。
记 \(\text{msb}(n)\) 为“\(n\) 的二进制最高位 \(1\) 的位置”,则:
- 判断 \(i\) 和 \(j\) 的二进制最高位 1 的位置
- 如果 \(i\) 和 \(j\) 的二进制最高位 1 的位置不同,假设 \(b_i = \text{msb}(i) > \text{msb}(j) = b_j\),以 \(2^{b_i}\) 的代价把 \(a[i]\) 挪到 \(a[i \oplus 2^{b_i}]\),即消除掉 \(i\) 的最高位 \(1\),然后回到第一步继续比较
- 如果 \(i\) 和 \(j\) 的二进制最高位 1 的位置相同,则可以先同时忽略这一位,继续回到第一步比较更低位
很容易能看出来,通过这个过程,把 \(a[i]\) 挪到 \(a[j]\) 的最小代价就是:
比直接一步交换 \(a[i]\) 和 \(a[j]\) 显然更优。
至于在这个过程中那些被改动了位置的数,其实不很重要。直觉上,把它们换回原位置的代价不会超过 \(2^{\text{msb}(i \oplus j)}\),考虑到答案只取最大代价,这些位置改变不会影响最终答案。
代码实现上倒是很简单,考虑对原数组排序,得到每个数的原 index(记为 \(i\))和排序后的目标 index(记为 \(j\)),枚举 \(2^{\text{msb}(i \oplus j)}\) 并取最大即可,时间复杂度就是排序的 \(n \log n\)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef std::vector<int> vi;
#define INT_INF 2147483647
#define forn(i, a, b) for(int i = a; i < b; i++)
#define readi(x) scanf("%d", &x)
#define readv(x, n) forn(i,0,n) scanf("%d", &x[i])
#ifdef DEBUG
#define printi(x) printf("DEBUG: %d\n", x)
#define prints(x) printf("%s", x)
#define printv(x, n) vi x(n); printf("DEBUG: "); forn(i,0,n) printf("%d ", x[i])
#else
#define printi(x) printf("%d\n", x)
#define prints(x) cout << x << endl
#define printv(x, n) forn(i,0,n) printf("%d ", x[i])
#endif
int main(void){
int t;
readi(t);
while (t--) {
int n, _;
readi(n);
readi(_);
vector<pair<int, int>> v;
forn (i, 0, n) {
int _tmp;
readi(_tmp);
v.push_back({_tmp, i});
}
sort(v.begin(), v.end());
int ans = 0;
forn (i, 0, n) {
if (v[i].second != i) {
ans = max(ans, (1 << __lg(v[i].second ^ i)));
}
}
printi(ans);
}
}
D2. XOR Sorting (Hard Version)
如果承接 D1 的思路考虑如何动态维护数组,会发现很困难:每次改变元素的时候,\(i\)(src index)倒是不会变,但是 \(j\)(dst index)会在一个子区间内造成 \(+1\) 或 \(-1\) 的影响,快速更新一个子区间的 \(\text{msb}(i \oplus (j + 1))\) 或者 \(\text{msb}(i \oplus (j - 1))\) 在我的知识范围内是一个 non-trivial 的事情。
其实在推出答案一定是 2 的幂次之后,就该从答案取值入手来考虑问题。我们考虑当付出 \(2^k\) 的代价的时候,可以做到什么?答案是,对于所在位置的二进制表示,前缀相同但低 \(k\) 位不同的一组数,能够通过交换做到任意重排,进一步地,也就是可以对这个子区间完成排序。
如果想将两个排好序的子区间合并成一个排好序的大子区间,又需要付出多少代价?
假设两个子区间的大小均为 \(2^k\),合并之后的子区间大小则为 \(2^{k+1}\)。如果左半子区间的最大值比右半子区间的最小值要小,那只需要排序好这两个子区间,不需要额外的代价就能完成大子区间的排序;否则,则存在元素需要跨越两个子区间的边界,需要付出 \(2^{k+1}\) 的代价来重新排序合并之后的子区间。
于是对于每个长度为 \(2^k\) 的子区间,只需维护其最大最小值,即可推知其排序的最小代价,加上单点修改操作和上面提到的可合并特性,天然适合用线段树来维护。
注意这破题比较卡 I/O,不要用 scanf/printf,至少使用关闭流同步的 cin/cout,否则会超时。线段树本身的常数在这题倒不是什么大问题。
#include <bits/stdc++.h>
#include <bit>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef std::vector<int> vi;
#define N 1000000
#define INT_INF 2147483647
#define forn(i, a, b) for(int i = a; i < b; i++)
#define readi(x) scanf("%d", &x)
#define readv(x, n) forn(i,0,n) scanf("%d", &x[i])
#ifdef DEBUG
#define printi(x) printf("DEBUG: %d\n", x)
#define prints(x) printf("%s", x)
#define printv(x, n) vi x(n); printf("DEBUG: "); forn(i,0,n) printf("%d ", x[i])
#else
#define printi(x) printf("%d\n", x)
#define prints(x) cout << x << endl
#define printv(x, n) forn(i,0,n) printf("%d ", x[i])
#endif
int x[N + 5], idx[N + 5];
int tree[4 * N + 5][3];
void build(int i, int l, int r, int n) {
if (l == r){
if (l <= n) {
tree[i][0] = tree[i][1] = x[l];
idx[l] = i;
}
else {
tree[i][0] = tree[i][1] = INT_INF;
}
tree[i][2] = 0;
return;
}
int mid = (l + r) >> 1;
build(i << 1, l, mid, n);
build(i << 1 | 1, mid + 1, r, n);
tree[i][0] = max(tree[i << 1][0], tree[i << 1 | 1][0]);
tree[i][1] = min(tree[i << 1][1], tree[i << 1 | 1][1]);
tree[i][2] = ((tree[i << 1][0] > tree[i << 1 | 1][1]) ? ((r - l + 1) >> 1) : max(tree[i << 1][2], tree[i << 1 | 1][2]));
return;
}
void update(int i, int l, int r, int i_j, int x_j) {
if (l == r){
tree[i][0] = tree[i][1] = x_j;
tree[i][2] = 0;
return;
}
int mid = (l + r) >> 1;
if (i_j <= mid) update(i << 1, l, mid, i_j, x_j);
else update(i << 1 | 1, mid + 1, r, i_j, x_j);
tree[i][0] = max(tree[i << 1][0], tree[i << 1 | 1][0]);
tree[i][1] = min(tree[i << 1][1], tree[i << 1 | 1][1]);
tree[i][2] = ((tree[i << 1][0] > tree[i << 1 | 1][1]) ? ((r - l + 1) >> 1) : max(tree[i << 1][2], tree[i << 1 | 1][2]));
return;
}
int main(void){
cin.tie(0)->sync_with_stdio(false);
int t;
cin >> t;
while (t--){
int n, q;
cin >> n >> q;
forn (i, 1, n + 1)
cin >> x[i];
unsigned int new_n = bit_ceil((unsigned)n);
build(1, 1, new_n, n);
cout << tree[1][2] << '\n';
while (q--) {
int i_j, x_j;
cin >> i_j >> x_j;
update(1, 1, new_n, i_j + 1, x_j);
cout << tree[1][2] << '\n';
}
}
}

浙公网安备 33010602011771号