做题记录 // 230318
好大的绅士脱起控来声势浩大。
GM 说上午只做思维,不费手;下午只敲模拟,不动脑。
你以为它们中和了吗?确实,但我左手一桶 NaOH,右手一桶浓 H₂SO₄,一起泼到 GM 脑袋上,它们确实中和了,GM 也确实没了。
A. 棒棒糖 Lollipop
http://222.180.160.110:1024/contest/3430/problem/1
由于询问和数组长度都是 \(10^6\) 级别的,我们需要对一次询问寻找常数或者是 \(\log\) 的解法(当然离线也行)。
GM 给了一个提示:计算一个数往后连续的 2 的个数。
太妙了!为什么是 2 的个数呢?不妨思考 2 的特性。
2 是最小的正偶数,这意味着区间左右端点可以通过添加或减少 2,达到在不改变区间和奇偶性的情况下,对区间和数值进行最小限度上的改变的目的。
我们以 \([1, n]\) 为初始求解区间,并记录其区间和为 \(t\),则 \(ans_t=(1, n)\)。
若我们的指针随意地向内挪动,那么由于数组中的 1 和 2 零散排列,我们并不能枚举到所有的区间和值,会出现漏枚和重复。联想到 2 对奇偶性带来的特殊贡献,我们可以考虑只枚举值奇偶性相同的区间和。
若左端点的元素值为 2,则左端点右移,使得当前区间和变为其前驱偶数区间和;否则,若右端点元素值为 2,则右端点左移。
若以上两个条件皆不满足,则说明左右端点元素值均为 1,则左端点右移一位且右端点左移一位后,区间和减少 2。
也就是说,我们每次操作都会且仅会使区间和减少 2,且至少使区间长度减少 1,所以我们可以在 \(\mathcal O(n)\) 内求解出所有和 \(t\) 奇偶性相同的区间和(因为 \(t\) 一定是最大的那一个)。
对于与 \(t\) 奇偶性不同的区间和,我们先找到一个最长的与 \(t\) 奇偶性不同的区间,然后以这个区间为求解起点,按照和上面相同的方法求解即可。那么这个区间应该如何寻找呢?
我们从区间 \([1, n]\) 的左边或右边减去一个 1 就可以改变整个区间的奇偶性。可是它的端点不一定是 1。这个时候为了保证剩余区间最长(可以枚举到所有该奇偶性下的区间和),看一看是堆在左边的 2 比较多还是堆在右边的 2 比较多。我们选取较少的一方,将其全部删除,就是新的求解区间。
namespace XSC062 {
using namespace fastIO;
const int maxn = 2e6 + 5;
char s[maxn];
int n, m, sum, t;
int l[maxn], r[maxn];
int main() {
scanf("%d %d", &n, &m);
scanf("%s", s + 1);
for (int i = n; i; --i) {
++sum;
if (s[i] == 'T')
++sum;
}
l[sum] = 1, r[sum] = n, t = sum;
for (int i = 1, j = n; i < j; ) {
if (s[i] == 'T') {
l[t -= 2] = ++i;
r[t] = j;
}
else if (s[j] == 'T') {
l[t -= 2] = i;
r[t] = --j;
}
else if (i + 2 <= j) {
l[t -= 2] = ++i;
r[t] = --j;
}
else break;
}
int p = 1, q = n;
while (s[p++] == 'T');
while (s[q--] == 'T');
if (p - 1 < n - q)
q = n;
else p = 1;
t = sum + 1;
t -= (p - 1) * 2;
t -= (n - q) * 2;
l[t] = p, r[t] = q;
for (int i = p, j = q; i <= j; ) {
if (s[i] == 'T') {
l[t -= 2] = ++i;
r[t] = j;
}
else if (s[j] == 'T') {
l[t -= 2] = i;
r[t] = --j;
}
else if (i + 2 <= j) {
l[t -= 2] = ++i;
r[t] = --j;
}
else break;
}
while (m--) {
read(t);
if (!l[t])
puts("NIE");
else {
print(l[t], ' ');
print(r[t], '\n');
}
}
return 0;
}
} // namespace XSC062
B. 移方块 Shift
http://222.180.160.110:1024/contest/3430/problem/2
GM 找了一篇很详实的题解,然而我看不懂。于是我自行找了 另一篇看起来很可靠的题解,虽然只有两句话,但是我看懂了。
太妙了!因为我们并不关心具体在什么操作块上发生了什么,为了让操作 1 更方便,我们把数组首位相连,形成一个环,并标记起点为 1。
执行一次操作 1,就相当于将起点前移一位;而操作 2 就变得好理解了:我们可以通过若干次操作 1 + 一次操作 2,达到「选取任意一个元素并将其前移两位」的目的。
所以我们可以通过若干次这样的组合操作,类似选择排序,将数组排序完毕。
可是问题来了,仅当当前位置离目标位置的距离为偶数,我们可以通过两位两位地左移达到目的,当距离为奇数的时候怎么办呢?
很简单,我们再执行一次 2 操作,当前位视觉上就会向右移动一位,也就是说,两次操作 2 后,当前位左移 1 位。
那么问题就很简单了,若当前位与目标位的距离为奇数,进行若干次「左移 2 位」操作,再进行一次「左移 1 位」操作;否则,只用进行若干次「左移 2 位」操作。
又一个问题出现了:左移操作是需要用到当前位的后两位进行帮助的。若后两位中同时包含已经通过先前的操作归位的块和未归位的块,那么循环位移 1 位时就会打乱它们的位置,得不偿失。
哪个位置会出现这样的问题呢?只有第 \(n-1\) 号位(所以对 \(1\sim n-2\) 号位就可以用前面的方法进行处理),它的后两位同时包含了未处理的 \(n\) 号位和已处理的 1 号位,且对它来说只有不进行循环位移和循环位移 1 位两个选项。
对于最后两位,它们的值为 \(n-1\) 和 \(n\),位置也为 \(n-1\) 和 \(n\)。排列情况无非两种:
- \(n-1\) 在 \(n-1\) 位置上,\(n\) 在 \(n\) 位置上,排序完成,皆大欢喜。
- \(n-1\) 在 \(n\) 位置上,\(n\) 在 \(n-1\) 位置上。
考虑不会在值 \(1\) 和 \(1\) 号位中插入 \(n - 1\),导致打乱的左移 2 位操作。不难发现,将值 \(n\) 拿出,剩下的数列共有 \(n-1\) 项,首位相连成环会得到 \(n-1\) 个空隙。值 \(n\) 原本处于某个空隙(位置 \(n-2\) 和 \(n\) 之间,注意位置 \(n-1\) 本身已经被拿走了,所以不存在)中,并和其目标空隙(位置 \(n\) 和 \(1\) 之间)相差 1 位。若两位两位移动且空隙数为偶数,那么注定无法到达,当空隙数为奇数时,则一定可以到达。
也就是说,我们对值 \(n\) 进行不断的左移 2 位操作,若 \(n\) 位偶数则能成功,反之不行,输出无解。
时间复杂度 \(\mathcal O(n^2)\)。
namespace XSC062 {
using namespace fastIO;
const int maxn = 2e3 + 5;
struct _ {
int u;
bool t;
_ (int u1, bool t1) {
u = u1, t = t1;
}
};
int n, p;
int a[maxn];
std::vector<_> res;
inline void Adda(int x) {
x %= n;
if (x == 0)
return;
if (res.size() && res.back().t == 0) {
(res.back().u += x) %= n;
if (!res.back().u)
res.pop_back();
}
else res.push_back(_(x, 0));
return;
}
inline void Addb(int x) {
x %= 3;
if (x == 0)
return;
if (res.size() && res.back().t == 1) {
(res.back().u += x) %= 3;
if (!res.back().u)
res.pop_back();
}
else res.push_back(_(x, 1));
return;
}
inline void moveTo(int &b, int e) {
Adda((b + n - e) % n);
b = e;
return;
}
inline int pre(int p) {
return (p == 1) ? n : (p - 1);
}
inline int nxt(int p) {
return (p == n) ? 1 : (p + 1);
}
inline void Go1(int &p) {
p = pre(p);
int u = a[p];
a[p] = a[nxt(p)];
a[nxt(p)] = a[nxt(nxt(p))];
a[nxt(nxt(p))] = u;
Adda(1);
Addb(2);
return;
}
inline void Go2(int &p) {
p = pre(pre(p));
int u = a[nxt(p)];
a[nxt(p)] = a[p];
a[p] = a[nxt(nxt(p))];
a[nxt(nxt(p))] = u;
Adda(2);
Addb(1);
return;
}
int main() {
read(n), p = 1;
for (int i = 1; i <= n; ++i)
read(a[i]);
for (int i = 1; i <= n - 2; ++i) {
for (int j = i; j <= n; ++j) {
if (a[j] == i) {
moveTo(p, j);
break;
}
}
if (p == i)
continue;
while (p - i > 1)
Go2(p);
if (p - i)
Go1(p);
}
if (a[n - 1] == n) {
moveTo(p, n - 1);
if (n & 1) {
puts("NIE DA SIE");
return 0;
}
while (p != 1)
Go2(p);
}
for (int i = 1; i <= n; ++i) {
if (a[i] == 1) {
moveTo(p, i);
break;
}
}
print(res.size(), '\n');
for (auto &i: res) {
print(i.u);
if (i.t == 0)
printf("a ");
else printf("b ");
}
return 0;
}
} // namespace XSC062
C. 涂色
http://222.180.160.110:1024/contest/3430/problem/3
H. std::tr1::sum
http://222.180.160.110:1024/contest/3430/problem/8
GM 说他看不懂,所以把标称发下来我们自己看:
GM 说得很对,这份代码每一行都在装 ×。已经装到了连我都觉得装 × 的程度了。
所以我对它进行了稍微的美化。
#include <algorithm>
#include <cstdio>
#include <cstring>
const int N = 1e5 + 51;
int a[N], b[N], a2[N], b2[N];
int n, m, f, k, l, *pa, *pb, mx1, mx2;
inline int max(int x, int y) {
return x > y ? x : y;
}
inline int min(int x, int y) {
return x < y ? x : y;
}
int main() {
scanf("%d%d%d%d", &n, &m, &k, &l);
if (min(2 * l + 1, n * m) < k) {
puts("-1");
return 0;
}
for (int i = 0; i < n; i++) {
a[i] = min(i, k - 1);
mx1 = max(mx1, a[i]);
}
for (int i = 0; i < m; i++) {
b[i] = min(i * n, max(0, k - n));
mx1 = max(mx1, b[i]);
}
for (int i = 0; i < m; i++) {
b2[i] = min(i, k - 1);
mx2 = max(mx2, b2[i]);
}
for (int i = 0; i < n; i++) {
a2[i] = min(i * m, max(0, k - m));
mx2 = max(mx2, a2[i]);
}
if (mx1 <= mx2)
pa = a, pb = b;
else pa = a2, pb = b2;
if (min(mx1, mx2) > l) {
puts("-1");
return 0;
}
for (int i = 0; i < n; i++)
printf("%d%c", pa[i], " \n"[i == n - 1]);
for (int i = 0; i < m; i++)
printf("%d%c", pb[i], " \n"[i == m - 1]);
return 0;
}
以下是我的理解:
我们将 \(A_i\) 设置为 \(\min(i - 1, k - 1)\) 以保证无论 \(B_i\) 的值为多少,总能存在 \(\min(n, k)\) 个不同的和。
当 \(k\leqslant n\) 时,我们设置 \(B_i\) 为 \(0\) 以避免出现更多的不同和。以下讨论就只针对 \(k>n\) 的情况。
我们注意到,如果我们将 \(B_i\) 设置为 \((i - 1)\times n\),不会带来任何重复的和,又因为 \(A\) 中有 \(n\) 个不同的和,总的不同的和的数量将会增加 \(n\)。
我们设 \(k = x \times n + y\),其中 \(0\leqslant y< n\),
// The number of defferent sums will increase by n
// because there can't be any sums the same as it!
// It's fantastic. If 'k - n' is bigger than
// '(i - 1) * n', it means there are still more than
// i values waiting to get. Then during 1 ~ i,
// each loop will add n to the final number.
// Then what about the left '(k - n) % n' sums?
// It's clear that the back of array B is all the same
// 'k - n'. In brief, if we do like this, will it bring
// n different new sums? Certainly not. Think about it:
// If we see 'k - n' as 'x * n + y', and we already
// have 0, n, 2 * n, ... in array B. Because what
// we did just now, only less than n numbers in b will
// be filled with k - n, and y can only be 1 ~ n - 1,
// The number in array A is 0 ~ n - 1. If one number
// of array A is t, another is 't + w'(Of course w < n)
// The last number of array B is x * n, what we're
// dealing with is 'x * n + y'(Also, y < n), then what
// is the relation between 't + x * n + y' and
// 't + w + x * n'? For w <= y, we find that we
// must be able to find out a sum the same as this
// new one. Only the others are useful to the answer.
// In conclution, if we set the left part 'k - n',
// Only 'k - n' different sums will come out.
—— · EOF · ——
真的什么也不剩啦 😖