固定半长把两半逐位比较折成一条相容数组 + 连续 q 个 1 即命中 + 位置压成 bitmask,移位与运算一次算出整排并倍增判连续 1
OJ: codeforces
题目 ID: 1948D
难度:提高
标签:字符串枚举位运算python
创建: 2026-11-04 16:49
更新: 2026-11-04 19:09
形式化题目
给定长度为 ?。每个 ? 可以独立替换成任意小写字母。求替换后
串联重复指长度
其中
把通配符吸收进
约束:
暴力解法
思路
最直接的做法枚举三样东西:半长
下面这份代码把第三层枚举消掉了,只保留
对固定
于是「存在起点
以
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| z | a | a | b | a | |
| b | a | a | b | z | |
| 0 | 1 | 1 | 1 | 0 |
这里有一处容易写错的地方:递推只和
代码
"""
很容易想到 n^3 的算法
采用思维转化:
问题: f[i] 表示第i个元素为结尾的时候, 定长为q, (i前面的每个元素j 都和j+q想等的)连续的最大长度是多少
f[i] = f[i - 1] + 1 if a[i] == a[i - q]
f[i] = 0 if a[i] != a[i - q]
然后发现一重要的数学性质:
if f[i] == q:
[i-q+1] [] [i] q=3
则 i-q+1 对应 i-q+1+q = i+1
则正好从i-q+1 -> i+q 是一个 Tandem Repeats
当定长为q的时候
f[i] =
"""
import sys
def check(s:str,q):
same = lambda a,b : a == b or b == '?' or a == '?'
# 转化成new array b
b = [ 1 if same(s[i], s[i+q]) else 0 for i in range(0,len(s)-q)]
# print(b)
c = [0] * len(b)
for i in range(len(b)):
if b[i] == 0:
c[i] = 0
else:
c[i] = c[i-1] + 1
if c[i] >= q:
return q
return 0
def solve(data):
for s in data:
# print(s,len(s)//2)
t = [check(s,q) for q in range(1,len(s)//2+1)]
# print(t)
ans = max(t,default=0)
print(ans*2)
if __name__ == "__main__":
n,*data = sys.stdin.buffer.read().decode().split()
solve(data)复杂度
外层
瓶颈
瓶颈不在复杂度阶数,而在每次基本操作的代价。这份实现有三处代价都落在 Python 解释器上:
- 每个
都新建一个长度为 的列表 b,列表推导里每一步是三次字符串比较加一次分支(a == b or b == '?' or a == '?')。 - 再新建一个同样长度的列表
c,用 Python 的for循环逐格写入,每格一次比较加一次赋值。 - 外层
从小到大枚举,必须把所有 都算完才能 max,找不到任何提前退出的机会。
总长 5000 时,前两条合计约
另外这份代码在 range(1, len(s)//2 + 1) 为空,max([]) 抛 ValueError。同理 c[i-1] 在 c[-1],只因列表初始化为全 0 才恰好正确。
正解
思路
算法骨架不动,仍然是「枚举
第一步:用 bitmask 表示位置集合。 对每个字符
第二步:一次算出整条
先说符号:?、或有一边是 ?、或两边是同一个字母,正是
右移会在高位留下越界的残留位,所以最后要截断到 m &= (1 << (n - q)) - 1。
第三步:用倍增判连续 1。 令
执行一轮后,第
第四步:
判据的正确性。 记条件为「
充分性(条件成立
说明 ,即两位相等,或至少一边是 ?。- 逐对处理:两位相等则不动;只有一边是
?就把?换成另一边的字母;两边都是?就都换成同一个字母。 - 每一对占据两个互不重叠的位置,替换之间不会冲突。
- 替换完成后两半逐位相同,子串成为长度
的串联重复。
必要性(目标成立
- 目标成立意味着存在一种替换使
对所有 成立。 - 替换只能改动
?,所以替换前这两位要么本来就相等,要么至少有一个是?。 - 这正是
,即 ; 取遍 就得到 全为 1。
两个方向合起来,判据与目标严格等价。
为什么不会漏掉答案。 若某段
代码
"""
题意: 问号可以替换成任意小写字母, 求最长的 Tandem Repeats (前半 == 后半) 的长度.
这是 main.py 的优化版 (原版 O(n^2) 的 Python 双层循环在 n=5000 时约 3~5s, 会 TLE).
沿用 main.py 里的数学结论:
对固定的半长 q, 令
b[i] = 1 if s[i] 与 s[i+q] 相容 (相等, 或有一边是 '?')
b[i] = 0 otherwise
则 [st, st+2q) 是 Tandem Repeats <=> b[st .. st+q-1] 全是 1
即: b 中必须存在一段长度 >= q 的连续 1.
(对应 main.py 里的 f 递推: f[i] = f[i-1] + 1 if b[i] else 0,
当 f[i] == q 时就找到了一个答案.
注意是拿 b[i] 判断, 不是拿 a[i] 和 a[i-q] 比较 —— +q 的比较已经在 b 里做完了.)
优化点: 把两处 Python 级循环换成大整数位运算
1. 每种字符出现的位置压成 bitmask, 则
b = (?mask | (?mask >> q)) | Σ (letter_mask & (letter_mask >> q))
一次位运算算出整排 b, 而不是 n 次字符比较.
2. "是否存在长度 q 的连续 1" 用倍增法 m &= m >> L (L = 1,2,4,...),
约 log q 次大整数移位, 而不是 q 次扫描.
3. q 从大到小枚举, 找到即退出, 并加 2q <= ans 剪枝.
复杂度: 约 O(n^2 / wordsize * log n), 实际几乎全在 C 层.
"""
import sys
def has_run(m, q):
"""m 的第 i 位为 1 表示 b[i]==1; 判断是否存在长度 >= q 的连续 1."""
L = 1
while L * 2 <= q:
m &= m >> L # 现在第 i 位为 1 <=> 原 m[i-L+1 .. i] 全 1 (长度 2L)
L *= 2
if not m:
return False
r = q - L # 还差 r 位, 再对齐一次
if r:
m &= m >> r
return m != 0
def check_bits(n, qmask, letters, q):
"""判断半长 q 下是否存在 Tandem Repeats."""
# 相容位置: 两边都是 '?' , 或者某一边是 '?' , 或者同字母
m = qmask | (qmask >> q)
for a in letters:
m |= a & (a >> q)
m &= (1 << (n - q)) - 1 # 只保留 0 .. n-q-1
return bool(m) and has_run(m, q)
def solve(data):
out = []
for s in data:
n = len(s)
if n < 2: # 长度 <2 不可能有 tandem repeat
out.append('0')
continue
# 每种字符出现在哪些位置: bit i 为 1 <=> s[i] == ch
pos = {}
for i, ch in enumerate(s):
pos[ch] = pos.get(ch, 0) | (1 << i)
qmask = pos.pop('?', 0) # '?' 的位置
letters = list(pos.values())
ans = 0
for q in range(n // 2, 0, -1): # 从大到小枚举, 找到就能提前退出
if 2 * q <= ans:
break
if check_bits(n, qmask, letters, q):
ans = 2 * q
break
out.append(str(ans))
sys.stdout.write('\n'.join(out) + '\n')
if __name__ == "__main__":
_t, *data = sys.stdin.buffer.read().decode().split()
solve(data)复杂度
位运算版本的时间是
本地实测(
| 数据 | 暴力解法 | 正解 | 加速比 |
|---|---|---|---|
| 随机 26 字母 | 2.6 s | 0.07 s | 37× |
随机 26 字母 + ? |
2.8 s | 0.07 s | 41× |
随机 ab |
2.9 s | 0.03 s | 85× |
abab… |
2.5 s | 0.02 s | 115× |
aaaa… |
2.1 s | 0.03 s | 84× |
????… |
2.2 s | 0.03 s | 83× |
同一算法的骨架,只把内层从解释器搬到大整数运算,就有 30 倍以上的差距;main.py 单次最短也有 2.0 秒,仍在限制之外。在 Python 里评估「能不能过」时,
总结
- 关键转化是固定半长
,把「两半逐位比较」折成一条相容数组 ,原问题变成「 中是否存在长度 的连续 1」。这一步把 降到 。 - 递推只用
的前一位,不要重复拿 和 比较; 的比较已经在 里完成。 - 判据是充要条件,充分性靠「逐对替换互不冲突」,必要性靠「替换只影响
?」,两个方向都成立才允许把答案交给它。 main-optimize.py的优化全部落在实现层,共四处:位置压成 bitmask 让整条一次算出、倍增法 把连续 1 判定压到 次移位、 从大到小枚举并命中即停、 剪枝。阶数没变,常数降了两个数量级。 - 边界:
时答案为 0。 main.py在此处会因max([])崩溃,main-optimize.py已显式处理。 - 验证记录:以独立的
枚举为基准, main-optimize.py通过 600 组随机数据对拍(字母表含ab、abc?、a?、?、abcdefghij等,长度 1~18),main.py与main-optimize.py在 600 组长度 2~18 的数据上输出一致,另有多测试点 1200 个小用例全部正确。满规模计时见上表。未使用官方完整测试数据,也未在 PyPy 下计时。