AtCoder Regular Contest 145
题目传送门:AtCoder Regular Contest 145。
A - AB Palindrome
题意简述
给定一个长度为 的字符串 ,仅包含字符 A
或 B
。
你可以执行如下操作零或更多次:在 中选择相邻两个字符,将它们替换为 AB
。
请判断 是否能变为回文串。
数据范围:。
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;
}
注意到,回文串必须保证 。所以我们有两个想法:
- 如果 ,可以在末尾操作,将 变为 。
- 如果 ,可以在开头操作,将 变为 。
同时,如果这两个条件都不满足,即 且 ,可以发现 和 都不可能被改变,于是 不可能变为回文串。
所以,这两个条件至少要满足一个,是要让 变为回文串的必要条件。
进一步地,如果两个条件中至少有一个被满足,例如有 ,可以具体地构造方案:
- 依次操作 、、……、,发现 变为 ,为回文串。
- 类似地,当 时:
依次操作 、、……、,发现 变为 ,为回文串。
于是我们构造性地证明了这两个条件的充分性。只需在 且 时输出 No
即可。
但是我们的证明中有疏忽,即当 时,无法将 变为回文串。
进行对 时的特判即可:只有当 自身已经为回文串时,才输出 Yes
。
时间复杂度为 。
B - AB Game
题意简述
Alice 和 Bob 在玩游戏。有一堆石子,初始时有 个石子。同时给定整数 和 。
- Alice 每次必须取走 的正倍数个石子。
- Bob 每次必须取走 的正倍数个石子。
Alice 和 Bob 轮流操作,Alice 先手操作。在回合内无法操作的玩家输掉游戏。
现固定 和 不变,假设 Alice 和 Bob 按最优策略行动,对 中的每个游戏,求出 Alice 能赢得其中的多少个游戏。
数据范围:。
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 输掉游戏,这对应 的情况。
反过来,如果 Alice 可以操作,则 Alice 也想要把剩余石子数量 的局面留给 Bob。注意到:
- 如果 ,则 Alice 操作后可以将石子数量变为 内的数(即对 取模,即 ),数量必然 ,故 Bob 输掉游戏。
- 如果 ,则 Alice 操作后无法保证 Bob 无法操作。
例如 、、 的情况,Alice 操作后,石子数量变为 ,此时 Bob 仍可操作。
然而,如果 Bob 仍可操作,在 Bob 的角度看,就转化为情况 1:操作后可以让 Alice 无法操作。
回到 Alice 的角度,这即是在说,如果第一回合无法让 Bob 变得无法操作,则 Alice 就输掉游戏。
那么,即是在问 Alice 能否将石子数量减少到 ,即问是否有 。
总结:
- 当 时,只要 ,Alice 就赢得游戏。
- 当 时,如果 且 ,则 Alice 赢得游戏。
对于 ,形式化为:
- 当 时,Alice 赢得满足 的游戏,共有 个。
- 当 时,Alice 赢得满足 的游戏,共有 个。
时间复杂度为 。
C - Split and Maximize
题意简述
对于一个 的排列 ,考虑将 拆分为两个子序列 和 , 的分数定义为所有拆分方案中的 的最大值。
考虑 的所有排列,请求出分数取到最大值的排列的数量,对 取模。
数据范围:。
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;
}
我们首先考虑分数的最大值是多少。根据排序不等式,容易知道最大值为 。
进一步地,即要求分数取到最大值的排列必须可以让每一对 与 都按顺序配对。
从而,一个排列的配对方式是被唯一确定的,当然其中有不合法的排列,即出现配对连线有包含的情况。
可以先枚举配对方式,然后分配 对 的顺序,最后分配每一对内 与 的先后顺序。
配对方式即长度为 的括号序列数量,即 Catalan 数 。答案为 。
线性时间预处理阶乘和阶乘逆元,时间复杂度为 。
D - Non Arithmetic Progression Set
题意简述
给定 和整数 ,构造一个含有 个整数的集合 ,满足如下条件:
- 中的数在 内且互不相同。
- 中的所有数之和恰好为 。
- 中不存在 个不同数构成等差数列。
可以证明,在数据范围内,满足条件的集合 一定存在。
数据范围:,。
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;
}
我们先不管总和为 的限制,先试图构造一个大小为 且不存在长度为 的等差数列的整数集,然后将它“调整”至和为 的最终状态。
如果构造出来的集合的总和为 ,我们可以将集合内的所有数增加 ,并最终做剩余的 次给元素增加 的调整。
由于 ,这要求我们构造的集合的值域跨度不能太大,否则整体变化 后可能超出 的界限。同时我们构造的集合也要能够方便地进行部分元素的微调。
回到集合的构造上。注意存在等差数列可以看作为:存在两个元素(在数轴上的位置)的中点上有另一个元素。
一个简单的基于分治的想法是,先构造大小为大约一半(即 )的集合,然后将其复制一份,平移到较远的位置,使得两部分距离足够远,不会产生等差数列。精细地考虑这个过程:
- 要构造大小为 的集合,只需递归进 的情况,返回一个大小为 的集合,假设返回的集合的值域跨度为 。
- 我们只需将其复制一份,然后平移 的距离,即可保证不存在两个元素的中点处有另一个元素,这是因为跨越两部分的元素对的中点会落在中间的空白区域内,而每一部分内的元素中不存在等差数列由递归保证。
- 如果 是奇数,只需在构造的大小为 的集合中随意丢弃一个元素。
- 当 时是边界情况,返回 即可。
富有经验的选手可以注意到,这实际上类似于 Cantor 集的构造方式,只不过此处是离散的。
于是,我们可以使用三进制数来更简单地构造一个大小为 的满足条件的集合:
- 选取最小的 个三进制表示中仅包含数位 和 的非负整数。
- 换句话说,即是在 中所有整数的二进制表示中将 换为 然后看作三进制表示后的整数。
- 即 的长度为 的前缀。
此种构造方式下,大小为 的集合的值域跨度为 ,其中 。所以当 时,值域跨度约为 级别,可以接受。
对于集合的微调,容易发现我们只需将其中最大的 个数加上 即可。其中不可能出现新的等差数列是容易证明的(只会将每次三分集的两侧间的距离拉得更大)。
不加精细实现地模拟二进制转三进制的过程,时间复杂度为 。
E - Adjacent XOR
题意简述
给定两个长度为 的非负整数序列 和 。
你可以执行如下操作零或更多次:指定一个下标 (),将 中的每个下标上的数 赋值为 ,注意所有赋值是同时执行的。本题中 表示二进制按位异或。
问在 次操作内,是否能将 变为 ,如果可能,请求出具体操作序列。你不需要最小化操作次数。
数据范围:,,。
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;
}
首先我们考虑,在不限制操作次数时,如何判断无解。
- 容易发现,无论怎么操作, 都只会与前面的数的一个子集做异或和,即总存在 ,此时 会变为 。
- 注意 可以为空集,但 本身永远不会被消去,例如 不可能变为 。
- 反过来,我们猜测这个条件也是充分的,即给出每个 对应的子集 (),总是可以构造操作方式。
- 容易使用归纳法证明这个结论,简单来说就是:
- 如果 ,则先在 内进行操作将 变为 ,这恰好等于 ,于是再进行一次 的操作即可得到 的最终结果。
- 如果 ,则先进行一次 的操作,将 变为 ,则可转化为 的情况。
- 使用线性代数的语言来说,就是 必须落在 张成的子空间中。而反过来只要这个条件被满足,总可以进行操作将 变成 (但操作次数不一定在 内)。
(此处在线性空间 中讨论,即 OI 中的“二进制线性基”相关算法)
接下来我们需要在有解时构造长度不超过 的操作序列。有理由猜测 对应 (,)。
注意到一次操作相当于对一个前缀进行差分,反过来考虑就是对一个前缀进行前缀和。
即从 操作回 的过程是,每次指定下标 ,对于每个 ,令 。
当然,在做此类题目时,反向考虑操作序列的“时光倒流”是常见思路。不过,由于“差分”仅与两个相邻元素有关,似乎更加方便,我在做题时花了很长时间考虑时间正向的过程,最终仅根据正向的思路推出了做法的关键步骤。此时我才发现这一步骤在反向考虑时更为自然,于是切换到反向的思路完善了最后的细节。接下来我也以反向的模型出发重写整个思路。
- 需要指出的是,无论是正向还是反向考虑操作,只要操作前有解,操作后也一定有解,即操作不会改变解的存在性。从每个前缀张成的空间考虑,这个结论是显然的,因为每次操作都是可逆的,且保持前缀子空间不变。这一结论即是说,不需要担心在众多操作中选取到了不合法的操作导致有解变为无解,于是后文中可以放心随意使用操作。
一个自然的想法是,先将 变为 ,然后递归进 的情况。其中每一步花费至多 次操作(或至少均摊)。
要改变 的值,至少要进行一次 的操作,将 变为 ,即 的所有元素的异或和。这个值自然不一定恰好等于 ,所以需要进行一些操作改变 中所有元素的异或和。
为了方便后续考虑,我们可以将 和 进行重赋值:
- 只须保证在相同的操作下,新的赋值与原本的序列可以对应。用线性代数的语言来说,即是进行基变换。
我们知道 和 中的元素分别张成同一个子空间,在 中选择这个子空间的字典序最小的基 ,其中 即是子空间维数,且不妨令 。可以认为这个基是有序的,并且将 重赋值为 (作为二进制数)(显然 线性无关)。由此可以将 和 中的每个元素重赋值。 - 由此,我们发现(重赋值后的) 形如 ,其中作为基的元素被标红,而 表示可以任取 或 。
同时, 通过操作能够得到的序列形如 。当然, 也必须符合此形式。
要将 中所有元素的异或和改变至 ,我们可以记 来表示差异,只需要让 变为 即可。经过上文的重赋值,这个需求变得非常容易解决:
- 考虑每次解决 的最高非零位,假设为 ,则对应着基中的 。
- 注意在 中 是第一个拥有 这一位的元素。
- 于是操作 会让 恰好增加一次在 中的贡献,也就是抵消掉了。
- 由此, 中的 即被去除。
- 更巧妙的是,由于更高位的 必然不会往前传递,并且在 中的基按从低至高的顺序排列,所以我们从高位向低位操作时就不会破坏 中已清空的高位。而破坏未经考虑的低位是没有关系的,因为它们会在后续过程中被清空,也不会反向影响到此位 。
由于基的大小最多为 ,按上述流程将 变为 最多花费 次操作。
此时,有 后,再操作 即可将 变为 。然后即可进行长度减去 的递归。
递归中的每一步花费至多 次操作。总共递归 轮( 时无需考虑)。总操作次数至多为 。
最终将 变为 的操作序列反向输出即可。
压位实现线性基,每次递归直接重构,时间复杂度为 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探