AtCoder Regular Contest 145

题目传送门:AtCoder Regular Contest 145

A - AB Palindrome

题意简述

给定一个长度为 \(n\) 的字符串 \(s\),仅包含字符 AB

你可以执行如下操作零或更多次:在 \(s\) 中选择相邻两个字符,将它们替换为 AB

请判断 \(s\) 是否能变为回文串。

数据范围:\(2 \le n \le 2 \times {10}^5\)

AC 代码
#include <cstdio>

const int MN = 200005;

int n;
char s[MN];

int main() {
	scanf("%d%s", &n, s + 1);
	if (n == 2)
		puts(s[1] == s[2] ? "Yes" : "No");
	else
		puts(s[1] == 'A' && s[n] == 'B' ? "No" : "Yes");
	return 0;
}

注意到,回文串必须保证 \(s_1 = s_n\)。所以我们有两个想法:

  • 如果 \(s_1 = \mathtt{B}\),可以在末尾操作,将 \(s_n\) 变为 \(\mathtt{B}\)
  • 如果 \(s_n = \mathtt{A}\),可以在开头操作,将 \(s_1\) 变为 \(\mathtt{A}\)

同时,如果这两个条件都不满足,即 \(s_1 = \mathtt{A}\)\(s_n = \mathtt{B}\),可以发现 \(s_1\)\(s_n\) 都不可能被改变,于是 \(s\) 不可能变为回文串。

所以,这两个条件至少要满足一个,是要让 \(s\) 变为回文串的必要条件。

进一步地,如果两个条件中至少有一个被满足,例如有 \(s_1 = \mathtt{B}\),可以具体地构造方案:

  • 依次操作 \(s_2 s_3\)\(s_3 s_4\)、……、\(s_{n - 1} s_n\),发现 \(s\) 变为 \(\mathtt{BAAA \cdots AAAB}\),为回文串。
  • 类似地,当 \(s_n = \mathtt{A}\) 时:
    依次操作 \(s_{n - 2} s_{n - 1}\)\(s_{n - 3} s_{n - 2}\)、……、\(s_1 s_2\),发现 \(s\) 变为 \(\mathtt{ABBB \cdots BBBA}\),为回文串。

于是我们构造性地证明了这两个条件的充分性。只需在 \(s_1 = \mathtt{A}\)\(s_n = \mathtt{B}\) 时输出 No 即可。

但是我们的证明中有疏忽,即当 \(n = 2\) 时,无法将 \(s = \mathtt{BA}\) 变为回文串。

进行对 \(n = 2\) 时的特判即可:只有当 \(s\) 自身已经为回文串时,才输出 Yes

时间复杂度为 \(\mathcal O (n)\)

B - AB Game

题意简述

Alice 和 Bob 在玩游戏。有一堆石子,初始时有 \(k\) 个石子。同时给定整数 \(a\)\(b\)

  • Alice 每次必须取走 \(a\) 的正倍数个石子。
  • Bob 每次必须取走 \(b\) 的正倍数个石子。

Alice 和 Bob 轮流操作,Alice 先手操作。在回合内无法操作的玩家输掉游戏。

现固定 \(a\)\(b\) 不变,假设 Alice 和 Bob 按最优策略行动,对 \(k = 1, 2, 3, \ldots, n - 1, n\) 中的每个游戏,求出 Alice 能赢得其中的多少个游戏。

数据范围:\(1 \le n, a, b \le {10}^{18}\)

AC 代码
#include <cstdio>
#include <algorithm>

typedef long long LL;

LL n, a, b;

int main() {
	scanf("%lld%lld%lld", &n, &a, &b);
	if (a <= b)
		printf("%lld\n", std::max(n - a + 1, 0ll));
	else
		if (n < a)
			puts("0");
		else
			printf("%lld\n", (n / a - 1) * b + std::min(n % a + 1, b));
	return 0;
}

如果一开始 Alice 就无法操作,那么就是 Alice 输掉游戏,这对应 \(k < a\) 的情况。

