T31069: 禁用词的字符串构造
二分查找和预处理, prefix sum, http://cs101.openjudge.cn/practice/31069/
给定只包含小写字母的字符串 A, B ,以及 k 个禁止出现的长度为 2 的字符串(称为禁用组合)。
构造字符串 C 的方式如下:
- C 起始是空串;
- 按顺序枚举 A 的每个字符 Ai,依次与 B 的每个字符 Bj 组合成串 AiBj;
- 若 AiBj 不在禁用组合中,则将该串按加入到 C 的末尾。
给定 q 个询问,每个询问给出 ti,求 C 的第 ti 个字符,保证所有询问都不会越界。
输入
第一行包含四个整数 LA, LB, k, q, 其中 LA, LB 分别为 A, B 的长度.. 第二行包含 LA 个小写字符, 表示字符串 A. 第三行包含 LB 个小写字符, 表示字符串 B. 接下来 k 行每行有两个小写字符, 用一个空格分开, 表示 k 组禁用组合. 接下来 q 行每行一个正整数, 第 i 个表示第 i 次询问对应的 ti. 注意字符串的下标从 0 开始. 记 C 的长度为 LC, 保证 0 <= ti < LC.
输出
共 q 行, 每行一个字符, 第 i 行的表示第 i 次询问的答案.
样例输入
4 2 2 10
cbaa
ac
b c
a a
0
1
2
3
4
5
6
7
8
9样例输出
c
a
c
c
b
a
a
c
a
c提示
对所有数据, LA, LB <= 200000, k <= 26^2, q <= 100000 . 对 60% 的数据: LA, LB <= 1000. 对另外 20% 的数据: q <= 10.
这是一个基于二分查找和预处理的算法设计。由于数据范围较大(
解题思路
观察字符串
的构成: - 字符
与 组合:如果 不是禁用组合,则将 和 顺序放入 。 - 对于固定的
,它与所有合法的 拼接。由于 的合法性仅取决于字符 本身(只有 26 种可能),我们可以为 的每个字符计算出它在 中贡献的子串长度。 - 令
为与字符 组合不被禁用的 中字符的数量。那么每个 在 中贡献的子串长度为 。
- 字符
预处理:
- 禁用组合:使用一个
的布尔型二维数组 forbidden[u][v]记录字符对是否被禁用。 - 过滤后的
串:对于字符 'a' 到 'z' 的每一个字符 (共 26 种可能),我们预先过滤出 串中与其组合合法的字符序列,记作 B_filtered[ch]。其长度即为。 - 前缀和定位:计算每个
生成子串长度的前缀和数组 。 表示由 生成的子串总长度。
- 禁用组合:使用一个
回答询问:
- 对于每一次询问
: - 利用二分查找在
中找到最大的 ,使得 。这说明第 个字符落在了由 产生的块中。 - 计算块内相对偏移量
。 - 如果
是偶数,说明对应的字符是 。 - 如果
是奇数,说明对应的字符是 中的字符。具体为 B_filtered[A_i][rem // 2]。
- 利用二分查找在
- 对于每一次询问
Python 代码实现
python
import bisect
import sys
def solve():
# 快速读取所有输入
input_data = sys.stdin.read().split()
if not input_data:
return
LA = int(input_data[0])
LB = int(input_data[1])
k = int(input_data[2])
q = int(input_data[3])
A = input_data[4]
B = input_data[5]
# 解析禁用组合
forbidden = [[False] * 26 for _ in range(26)]
idx = 6
for _ in range(k):
u = ord(input_data[idx]) - 97
v = ord(input_data[idx + 1]) - 97
forbidden[u][v] = True
idx += 2
# 解析询问
queries = []
for _ in range(q):
queries.append(int(input_data[idx]))
idx += 1
# 预处理:对 26 个可能的 A 字符,过滤 B 中与之相配的字符序列
B_filtered = ["" for _ in range(26)]
for a_idx in range(26):
# 找出对于字符 chr(97 + a_idx) 而言,不被禁用的 B 字符集合
valid_set = {
chr(97 + b_idx)
for b_idx in range(26)
if not forbidden[a_idx][b_idx]
}
# 过滤 B
B_filtered[a_idx] = "".join([b for b in B if b in valid_set])
# 预处理:计算前缀和 P
P = [0] * (LA + 1)
A_ord = [ord(char) - 97 for char in A]
B_lengths = [len(B_filtered[i]) for i in range(26)]
for i in range(LA):
P[i + 1] = P[i] + 2 * B_lengths[A_ord[i]]
# 回答询问
out = []
for t in queries:
# 寻找 t 落在 A 的哪个字符块内
idx_A = bisect.bisect_right(P, t) - 1
rem = t - P[idx_A]
if rem % 2 == 0:
out.append(A[idx_A])
else:
out.append(B_filtered[A_ord[idx_A]][rem // 2])
# 打印输出
print("\n".join(out))
if __name__ == "__main__":
solve()复杂度分析
时间复杂度:
- 预处理:过滤
串需要运行 26 次,每次遍历长度为 的字符串。这部分耗时 。计算前缀和 耗时 。预处理总时间复杂度为 。 - 查询:每次查询使用一次二分查找,复杂度为
。 次查询总耗时为 。 - 对于最大数据规模(
),整体运行大约需要 秒,完全能够满足时间限制。
- 预处理:过滤
空间复杂度:
- 存储
B_filtered的 26 个过滤后字符串的总长度不超过。 - 前缀和数组
长度为 。 - 整体空间复杂度为
,在最大数据下大约占用数十MB内存,符合常规空间限制。
- 存储