AtCoder Regular Contest 145

题目传送门:AtCoder Regular Contest 145

A - AB Palindrome

题意简述

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

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

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

数据范围:2n2×105

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;
}

注意到,回文串必须保证 s1=sn。所以我们有两个想法:

  • 如果 s1=B,可以在末尾操作,将 sn 变为 B
  • 如果 sn=A,可以在开头操作,将 s1 变为 A

同时,如果这两个条件都不满足,即 s1=Asn=B,可以发现 s1sn 都不可能被改变,于是 s 不可能变为回文串。

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

进一步地,如果两个条件中至少有一个被满足,例如有 s1=B,可以具体地构造方案:

  • 依次操作 s2s3s3s4、……、sn1sn,发现 s 变为 BAAAAAAB,为回文串。
  • 类似地,当 sn=A 时:
    依次操作 sn2sn1sn3sn2、……、s1s2,发现 s 变为 ABBBBBBA,为回文串。

于是我们构造性地证明了这两个条件的充分性。只需在 s1=Asn=B 时输出 No 即可。

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

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

时间复杂度为 O(n)

B - AB Game

题意简述

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

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

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

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

数据范围:1n,a,b1018

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

总结:

  • ab 时,只要 ka,Alice 就赢得游戏。
  • a<b 时,如果 kakmoda<b,则 Alice 赢得游戏。

对于 k[1,n],形式化为:

  • ab 时,Alice 赢得满足 k[a,n] 的游戏,共有 max(na+1,0) 个。
  • a>b 时,Alice 赢得满足 k([a,a+b)[2a,2a+b)[3a,3a+b))[1,n] 的游戏,共有 (na1)b+min(nmoda+1,b) 个。

时间复杂度为 O(1)

C - Split and Maximize

题意简述

对于一个 12n 的排列 [p1,p2,,p2n],考虑将 p 拆分为两个子序列 [a1,a2,,an][b1,b2,,bn]p分数定义为所有拆分方案中的 i=1naibi 的最大值。

考虑 12n 的所有排列,请求出分数取到最大值的排列的数量,对 998244353 取模。

数据范围:1n2×105

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;
}

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

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

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

可以先枚举配对方式,然后分配 n2i1,2i 的顺序,最后分配每一对内 2i12i 的先后顺序。

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

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

D - Non Arithmetic Progression Set

题意简述

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

  • S 中的数在 [107,107] 内且互不相同。
  • S 中的所有数之和恰好为 m
  • S 中不存在 3 个不同数构成等差数列。

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

数据范围:1n104|m|n106

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,我们可以将集合内的所有数增加 msn,并最终做剩余的 (ms)modn 次给元素增加 1 的调整。

由于 m/n[106,106],这要求我们构造的集合的值域跨度不能太大,否则整体变化 msn 后可能超出 [107,107] 的界限。同时我们构造的集合也要能够方便地进行部分元素的微调。

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

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

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

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

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

此种构造方式下,大小为 n 的集合的值域跨度为 O(nlog23),其中 log231.58。所以当 n104 时,值域跨度约为 106 级别,可以接受。

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

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

E - Adjacent XOR

题意简述

给定两个长度为 n 的非负整数序列 [a1,a2,,an][b1,b2,,bn]

你可以执行如下操作零或更多次:指定一个下标 k1kn),将 i[2,k] 中的每个下标上的数 ai 赋值为 aiai1,注意所有赋值是同时执行的。本题中 表示二进制按位异或。

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

数据范围:2n10000ai,bi<2mm=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;
}

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

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

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

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

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

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

要改变 bn 的值,至少要进行一次 k=n 的操作,将 bn 变为 i=1nbi,即 b 的所有元素的异或和。这个值自然不一定恰好等于 an,所以需要进行一些操作改变 b 中所有元素的异或和。

为了方便后续考虑,我们可以将 ba 进行重赋值

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

要将 b 中所有元素的异或和改变至 an,我们可以记 x=ani=1nbi 来表示差异,只需要让 x 变为 0 即可。经过上文的重赋值,这个需求变得非常容易解决:

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

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

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

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

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

压位实现线性基,每次递归直接重构,时间复杂度为 O(n2m)

F - Modulo Sum of Increasing Sequences

题意简述
AC 代码
posted @   粉兔  阅读(1220)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探
点击右上角即可分享
微信分享提示