反过来,如果 Alice 可以操作,则 Alice 也想要把剩余石子数量 \(< b\) 的局面留给 Bob。注意到:

  1. 如果 \(a \le b\),则 Alice 操作后可以将石子数量变为 \([0, a)\) 内的数(即对 \(a\) 取模,即 \(k \bmod a\)),数量必然 \(< b\),故 Bob 输掉游戏。
  2. 如果 \(a > b\),则 Alice 操作后无法保证 Bob 无法操作。
    例如 \(k = 9\)\(a = 5\)\(b = 3\) 的情况,Alice 操作后,石子数量变为 \(4\),此时 Bob 仍可操作。
    然而,如果 Bob 仍可操作,在 Bob 的角度看,就转化为情况 1:操作后可以让 Alice 无法操作。
    回到 Alice 的角度,这即是在说,如果第一回合无法让 Bob 变得无法操作,则 Alice 就输掉游戏。
    那么,即是在问 Alice 能否将石子数量减少到 \(< b\),即问是否有 \(k \bmod a < b\)

总结:

  • \(a \le b\) 时,只要 \(k \ge a\),Alice 就赢得游戏。
  • \(a < b\) 时,如果 \(k \ge a\)\(k \bmod a < b\),则 Alice 赢得游戏。

对于 \(k \in [1, n]\),形式化为:

  • \(a \le b\) 时,Alice 赢得满足 \(k \in [a, n]\) 的游戏,共有 \(\max(n - a + 1, 0)\) 个。
  • \(a > b\) 时,Alice 赢得满足 \(k \in ([a, a + b) \cup [2 a, 2 a + b) \cup [3 a, 3 a + b) \cup \cdots) \cap [1, n]\) 的游戏,共有 \((\lfloor \frac{n}{a} \rfloor - 1) \cdot b + \min(n \bmod a + 1, b)\) 个。

时间复杂度为 \(\mathcal O (1)\)

C - Split and Maximize

题意简述

对于一个 \(1 \sim 2 n\) 的排列 \([p_1, p_2, \ldots, p_{2 n}]\),考虑将 \(p\) 拆分为两个子序列 \([a_1, a_2, \ldots, a_n]\)\([b_1, b_2, \ldots, b_n]\)\(p\)分数定义为所有拆分方案中的 \(\displaystyle \sum_{i = 1}^{n} a_i b_i\) 的最大值。

考虑 \(1 \sim 2 n\) 的所有排列,请求出分数取到最大值的排列的数量,对 \(998244353\) 取模。

数据范围:\(1 \le n \le 2 \times {10}^5\)

AC 代码
#include <cstdio>

typedef long long LL;
const int Mod = 998244353;
const int MN = 100005;

inline int qPow(int b, int e) {
	int a = 1;
	for (; e; e >>= 1, b = (int)((LL)b * b % Mod))
		if (e & 1) a = (int)((LL)a * b % Mod);
	return a;
}
inline int gInv(int b) { return qPow(b, Mod - 2); }

int Fac[MN * 2], iFac[MN * 2];
inline void Init(int N) {
	Fac[0] = 1;
	for (int i = 1; i <= N; ++i) Fac[i] = (int)((LL)Fac[i - 1] * i % Mod);
	iFac[N] = gInv(Fac[N]);
	for (int i = N; i >= 1; --i) iFac[i - 1] = (int)((LL)iFac[i] * i % Mod);
}
inline int Binom(int N, int M) {
	if (M < 0 || M > N) return 0;
	return (int)((LL)Fac[N] * iFac[M] % Mod * iFac[N - M] % Mod);
}

int n;

int main() {
	scanf("%d", &n);
	Init(2 * n);
	int ans = (Binom(2 * n, n) - Binom(2 * n, n - 1) + Mod) % Mod;
	ans = (int)((LL)ans * Fac[n] % Mod * qPow(2, n) % Mod);
	printf("%d\n", ans);
	return 0;
}

我们首先考虑分数的最大值是多少。根据排序不等式,容易知道最大值为 \(\displaystyle \sum_{i = 1}^{n} (2 i - 1) \cdot (2 i)\)

进一步地,即要求分数取到最大值的排列必须可以让每一对 \(2 i - 1\)\(2 i\) 都按顺序配对。

从而,一个排列的配对方式是被唯一确定的,当然其中有不合法的排列,即出现配对连线有包含的情况。

可以先枚举配对方式,然后分配 \(n\)\(\langle 2 i - 1, 2 i \rangle\) 的顺序,最后分配每一对内 \(2 i - 1\)\(2 i\) 的先后顺序。

