AtCoder Beginner Contest 433 题解
AtCoder Beginner Contest 433 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2025-11-22 |
| 比赛链接 | https://atcoder.jp/contests/abc433 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场整体难度适中偏上。A 是简单的一元方程判断,B 是基础的双重遍历;C 与 F 围绕"1122 串"这一概念展开,分别考察连续子串计数与子序列计数,前者用 run-length encoding,后者需要范德蒙德恒等式的组合技巧;D 是经典的"按位数分组 + 模算术"计数,属于面向量级题的基本功;E 是一道有区分度的构造题,要求构造一个行、列最大值给定且元素互不相同的矩阵,贪心 + 充分条件判断即可;F 需要较扎实的组合恒等式推导;G 则是全场最难,用后缀自动机把字符串游戏转化为 DAG 上的博弈,再跑记忆化博弈 DP。总体来说 C/D 是分水岭,E/F/G 值得重点掌握。
A - Happy Birthday! 4
题目链接:https://atcoder.jp/contests/abc433/tasks/abc433_a
题目大意
给定三个正整数 。高桥君现在 岁,青木君现在 岁,两人在每年 1 月 1 日会同时增加一岁。问在未来的某个时刻(含今年,即不增加岁数也算)是否存在一个时刻,使得高桥君的年龄恰好是青木君年龄的 倍。存在输出 Yes,否则输出 No。
思路
关键观察:两人的年龄永远是整数,且每过一年两人同时加 1。因此设 ()为经过的年数,题目就是在问:是否存在非负整数 使得
这是一个关于 的一元一次方程,直接解出 :
因为 ,所以 ,不会出现除零。 必须是非负整数,因此答案为 Yes 当且仅当同时满足:
- (保证 );
- 能被 整除(保证 是整数)。
为什么正确: 只要取满足等式的非负整数,那个时刻( 年后)两人的年龄就恰好成 倍关系;反之若 是负数,说明该比值关系已经"过去"了,永远不会再出现(因为当 时 ,若 则比值在递减方向上是单向的,错过了就不会再来)。
注意边界:题目特别强调"包括今年",即允许 。 对应 ,此时两个条件都满足,输出 Yes(如样例 3:,,故当前就满足)。另一个易错点是当 为负但恰好能被 整除时(例如 ),整除性成立但 ,必须输出 No——所以 的判断必不可少。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int X, Y, Z;
cin >> X >> Y >> Z;
int d = X - Y * Z;
if (d >= 0 && d % (Z - 1) == 0) cout << "Yes\n";
else cout << "No\n";
return 0;
}官方题解给出的 Python 实现:
x, y, z = map(int, input().split())
if (x - y * z) % (z - 1) == 0 and x - y * z >= 0:
print("Yes")
else:
print("No")复杂度分析
- 时间复杂度:
- 空间复杂度:
B - Nearest Taller
题目链接:https://atcoder.jp/contests/abc433/tasks/abc433_b
题目大意
有 个人从左到右排成一列,第 个人的身高为 。对每个 ,问是否存在站在第 个人左边且比他高的人,若存在,找出其中离第 个人最近的一个人的编号;不存在则输出 。
思路
关键观察:要找的是"最近的、比 高的人",而"最近"意味着我们应该从 左边紧邻的人开始,由近及远地逐个检查。一旦找到第一个满足 的 ,它必然就是答案。
算法:对每个 ,用内层循环让 从 递减到 ,第一个满足 的 即为所求;若内层循环跑完都没有找到,输出 。
为什么正确:由于内层循环严格按 的顺序扫描,第一个人被找到时就是所有满足条件者中距离 最近的,因此正确。,最坏情况下每个 都要扫描 个位置,总复杂度 ,绰绰有余。
易错点:题目中身高允许相等(见样例 2),判断时必须是严格的 >;输出的是编号 (数组下标从 0 开始)。另外"左边"不包含第 个人自己。
若把 放大到 级别,本题也可以用单调栈在 内求解(维护一个高度严格递减的栈,每次弹出所有 的栈顶,此时栈顶即为答案),但本题数据范围不需要。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n; i++) {
int ans = -1;
for (int j = i - 1; j >= 0; j--) {
if (a[j] > a[i]) {
ans = j + 1;
break;
}
}
cout << ans << '\n';
}
return 0;
}官方题解给出的 Python 实现:
n = int(input())
a = list(map(int, input().split()))
for i in range(n):
ans = -1
for j in range(i - 1, -1, -1):
if a[j] > a[i]:
ans = j + 1
break
print(ans)复杂度分析
- 时间复杂度:
- 空间复杂度:
C - 1122 Substring 2
题目链接:https://atcoder.jp/contests/abc433/tasks/abc433_c
题目大意
给定一个由数字组成的字符串 。一个非空字符串 被称为 1122 串,如果它同时满足:
- 是偶数;
- 的前 个字符全部相同,后 个字符也全部相同;
- 的第一个字符加 等于 的最后一个字符。
例如 1122、01、444555 是 1122 串,而 1222、90 不是。求 中有多少个子串是 1122 串(从不同位置取出的相同子串要分别计数)。。
思路
问题转化:设一个 1122 子串 ,其中前半段都是数字 ,后半段都是数字 。因为前后两半长度相等,分界点必然位于 的某两个相邻位置 与 之间,即
等价于 。此时 、。也就是说,每个 1122 子串都唯一对应一个满足 的"分界"位置 。
统计:固定分界 (满足 )。设:
- 为以 结尾的连续相同字符段的长度(即左段长度);
- 为以 开头的连续相同字符段的长度(即右段长度)。
子串起点 必须落在左段内(有 种选择),终点 必须落在右段内(有 种选择),并且还要满足 。对于起点 ,唯一合法的终点是 ;当 遍历左段的 个位置时, 恰好遍历 这 个位置,其中落在右段 内的个数为
因此该分界对答案的贡献为 。对每个满足 的 求和即可。
为什么每个子串恰好被计一次:一个 1122 子串的两半各为一段连续相同数字,前半段的最后一个字符 与后半段的第一个字符 紧邻且相差 1,因此分界是唯一确定的;起点落在左段、终点落在右段且满足 ,与上述计数一一对应,不会重复也不会遗漏。
实现:对 做 run-length encoding,得到若干段 。然后对相邻两段,若前段的数字加 等于后段的数字,就把 累加进答案。注意比较时用整数(或字符):'9'+1 不再是数字,条件自动不成立,无需特判,但也可以显式跳过。
以样例 2 7788788 为例:RLE 为 7878。相邻段中 7→8 出现两次(第一处 ,第二处 ),合计 ,与样例一致。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
int n = s.size();
vector<pair<char, int>> runs; // 各段数字与其长度
for (int i = 0; i < n; ) {
int j = i;
while (j < n && s[j] == s[i]) j++;
runs.push_back({s[i], j - i});
i = j;
}
long long ans = 0;
for (int i = 0; i + 1 < (int)runs.size(); i++) {
if (runs[i].first + 1 == runs[i + 1].first) // 前段 +1 等于后段
ans += min(runs[i].second, runs[i + 1].second);
}
cout << ans << '\n';
return 0;
}官方题解(解法一,RLE)的 Python 实现:
from itertools import groupby
s = input()
a = [(v, len(list(x))) for v, x in groupby(s)]
ans = 0
for i in range(len(a) - 1):
if int(a[i][0]) + 1 == int(a[i + 1][0]):
ans += min(a[i][1], a[i + 1][1])
print(ans)复杂度分析
- 时间复杂度:(RLE 只需线性扫描,统计各段长度)
- 空间复杂度:(存储各段;若不显式存储,仅用两个辅助数组也可做到 )
D - 183183
题目链接:https://atcoder.jp/contests/abc433/tasks/abc433_d
题目大意
对正整数 ,定义 为把 的十进制表示(不带前导零)依次拼接后得到的新整数,例如 。给定正整数 和长为 的正整数序列 ,求满足 且 是 的倍数的有序对 的个数。。
思路
问题转化:设 的十进制位数为 ,则拼接的本质是"把 放在 后面",即
于是 是 的倍数当且仅当
算法:由于 ,位数最多为 。把所有的 按位数 分组,每组存下每个 的值并排序。然后对每个 、每个位数 :
- 计算 ;
- 在第 组中用二分查找数出值等于 的元素个数,累加到答案。
这样,对于固定的 ,所有满足 且位数为 的 都被精确计数;遍历全部 后,以 为前项的合法对全部统计完毕。由于外层遍历所有 、内层按位数分组查找,每一对 恰好在其 的位数那一组被计数一次,不重不漏。注意 也是允许的,该计数方式天然涵盖。
实现细节:计算 时无需快速幂,令 cur = A_i % M,然后每轮循环 cur = cur * 10 % M 即可顺序得到 对应的值;目标余数取 key = (M - cur) % M(当 cur=0 时 key=0,(M-0)%M 在 C++ 中为 0,正确)。用 lower_bound/upper_bound 即可统计区间内等于 key 的个数。 与乘积均可能超过 int,全部使用 long long 更稳妥。
为什么正确:推导中的同余等价关系是充要条件;二分查找基于已排序的分组数组,统计的是"所有位数为 且模 余 的 的个数",与条件的表述完全一致。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
long long m;
cin >> n >> m;
vector<long long> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
vector<vector<long long>> g(11); // g[k]: 位数为 k 的数的模值
for (long long v : a)
g[to_string(v).size()].push_back(v % m);
for (auto &gg : g) sort(gg.begin(), gg.end());
long long ans = 0;
for (long long ai : a) {
long long cur = ai % m;
for (int k = 1; k <= 10; k++) {
cur = cur * 10 % m; // 此时 cur = A_i * 10^k mod M
long long key = (m - cur) % m; // 需要的 A_j mod M
ans += upper_bound(g[k].begin(), g[k].end(), key)
- lower_bound(g[k].begin(), g[k].end(), key);
}
}
cout << ans << '\n';
return 0;
}官方题解给出的 Python 实现:
import bisect
import sys
input = sys.stdin.readline
n, m = map(int, input().split())
a = list(map(int, input().split()))
g = [[] for _ in range(11)]
for v in a:
g[len(str(v))].append(v % m)
for gg in g:
gg.sort()
ans = 0
for ai in a:
for k in range(1, 11):
ai = (ai * 10) % m
key = (m - ai) % m
ans += bisect.bisect_left(g[k], key + 1) - bisect.bisect_left(g[k], key)
print(ans)复杂度分析
- 时间复杂度:,即 (常数约为 )
- 空间复杂度:
E - Max Matrix 2
题目链接:https://atcoder.jp/contests/abc433/tasks/abc433_e
题目大意
给定 、长为 的整数序列 和长为 的序列 。问是否存在一个 的整数矩阵 ,满足:
- 所有 都在 内,且两两不同(由于共有 个格子、恰好 个可选值,等价于 是 的一个排列);
- 第 行的最大值恰为 ;
- 第 列的最大值恰为 。
存在则输出 Yes 与任意一个合法矩阵,否则输出 No。共 组数据,。
思路
必要条件(重复即无解):若 中有重复值 ,说明有两行的最大值都是 ,而 在矩阵中只能出现一次、又必须出现在每一行内作为该行最大值,矛盾。 同理。因此先检查 各自内部是否有重复,若有直接输出 No。
贪心构造:从大到小依次放置值 。定义辅助信息:
- :满足 的行号(不存在则为 );
- :满足 的列号(不存在则为 );
- :满足 的所有格子 的集合。
一个格子 能放下值 的前提是 且 ,即 。所以我们把 的格子理解为"在轮到 时才开始变得可用"。
维护一个队列 ,存放"两边最大值都比当前值大、已经可用但还没被填"的格子。按 从大到小处理,分四种情况:
- 同时出现在 和 中:第 行的最大值、第 列的最大值都必须由 承担,唯一的办法是把 放在交叉格 上。该格子满足 ,属于 ,此时必然尚未被使用。放好后, 中其余格子全部入队。
- 只出现在 中(行 ): 必须放在第 行。由于 ,任何列的最大值都不是 ,所以合法放置点是第 行中满足 的格子——这些格子恰好就是 (因为 且 时 )。若 为空,说明第 行没有一列的最大值大于 ,无处安放 ,无解。否则从 中任取一个格子放 ,其余入队。
- 只出现在 中:与情况 2 对称。
- 在 中都没出现: 是"内部值",可以放在任意一个两边最大值都大于 的空格里,即从队列 中取一个格子。若 为空,说明所有 且 的格子都已被更大的值填满, 无处可放,无解。
为什么正确:
- 每个 中的格子在处理到 之前不会被触碰:因为一个格子唯一属于某个 (),它最早只在步骤 被加入队列。因此情况 1、2、3 中" 必须占据的格子"在需要时一定仍然空闲。
- 从队列中取格子不会破坏后续的行/列最大值: 中的格子 满足 ,即 且 。把较小的值 放进去不影响 这两个更大最大值的达成;而 (或 )真正需要安放时,所在的行(或列)中所有 的格子都属于 ,直到步骤 才被使用,届时仍然空闲。由于我们按从大到小填,候选空间单调不减,贪心不会"饿死"后续的必须放置。
- 失败的充分性:某一步找不到合法格,等价于"值大于 的个数超过了 "这个显然必要的不等式被违反,因此无解,输出
No正确。
实现注意:建立 需要 的枚举,所有 中的格子总数恰为 ,每个格子至多入队出队一次,因此每组的复杂度为 ,总复杂度 ,满足限制。判断重复时用大小为 的数组 即可。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int N, M;
cin >> N >> M;
long long nm = 1LL * N * M;
vector<long long> X(N), Y(M);
for (auto &v : X) cin >> v;
for (auto &v : Y) cin >> v;
vector<int> gx(nm + 1, -1), gy(nm + 1, -1);
bool dup = false;
for (int i = 0; i < N; i++) {
if (gx[X[i]] != -1) dup = true;
gx[X[i]] = i;
}
for (int j = 0; j < M; j++) {
if (gy[Y[j]] != -1) dup = true;
gy[Y[j]] = j;
}
if (dup) { cout << "No\n"; continue; }
vector<vector<pair<int, int>>> ok(nm + 1);
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
ok[min(X[i], Y[j])].push_back({i, j});
vector<vector<long long>> ans(N, vector<long long>(M, 0));
deque<pair<int, int>> q;
bool fail = false;
for (long long v = nm; v >= 1; v--) {
if (gx[v] == -1 && gy[v] == -1) { // 情况 4:内部值
if (q.empty()) { fail = true; break; }
auto [i, j] = q.front(); q.pop_front();
ans[i][j] = v;
for (auto &c : ok[v]) q.push_back(c);
} else if (gx[v] == -1 || gy[v] == -1) { // 情况 2/3:只在一侧
if (ok[v].empty()) { fail = true; break; }
auto [i, j] = ok[v].back(); ok[v].pop_back();
ans[i][j] = v;
for (auto &c : ok[v]) q.push_back(c);
} else { // 情况 1:两侧都是最大值
int i = gx[v], j = gy[v];
ans[i][j] = v;
for (auto &c : ok[v])
if (c.first != i || c.second != j) q.push_back(c);
}
}
if (fail) { cout << "No\n"; continue; }
cout << "Yes\n";
for (auto &row : ans) {
for (int j = 0; j < M; j++) {
if (j) cout << ' ';
cout << row[j];
}
cout << '\n';
}
}
return 0;
}官方题解给出的 Python 实现:
import sys
from collections import deque
input = sys.stdin.readline
def solve():
n, m = map(int, input().split())
nm = n * m
x = [v - 1 for v in map(int, input().split())]
y = [v - 1 for v in map(int, input().split())]
gx = [-1] * nm
gy = [-1] * nm
for i in range(n):
if gx[x[i]] != -1:
print("No")
return
gx[x[i]] = i
for j in range(m):
if gy[y[j]] != -1:
print("No")
return
gy[y[j]] = j
ok = [[] for _ in range(nm)]
for i in range(n):
for j in range(m):
ok[min(x[i], y[j])].append((i, j))
ans = [[-1] * m for _ in range(n)]
q = deque()
for v in range(nm - 1, -1, -1):
if gx[v] == -1 and gy[v] == -1:
if not q:
print("No")
return
i, j = q.popleft()
ans[i][j] = v + 1
for ii, jj in ok[v]:
q.append((ii, jj))
continue
if gx[v] == -1 or gy[v] == -1:
if not ok[v]:
print("No")
return
i, j = ok[v].pop()
ans[i][j] = v + 1
for ii, jj in ok[v]:
q.append((ii, jj))
continue
i, j = gx[v], gy[v]
ans[i][j] = v + 1
for ii, jj in ok[v]:
if i != ii or j != jj:
q.append((ii, jj))
print("Yes")
for r in ans:
print(*r)
for _ in range(int(input())):
solve()复杂度分析
- 时间复杂度: 每组数据,所有数据总和为
- 空间复杂度:
F - 1122 Subsequence 2
题目链接:https://atcoder.jp/contests/abc433/tasks/abc433_f
题目大意
与 C 题相同的 1122 串定义:非空、长度为偶数、前半段全为同一个数字 、后半段全为数字 。给定数字串 (),求 中有多少个(不必连续的)子序列是 1122 串,答案对 取模。从不同位置取出的相同子序列分别计数。
思路
固定"中间位置":子序列不一定连续,我们需要一个"锚点"。注意到一个 1122 串 的长度为 ,它的第 个字符(即第一半的最后一个字符)是前半段的收尾,设为 。于是 ,且第二半需要 个 。
定义量:对位置 ():
- : 中位于 左边、数字为 的字符个数;
- : 中位于 右边、数字为 的字符个数。
若要构成总长为 的 1122 子序列,第一半需要从左侧的 个 中选出 个(加上固定的 共 个),第二半需要从右侧的 个 中选出 个,因此固定 的贡献为
化简(范德蒙德恒等式):令 ,则
其中第二步用了 与范德蒙德恒等式 。因此固定 的贡献简化为
(也可以用生成函数看: 是 与 卷积的 次项系数,即 。)
答案:对每个满足 的位置 累加 再取模。
为什么正确:上述对应是一一对应的——任意 1122 子序列 的第一半最后一个字符落在某个唯一位置 ,于是 由"从 左侧选 个 + 位置 + 从右侧选 个 "唯一决定;反过来任何这样的选择都构成一个 1122 子序列。求和后每个子序列恰被计一次。
实现:预处理阶乘与阶乘逆元到 ,就能 求组合数。先统计每个数字在 中的总出现次数 total[d],再从左到右扫描,用 seen[d] 记录已扫过的数字计数,则扫描到 时 、。组合数需要 时返回 (例如 时 ,自然没有贡献)。
参考代码
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 998244353;
long long modpow(long long a, long long e) {
long long r = 1;
while (e) {
if (e & 1) r = r * a % MOD;
a = a * a % MOD;
e >>= 1;
}
return r;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
int n = s.size();
vector<long long> fact(n + 1), invf(n + 1);
fact[0] = 1;
for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % MOD;
invf[n] = modpow(fact[n], MOD - 2);
for (int i = n; i >= 1; i--) invf[i - 1] = invf[i] * i % MOD;
auto comb = [&](int nn, int kk) -> long long {
if (nn < 0 || kk < 0 || nn < kk) return 0;
return fact[nn] * invf[kk] % MOD * invf[nn - kk] % MOD;
};
vector<int> total(10, 0), seen(10, 0);
for (char c : s) total[c - '0']++;
long long ans = 0;
for (int i = 0; i < n; i++) {
int d = s[i] - '0';
if (d <= 8) { // d=9 时 d+1 不存在
int p = seen[d]; // i 左边 d 的个数
int q = total[d + 1] - seen[d + 1]; // i 右边 d+1 的个数
ans = (ans + comb(p + q, p + 1)) % MOD;
}
seen[d]++;
}
cout << ans << '\n';
return 0;
}官方题解给出的 Python 实现:
MOD = 998244353
INF = 2_000_000 + 2025
fact = [0] * (INF + 1)
finv = [0] * (INF + 1)
fact[0] = 1
for i in range(1, INF + 1):
fact[i] = fact[i - 1] * i % MOD
finv[INF] = pow(fact[INF], MOD - 2, MOD)
for i in range(INF, 0, -1):
finv[i - 1] = finv[i] * i % MOD
def comb(n, k):
if n < 0 or k < 0 or n < k:
return 0
return fact[n] * finv[k] % MOD * finv[n - k] % MOD
s = [ord(c) - ord('0') for c in input()]
n = len(s)
d = [[0] * (n + 1) for _ in range(10)]
for i in range(n):
for j in range(10):
d[j][i + 1] = d[j][i]
d[s[i]][i + 1] += 1
ans = 0
for i in range(n - 1):
if s[i] == 9:
continue
p = d[s[i]][i + 1] - 1
q = d[s[i] + 1][n] - d[s[i] + 1][i + 1]
ans = (ans + comb(p + q, p + 1)) % MOD
print(ans)复杂度分析
- 时间复杂度:
- 空间复杂度:
G - Substring Game
题目链接:https://atcoder.jp/contests/abc433/tasks/abc433_g
题目大意
给定由小写字母组成的字符串 。Alice 和 Bob 玩如下游戏:准备一个空串 ,Alice 先手,轮流操作。每次操作,当前玩家选择一个小写字母追加到 末尾,但要求追加后 仍然是 的子串。无法操作的玩家输。两人都最优时,判断 Alice 是否获胜。共 组数据,所有 的长度之和不超过 。
思路
转化为图上博弈:游戏状态就是当前的 ,一个合法的操作相当于把 扩展成 ,其中 仍是 的子串。这是典型的有向无环图(DAG)上的普通公平博弈:每个状态是一个节点,操作是沿着边走到后继;无后继的节点是必败态,存在一条通向必败态的边的节点是必胜态。
后缀自动机: 的子串数量可能是 的,无法全部枚举。但两个不同的子串 若在后缀自动机中处于同一状态,则它们具有相同的 集合,从而对任意字符 , 是子串当且仅当 是子串,且扩展后到达同一状态。因此游戏等价于在 的后缀自动机上从初始状态(代表空串)出发走一条路径:每步沿一条字符边前进,无法继续移动者输。
后缀自动机具有如下性质:
- 状态数不超过 ,边数不超过 ;
- 每条转移边 都满足 (实际上 ),因此转移图是无环的。
博弈 DP:设 win[v] 表示"轮到当前玩家、棋子位于状态 时,当前玩家是否能获胜"。则
即存在一条走向必败态 的边则必胜;没有出边或所有后继都必胜则必败。由于图是 DAG,可按 从大到小对状态排序后顺序递推(后继的 严格更大,所以先算完)。初始棋子位于空串对应的状态 ,若 为真则 Alice 胜,否则 Bob 胜。
为什么正确:普通公平博弈的必胜/必败判定标准(存在一步到必败态则必胜;无路可走或所有路都到必胜态则必败)是经典结论,这里状态集与转移集与后缀自动机一一对应,故完全等价。因为 DAG 无环,先手后手不会形成循环,博弈必在有限步内结束。
实现细节:用经典的 增量构造后缀自动机;DP 时按 降序排列状态。对每组数据总状态、总边数都是 ,所有数据求和后仍为 。若用 std::sort 排序状态,多一个 因子;可用计数排序按 排序以严格达到 (本题数据规模下直接排序也完全可行)。
参考代码
#include <bits/stdc++.h>
using namespace std;
struct SuffixAutomaton {
struct State {
int len, link;
array<int, 26> next;
State() : len(0), link(-1) { next.fill(-1); }
};
vector<State> st;
int last;
SuffixAutomaton(int n = 0) {
st.reserve(2 * n);
st.emplace_back();
last = 0;
}
void extend(int c) {
int cur = (int)st.size();
st.emplace_back();
st[cur].len = st[last].len + 1;
int p = last;
while (p != -1 && st[p].next[c] == -1) {
st[p].next[c] = cur;
p = st[p].link;
}
if (p == -1) {
st[cur].link = 0;
} else {
int q = st[p].next[c];
if (st[p].len + 1 == st[q].len) {
st[cur].link = q;
} else {
int clone = (int)st.size();
st.emplace_back();
st[clone].len = st[p].len + 1;
st[clone].next = st[q].next;
st[clone].link = st[q].link;
while (p != -1 && st[p].next[c] == q) {
st[p].next[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
string S;
cin >> S;
SuffixAutomaton sam((int)S.size());
for (char c : S) sam.extend(c - 'a');
int sz = (int)sam.st.size();
vector<int> order(sz);
iota(order.begin(), order.end(), 0);
// len 严格沿着转移边增大,故按 len 降序递推
sort(order.begin(), order.end(), [&](int a, int b) {
return sam.st[a].len > sam.st[b].len;
});
vector<char> win(sz, 0);
for (int v : order) {
bool w = false;
for (int c = 0; c < 26 && !w; c++) {
int q = sam.st[v].next[c];
if (q != -1 && !win[q]) w = true;
}
win[v] = w;
}
cout << (win[0] ? "Alice" : "Bob") << '\n';
}
return 0;
}复杂度分析
- 时间复杂度: 每组数据(后缀自动机构建 + 对状态数线性扫描转移;状态排序若用计数排序为 ,用
std::sort则为 ,本题规模下均可行);全部数据为 - 空间复杂度:(状态数 ,边数 )