做题记录 // 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\)。排列情况无非两种:

  1. \(n-1\)\(n-1\) 位置上,\(n\)\(n\) 位置上,排序完成,皆大欢喜。
  2. \(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.
posted @ 2023-03-18 10:59  XSC062  阅读(45)  评论(0编辑  收藏  举报