配对方式即长度为 \(2 n\) 的括号序列数量,即 Catalan 数 \(C(n)\)。答案为 \(C(n) \cdot n! \cdot 2^n\)

线性时间预处理阶乘和阶乘逆元,时间复杂度为 \(\mathcal O (n)\)

D - Non Arithmetic Progression Set

题意简述

给定 \(n\) 和整数 \(m\),构造一个含有 \(n\) 个整数的集合 \(S\),满足如下条件:

  • \(S\) 中的数在 \([-{10}^7, {10}^7]\) 内且互不相同。
  • \(S\) 中的所有数之和恰好为 \(m\)
  • \(S\) 中不存在 \(3\) 个不同数构成等差数列。

可以证明,在数据范围内,满足条件的集合 \(S\) 一定存在。

数据范围:\(1 \le n \le {10}^4\)\(\lvert m \rvert \le n \cdot {10}^6\)

AC 代码
#include <cstdio>

typedef long long LL;

const int MN = 10005;

int N;
LL M;
int S[MN];

int main() {
	scanf("%d%lld", &N, &M);
	LL sum = 0;
	for (int i = 0; i < N; ++i) {
		int x = 0;
		for (int y = i, z = 2; y; y >>= 1, z *= 3)
			if (y & 1)
				x += z;
		S[i + 1] = x;
		sum += x;
	}
	int num = (int)(((M - sum) % N + N) % N);
	int diff = (int)((M - sum - num) / N);
	for (int i = 1; i <= N; ++i)
		S[i] += diff + (N - i < num ? 1 : 0);
	for (int i = 1; i <= N; ++i)
		printf("%d%c", S[i], " \n"[i == N]);
	return 0;
}

我们先不管总和为 \(m\) 的限制,先试图构造一个大小为 \(n\) 且不存在长度为 \(3\) 的等差数列的整数集,然后将它“调整”至和为 \(m\) 的最终状态。

如果构造出来的集合的总和为 \(s\),我们可以将集合内的所有数增加 \(\lfloor \frac{m - s}{n} \rfloor\),并最终做剩余的 \((m - s) \bmod n\) 次给元素增加 \(1\) 的调整。

由于 \(m / n \in [-{10}^6, {10}^6]\),这要求我们构造的集合的值域跨度不能太大,否则整体变化 \(\lfloor \frac{m - s}{n} \rfloor\) 后可能超出 \([-{10}^7, {10}^7]\) 的界限。同时我们构造的集合也要能够方便地进行部分元素的微调。

回到集合的构造上。注意存在等差数列可以看作为:存在两个元素(在数轴上的位置)的中点上有另一个元素。

一个简单的基于分治的想法是,先构造大小为大约一半(即 \(n / 2\))的集合,然后将其复制一份,平移到较远的位置,使得两部分距离足够远,不会产生等差数列。精细地考虑这个过程:

  • 要构造大小为 \(n\) 的集合,只需递归进 \(\lceil n / 2 \rceil\) 的情况,返回一个大小为 \(\lceil n / 2 \rceil\) 的集合,假设返回的集合的值域跨度为 \(t\)
  • 我们只需将其复制一份,然后平移 \(> 2 t\) 的距离,即可保证不存在两个元素的中点处有另一个元素,这是因为跨越两部分的元素对的中点会落在中间的空白区域内,而每一部分内的元素中不存在等差数列由递归保证。
  • 如果 \(n\) 是奇数,只需在构造的大小为 \(n - 1\) 的集合中随意丢弃一个元素。
  • \(n = 1\) 时是边界情况,返回 \(\{ 0 \}\) 即可。

富有经验的选手可以注意到,这实际上类似于 Cantor 集的构造方式,只不过此处是离散的。
于是,我们可以使用三进制数来更简单地构造一个大小为 \(n\) 的满足条件的集合:

  • 选取最小的 \(n\) 个三进制表示中仅包含数位 \(0\)\(2\) 的非负整数。
  • 换句话说,即是在 \([0, n)\) 中所有整数的二进制表示中将 \(1\) 换为 \(2\) 然后看作三进制表示后的整数。
  • \(\{ 0, 2, 6, 8, 18, 20, 24, 26, 54, \ldots \}\) 的长度为 \(n\) 的前缀。

此种构造方式下,大小为 \(n\) 的集合的值域跨度为 \(\mathcal O(n^{\log_2 3})\),其中 \(\log_2 3 \approx 1.58\)。所以当 \(n \le {10}^4\) 时,值域跨度约为 \({10}^6\) 级别,可以接受。

对于集合的微调,容易发现我们只需将其中最大的 \((m - s) \bmod n\) 个数加上 \(1\) 即可。其中不可能出现新的等差数列是容易证明的(只会将每次三分集的两侧间的距离拉得更大)。

不加精细实现地模拟二进制转三进制的过程,时间复杂度为 \(\mathcal O (n \log n)\)

E - Adjacent XOR

题意简述

给定两个长度为 \(n\) 的非负整数序列 \([a_1, a_2, \ldots, a_n]\)\([b_1, b_2, \ldots, b_n]\)

你可以执行如下操作零或更多次:指定一个下标 \(k\)\(1 \le k \le n\)),将 \(i \in [2, k]\) 中的每个下标上的数 \(a_i\) 赋值为 \(a_i \oplus a_{i - 1}\),注意所有赋值是同时执行的。本题中 \(\oplus\) 表示二进制按位异或。

问在 \(70000\) 次操作内,是否能将 \(a\) 变为 \(b\),如果可能,请求出具体操作序列。你不需要最小化操作次数。

数据范围:\(2 \le n \le 1000\)\(0 \le a_i, b_i < 2^m\)\(m = 60\)

AC 代码
#include <cstdio>
#include <algorithm>
#include <vector>

typedef unsigned long long ULL;

struct Basis {
	int siz;
	ULL b[60];
	ULL c[60];
	Basis() {
		siz = 0;
		std::fill(b, b + 60, 0llu);
		std::fill(c, c + 60, 0llu);
	}
	bool Insert(ULL x) {
		ULL y = 0llu;
		for (int j = 59; j >= 0; --j) if (x >> j & 1) {
			if (b[j])
				x ^= b[j],
				y ^= c[j];
			else {
				y ^= 1llu << siz++;
				for (int k = 0; k <= j - 1; ++k)
					if (b[k] && (x >> k & 1))
						x ^= b[k],
						y ^= c[k];
				b[j] = x, c[j] = y;
				for (int k = j + 1; k <= 59; ++k)
					if (b[k] >> j & 1)
						b[k] ^= x,
						c[k] ^= y;
				return true;
			}
		}
		return false;
	}
	bool Contains(ULL x) {
		for (int j = 59; j >= 0; --j) if (x >> j & 1) {
			if (b[j])
				x ^= b[j];
			else
				return false;
		}
		return true;
	}
	ULL Coordinate(ULL x) {
		ULL y = 0llu;
		for (int j = 59; j >= 0; --j) if (x >> j & 1)
			x ^= b[j], y ^= c[j];
		return y;
	}
};

const int MN = 1005;

int N;
ULL A[MN], B[MN];

std::vector<int> Ans;
void Operate(int p) {
	Ans.push_back(p);
	for (int i = 2; i <= p; ++i)
		B[i] ^= B[i - 1];
}
void Solve() {
	for (int t = N; t >= 2; --t) {
		if (B[t] == A[t])
			continue;
		Basis Z;
		int pos[60], cnt = 0;
		for (int i = 1; i <= t; ++i)
			if (Z.Insert(B[i]))
				pos[cnt++] = i;
		for (int i = 1; i <= t; ++i)
			B[i] = Z.Coordinate(B[i]),
			A[i] = Z.Coordinate(A[i]);
		while (true) {
			ULL s = A[t];
			for (int i = 1; i <= t; ++i) s ^= B[i];
			if (!s) break;
			int j = 0;
			while (s >> (j + 1)) ++j;
			Operate(pos[j] + 1);
		}
		Operate(t);
	}
}

int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; ++i) scanf("%llu", &A[i]);
	for (int i = 1; i <= N; ++i) scanf("%llu", &B[i]);
	Basis Z;
	for (int i = 1; i <= N; ++i) {
		if (!Z.Contains(A[i] ^ B[i]))
			return puts("No"), 0;
		Z.Insert(B[i]);
	}
	Solve();
	std::reverse(Ans.begin(), Ans.end());
	int siz = (int)Ans.size();
	printf("Yes\n%d\n", siz);
	for (int i = 0; i < siz; ++i)
		printf("%d%c", Ans[i], " \n"[i == siz - 1]);
	return 0;
}

首先我们考虑,在不限制操作次数时,如何判断无解。

  • 容易发现,无论怎么操作,\(a_i\) 都只会与前面的数的一个子集做异或和,即总存在 \(I \subseteq \{ 1, 2, \ldots, i - 1 \}\),此时 \(a_i\) 会变为 \(\displaystyle a_i \oplus \bigoplus_{i \in I} a_i\)
  • 注意 \(I\) 可以为空集,但 \(a_i\) 本身永远不会被消去,例如 \([0, 1]\) 不可能变为 \([0, 0]\)
  • 反过来,我们猜测这个条件也是充分的,即给出每个 \(i\) 对应的子集 \(I_i\)\(1 \le i \le n\)),总是可以构造操作方式。
  • 容易使用归纳法证明这个结论,简单来说就是:
    • 如果 \(n - 1 \in I_n\),则先在 \([1, n - 1]\) 内进行操作将 \(a_{n - 1}\) 变为 \(\displaystyle a_{n - 1} \oplus \bigoplus_{i \in I_n \setminus (n - 1)} a_i\),这恰好等于 \(\displaystyle \bigoplus_{i \in I_n} a_i\),于是再进行一次 \(k = n\) 的操作即可得到 \(a_n\) 的最终结果。
    • 如果 \(n - 1 \notin I_n\),则先进行一次 \(k = n\) 的操作,将 \(a_n\) 变为 \(a_n \oplus a_{n - 1}\),则可转化为 \(n - 1 \in I_n\) 的情况。
  • 使用线性代数的语言来说,就是 \(b_i \oplus a_i\) 必须落在 \(\{ a_1, a_2, \ldots, a_i \}\) 张成的子空间中。而反过来只要这个条件被满足,总可以进行操作将 \(a\) 变成 \(b\)(但操作次数不一定在 \(70000\) 内)。
    (此处在线性空间 \(\mathbb{F}_2^m\) 中讨论,即 OI 中的“二进制线性基”相关算法)

接下来我们需要在有解时构造长度不超过 \(70000\) 的操作序列。有理由猜测 \(70000\) 对应 \(n m + \mathcal O(n)\)\(n \le 1000\)\(m \le 60\))。

注意到一次操作相当于对一个前缀进行差分,反过来考虑就是对一个前缀进行前缀和
即从 \(b\) 操作回 \(a\) 的过程是,每次指定下标 \(k\),对于每个 \(1 \le i \le k\),令 \(\displaystyle b'_i \gets \bigoplus_{j = 1}^{i} b_j\)
当然,在做此类题目时,反向考虑操作序列的“时光倒流”是常见思路。不过,由于“差分”仅与两个相邻元素有关,似乎更加方便,我在做题时花了很长时间考虑时间正向的过程,最终仅根据正向的思路推出了做法的关键步骤。此时我才发现这一步骤在反向考虑时更为自然,于是切换到反向的思路完善了最后的细节。接下来我也以反向的模型出发重写整个思路。

  • 需要指出的是,无论是正向还是反向考虑操作,只要操作前有解,操作后也一定有解,即操作不会改变解的存在性。从每个前缀张成的空间考虑,这个结论是显然的,因为每次操作都是可逆的,且保持前缀子空间不变。这一结论即是说,不需要担心在众多操作中选取到了不合法的操作导致有解变为无解,于是后文中可以放心随意使用操作

一个自然的想法是,先将 \(b_n\) 变为 \(a_n\),然后递归进 \(n - 1\) 的情况。其中每一步花费至多 \(m + \mathcal O (1)\) 次操作(或至少均摊)。

要改变 \(b_n\) 的值,至少要进行一次 \(k = n\) 的操作,将 \(b_n\) 变为 \(\displaystyle \bigoplus_{i = 1}^{n} b_i\),即 \(b\) 的所有元素的异或和。这个值自然不一定恰好等于 \(a_n\),所以需要进行一些操作改变 \(b\) 中所有元素的异或和。

为了方便后续考虑,我们可以将 \(b\)\(a\) 进行重赋值

  • 只须保证在相同的操作下,新的赋值与原本的序列可以对应。用线性代数的语言来说,即是进行基变换。
    我们知道 \(b\)\(a\) 中的元素分别张成同一个子空间,在 \(b\) 中选择这个子空间的字典序最小的基 \(\{ b_{i_0}, b_{i_1}, \ldots, b_{i_{r - 1}} \}\),其中 \(r\) 即是子空间维数,且不妨令 \(i_0 < i_1 < \cdots < i_{r - 1}\)。可以认为这个基是有序的,并且将 \(b_{i_j}\) 重赋值为 \(2^j\)(作为二进制数)(显然 \(2^0, 2^1, \ldots, 2^{r - 1}\) 线性无关)。由此可以将 \(b\)\(a\) 中的每个元素重赋值。
  • 由此,我们发现(重赋值后的)\(b\) 形如 \([\overline{000}, \ldots, \overline{000}, \color{red}{\overline{001}}, \overline{00{\ast}}, \ldots, \overline{00{\ast}}, \color{red}{\overline{010}}, \overline{0{\ast}{\ast}}, \ldots, \overline{0{\ast}{\ast}}, \color{red}{\overline{100}}, \overline{{\ast}{\ast}{\ast}}, \ldots, \overline{{\ast}{\ast}{\ast}}]\),其中作为基的元素被标红,而 \({\ast}\) 表示可以任取 \(0\)\(1\)
    同时,\(b\) 通过操作能够得到的序列形如 \([\overline{000}, \ldots, \overline{000}, \color{red}{\overline{001}}, \overline{00{\ast}}, \ldots, \overline{00{\ast}}, \color{red}{\overline{01{\ast}}}, \overline{0{\ast}{\ast}}, \ldots, \overline{0{\ast}{\ast}}, \color{red}{\overline{1{\ast}{\ast}}}, \overline{{\ast}{\ast}{\ast}}, \ldots, \overline{{\ast}{\ast}{\ast}}]\)。当然,\(a\) 也必须符合此形式。

要将 \(b\) 中所有元素的异或和改变至 \(a_n\),我们可以记 \(\displaystyle x = a_n \oplus \bigoplus_{i = 1}^{n} b_i\) 来表示差异,只需要让 \(x\) 变为 \(0\) 即可。经过上文的重赋值,这个需求变得非常容易解决:

  • 考虑每次解决 \(x\) 的最高非零位,假设为 \(2^j\),则对应着基中的 \(b_{i_j}\)
  • 注意在 \(b\)\(b_{i_j}\) 是第一个拥有 \(2^j\) 这一位的元素。
  • 于是操作 \(k = i_j + 1\) 会让 \(b_{i_j}\) 恰好增加一次在 \(x\) 中的贡献,也就是抵消掉了。
  • 由此,\(x\) 中的 \(2^j\) 即被去除。
  • 更巧妙的是,由于更高位的 \(1\) 必然不会往前传递,并且在 \(b\) 中的基按从低至高的顺序排列,所以我们从高位向低位操作时就不会破坏 \(x\) 中已清空的高位。而破坏未经考虑的低位是没有关系的,因为它们会在后续过程中被清空,也不会反向影响到此位 \(2^j\)

由于基的大小最多为 \(m\),按上述流程将 \(x\) 变为 \(0\) 最多花费 \(m\) 次操作。

此时,有 \(x = 0\) 后,再操作 \(k = n\) 即可将 \(b_n\) 变为 \(a_n\)。然后即可进行长度减去 \(1\) 的递归。

递归中的每一步花费至多 \(m + 1\) 次操作。总共递归 \(n - 1\) 轮(\(n = 1\) 时无需考虑)。总操作次数至多为 \((n - 1) \cdot (m + 1)\)

最终将 \(b\) 变为 \(a\) 的操作序列反向输出即可。

压位实现线性基,每次递归直接重构,时间复杂度为 \(\mathcal O (n^2 m)\)

F - Modulo Sum of Increasing Sequences

题意简述
AC 代码
posted @ 2022-08-08 19:08  粉兔  阅读(1061)  评论(0编辑  收藏  举报