跳转至

第 4 章 字符串

配套例题:PIO10 单组_字符串、PIO11 多组_字符串_T组形式、PIO12 单组_二维字符数组、PIO13 多组_带空格的字符串_T组形式、BISHI10 小红的字符串修改 来源:菜鸟教程 Python3 字符串

字符串是笔试里出现频率最高的数据类型——输入是字符串、输出是字符串、一半的题面本身就在讲字符串。

从 C++ 转过来要先记住一条:Python 的 str 是不可变的。 它不是 std::string(可以 s[0] = 'x'),而更像 Java 的 String。 这一条决定了本章后半段几乎所有的性能讨论。


4.1 字面量与转义

四种引号

s1 = 'single'
s2 = "double"                   # 单双引号完全等价,没有 char/string 之分
s3 = '''三引号
可以跨行'''
s4 = """三引号
也可以用双引号"""

Python 没有字符类型'a' 就是一个长度为 1 的字符串,s[0] 拿到的也是长度为 1 的字符串。 这跟 C++ 的 char 是完全不同的东西——见 4.7 的 ord/chr

选单引号还是双引号的唯一标准是哪个能少写转义

print("it's ok")                # 不用转义
print('he said "hi"')           # 不用转义

转义字符表

转义 含义 竞赛中的用处
\n 换行 "\n".join(...) 拼接输出
\t 制表符 少数题目要求 Tab 分隔
\\ 反斜杠本身 路径、正则
\' \" 引号 引号冲突时
\r 回车 Windows 换行残留,见下面的坑
\0 空字符 几乎用不到
\xhh 十六进制字符 "\x41" 就是 "A"
\ooo 八进制字符 "\101" 就是 "A"
行尾 \ 续行 01-语法与执行模型

\r 是 OJ 上最阴的 WA 来源。如果测试数据是 Windows 格式(\r\n), input() 只去掉 \n,留下的 \r 会跟着答案一起输出,肉眼看不见但评测机看得见。 读进来的字符串一律 .strip().rstrip(),代价可以忽略。

原始字符串 r"..."

前缀 r 让反斜杠失去转义能力:

print(r"\n")                    # 输出两个字符:反斜杠和 n
print("\n")                     # 输出一个换行

竞赛里基本只在写正则时用得上(re.compile(r"\d+"))。 注意 r"..." 不能以奇数个反斜杠结尾r"\" 是语法错误。

字符串拼接的字面量特性

相邻的字符串字面量会在编译期自动拼接,这是写长提示串的惯用法:

msg = ("这是一段很长的话,"
       "写成两行更好读,"
       "运行时它们是一个字符串")

注意这只对字面量成立,变量之间必须用 +


4.2 索引与切片

索引与 C++ 一样从 0 开始,但 Python 额外支持负索引(从末尾数,-1 是最后一个):

 s =   P   y   t   h   o   n
正索引 0   1   2   3   4   5
负索引-6  -5  -4  -3  -2  -1
s = "Python"
s[0]        # 'P'
s[-1]       # 'n'      ← 取最后一个字符,不用写 s[len(s)-1]
s[6]        # IndexError: string index out of range

切片 s[start:stop:step]

切片是 Python 最重要的语法之一,永远左闭右开,且越界不报错

s = "abcdefg"
s[1:4]      # 'bcd'     [1, 4)
s[:3]       # 'abc'     省略 start = 0
s[3:]       # 'defg'    省略 stop = len(s)
s[:]        # 'abcdefg' 完整副本
s[-3:]      # 'efg'     最后三个
s[:-3]      # 'abcd'    去掉最后三个
s[::2]      # 'aceg'    步长 2
s[::-1]     # 'gfedcba' ← 反转字符串的标准写法
s[100:200]  # ''        越界不报错,返回空串
需求 写法
反转 s[::-1]
去掉首尾各一个字符 s[1:-1]
\(k\) s[:k]
\(k\) s[-k:](注意 \(k=0\) 时会返回整串!)
每隔一个取 s[::2]
判断回文 s == s[::-1]

s[-k:]k == 0 时是 s[0:],也就是整个串,不是空串。 需要「取最后 k 个,k 可能为 0」时写 s[len(s) - k:]

切片的复杂度是 \(O(\text{切片长度})\),因为它复制。 在循环里反复 s[i:j] 是隐蔽的 \(O(n^2)\)

# ❌ 每次切片都复制,总复杂度 O(n²)
for i in range(n):
    if s[i:i + m] == t:
        ...

# ✅ 用 find / 或者哈希,见第 36、71 章
pos = s.find(t)

4.3 字符串是不可变的

s = "abc"
s[0] = "x"          # ❌ TypeError: 'str' object does not support item assignment

要「修改」只能造新串:

s = "x" + s[1:]                     # 换第一个字符
a = list(s); a[0] = "x"; s = "".join(a)   # 需要多处修改时先转 list

需要频繁按下标改字符时,先 a = list(s) 变成字符列表,改完再 "".join(a)。 这是竞赛里处理字符串修改的标准套路:list 可变、\(O(1)\) 随机写,join 一次 \(O(n)\) 收尾。

循环拼接是 \(O(n^2)\) 陷阱

这是 Python 算法题的第二大 TLE 来源(第一是 in list,见 03-运算符与位运算):

# ❌ 每次 += 都要新建一个字符串并复制全部已有内容
res = ""
for x in a:
    res += str(x) + " "

\(i\) 次拼接复制 \(i\) 个字符,总代价 \(1 + 2 + \dots + n = O(n^2)\)\(n = 10^5\) 时是 \(5 \times 10^9\) 次字符复制,必然 TLE。

正确写法是先收集到列表,最后 join 一次

# ✅ O(总长度)
parts = []
for x in a:
    parts.append(str(x))
res = " ".join(parts)

# ✅ 更短
res = " ".join(map(str, a))

join 会先遍历一遍算出总长度,一次性分配内存,然后逐段拷贝,总复杂度 \(O(\sum |s_i|)\)

有人会说「CPython 对 s += t 做了原地扩容优化,实测不慢」——这是真的, 但只在 s 的引用计数为 1 时生效。一旦把中间结果存进列表、传给函数、或者换成 PyPy, 优化立刻失效,退化回 \(O(n^2)\)。这种「看运气」的性能不能依赖。

记住结论:任何时候拼接字符串都用 join,没有例外。

同理,输出大量行时不要在循环里 print

sys.stdout.write("\n".join(out) + "\n")     # 见 20 章

4.4 字符串方法全表

Python 的 str 有 40 多个方法,全部返回新串,绝不修改原串。 按用途分成六类,竞赛高频的用 ★ 标出。

查找与定位

方法 作用 找不到时
s.find(t[, b, e]) 返回 t 首次出现的下标 返回 -1
s.rfind(t) 从右找 返回 -1
s.index(t) find ValueError
s.rindex(t) rfind ValueError
s.count(t) t 出现次数(不重叠 返回 0
t in s 是否包含 返回 False
s.startswith(t) 是否以 t 开头,参数可以是元组
s.endswith(t) 是否以 t 结尾,参数可以是元组
s = "abcabc"
s.find("bc")            # 1
s.rfind("bc")           # 4
s.find("bc", 2)         # 4      从下标 2 开始找
s.count("abc")          # 2
"aaaa".count("aa")      # 2      ← 不重叠!不是 3
s.startswith(("ab", "xy"))      # True   参数可以是元组,等于「或」

count 统计的是不重叠出现次数。求重叠出现次数要自己写循环或用 KMP, 见 71-字符串匹配KMP

判定(全是 is*,返回 bool)

方法 含义 空串时
s.isdigit() 全是数字字符(含 ² 这类上标) False
s.isdecimal() 全是十进制数字(最严格) False
s.isnumeric() 全是数值字符(含 ½、汉字「一」) False
s.isalpha() 全是字母(含汉字) False
s.isalnum() 全是字母或数字 False
s.islower() / s.isupper() 全小写 / 全大写 False
s.isspace() 全是空白 False
s.istitle() 是否每个单词首字母大写 False
s.isascii() 全是 ASCII(3.7+) True
"²".isdigit()       # True   ← 上标 2 也算 digit
"²".isdecimal()     # False  ← 但不是 decimal
"½".isnumeric()     # True
"一".isnumeric()    # True   ← 汉字数字
"-5".isdigit()      # False  ← 负号不是数字!

"-5".isdigit()False。想判断「这个 token 是不是合法整数」, 老老实实 try: int(s) except ValueError:,不要用 isdigit。 另外要严格判十进制阿拉伯数字请用 isdecimalisdigit 会放过全角/上标字符。

大小写转换

方法 作用
s.lower() / s.upper() 全转小写 / 大写
s.capitalize() 首字母大写,其余小写
s.title() 每个单词首字母大写
s.swapcase() 大小写互换
s.casefold() lower() 更激进的折叠(处理德语 ß 等)
"hello world".title()       # 'Hello World'
"hello".capitalize()        # 'Hello'
"Hello".swapcase()          # 'hELLO'

大小写不敏感的比较统一写 a.lower() == b.lower()

修剪与填充

方法 作用
s.strip([chars]) 去掉首尾指定字符集(默认空白)
s.lstrip() / s.rstrip() 只去左 / 只去右
s.zfill(w) 左补 0 到宽度 w正确处理负号
s.ljust(w[, c]) / s.rjust(w[, c]) 左 / 右对齐填充
s.center(w[, c]) 居中填充
s.expandtabs(n) Tab 展开成空格
s.removeprefix(p) / s.removesuffix(p) 去掉前缀 / 后缀(3.9+
"  hi  ".strip()            # 'hi'
"xxhixx".strip("x")         # 'hi'     ← 参数是字符集合,不是子串
"ab".zfill(5)               # '000ab'
"-12".zfill(6)              # '-00012' ← 负号留在最前面
"abc".center(9, "*")        # '***abc***'

strip("abc") 去掉的是「首尾所有属于 {a,b,c} 的字符」,不是去掉子串 "abc"。 想去前缀用 removeprefix(3.9+)或切片。这是极高频的误用。

分割与拼接

方法 作用
s.split() 任意空白切分,自动忽略连续空白和首尾空白
s.split(sep[, maxsplit]) sep 切分,连续 sep 会产生空串
s.rsplit(sep, k) 从右切,最多切 k
s.splitlines() 按行切分(认 \n \r \r\n
s.partition(sep) 切成三元组 (前, sep, 后),只切第一次
s.rpartition(sep) 同上,从右切
sep.join(iterable) sep 连接可迭代对象里的字符串

split()split(' ') 的差异是竞赛输入解析的高频坑

"a b  c".split()            # ['a', 'b', 'c']          ← 连续空格合并
"a b  c".split(" ")         # ['a', 'b', '', 'c']      ← 多出一个空串!
"a,b,,c".split(",")         # ['a', 'b', '', 'c']

读输入永远用无参数的 split(),它同时处理空格、Tab、换行,且天然忽略首尾空白。

"-".join(["a", "b", "c"])   # 'a-b-c'
"".join(map(str, [1, 2, 3]))    # '123'
"".join([1, 2])             # ❌ TypeError: 元素必须是 str,先 map(str, ...)

替换与映射

方法 作用
s.replace(old, new[, cnt]) 替换,cnt 限制次数
str.maketrans(a, b[, del]) 造字符映射表
s.translate(table) 按映射表批量替换单字符
"abcabc".replace("a", "X")          # 'XbcXbc'
"abcabc".replace("a", "X", 1)       # 'Xbcabc'
"a b c".replace(" ", "")            # 'abc'   ← 删空格的标准写法

tab = str.maketrans("abc", "xyz")
"aabbcc".translate(tab)             # 'xxyyzz'

一次替换多种字符时用 translate,不要链式 replaces.replace('a','x').replace('b','y').replace('c','z') 要扫 3 遍串, translate 只扫 1 遍,且是 C 实现。凯撒密码、字符映射类题目直接用它。

复杂度速查

操作 复杂度 备注
len(s) \(O(1)\) 长度是存好的
s[i] \(O(1)\)
s[i:j] \(O(j-i)\) 复制
s + t \(O(\|s\|+\|t\|)\) 循环里用就是 \(O(n^2)\)
s * k \(O(k\|s\|)\)
t in ss.find(t) 最坏 \(O(\|s\|\|t\|)\),实测接近线性 CPython 用了 two-way 算法
s.count(t) 同上
s.split() \(O(\|s\|)\)
sep.join(a) \(O(\sum \|a_i\|)\)
s[::-1] \(O(\|s\|)\) "".join(reversed(s))
s.replace / translate \(O(\|s\|)\)
s == t \(O(\min(\|s\|,\|t\|))\) 长度不等直接 False\(O(1)\)
hash(s) \(O(\|s\|)\) 首次,之后 \(O(1)\) 结果会缓存在字符串对象里
ord(c) / chr(i) \(O(1)\)

4.5 三种格式化

Python 有三套字符串格式化语法,都还活着,都要认识。

写法 语法 何时用
% 旧式 "%d" % x 只输出一个浮点数时最短
str.format "{}".format(x) 需要重复使用同一参数时
f-string f"{x}" 默认选择,最快最短(3.6+)

一、% 格式化

"%d" % 42                       # '42'
"我叫 %s 今年 %d 岁" % ("小明", 10)     # 多个参数必须用元组
"%.3f" % 3.14159                # '3.142'
"%5.2f|%-5d|%+d" % (3.14159, 42, 7)     # ' 3.14|42   |+7'
"%o %x %X %e" % (64, 255, 255, 12345.7) # '100 ff FF 1.234570e+04'
"%%"                            # 输出一个百分号
占位符 含义
%d %i 整数
%s 任意对象(调用 str()
%f 浮点,默认 6 位小数
%e %E 科学计数法
%g %G 自动在 %f%e 之间选
%o %x %X 八 / 十六进制
%c 按 ASCII 码或单字符

辅助指令:%[标志][宽度][.精度]类型,标志有 -(左对齐)、+(显示正号)、0(补零)、#(加 0x/0o 前缀)。

"%d" % xx 如果是元组会被当成多个参数, 想输出一个元组必须写 "%s" % (t,)。这是 % 格式化最容易踩的坑,也是 f-string 胜出的原因之一。

二、str.format

"{} {}".format("a", "b")            # 'a b'          按顺序
"{0} {1} {0}".format("a", "b")      # 'a b a'        按下标,可重复
"{x} {y}".format(x=1, y=2)          # '1 2'          按名字
"{:>8}".format("hi")                # '      hi'     右对齐
"{:.3f}".format(1 / 3)              # '0.333'
"{:08.3f}".format(1 / 3)            # '0000.333'

三、f-string(推荐)

f-string 在编译期就展开成字符串拼接指令,运行时不需要解析格式串, 所以它是三者中最快的。花括号里可以放任意表达式:

n, x = 255, 1 / 3
f"{n}"                  # '255'
f"{n + 1}"              # '256'         ← 里面可以写表达式
f"{x:.3f}"              # '0.333'
f"{n:08b}"              # '11111111'    ← 二进制补零到 8 位
f"{n:#x}"               # '0xff'
f"{n:,}"                # '255'         千位分隔符
f"{n:^9}"               # '   255   '   居中
f"{'YES' if n else 'NO'}"               # 三元也能塞
f"{x=}"                 # 'x=0.3333333333333333'   ← 3.8+,调试神器

格式说明符的完整形式:

{值:[[填充]对齐][符号][#][0][宽度][,][.精度][类型]}
部分 取值 例子
对齐 <>^= 符号后填充 f"{s:*^10}"
符号 + 总显示 - 只负数 正数留空格 f"{n:+d}"
宽度 整数 f"{n:5d}"
精度 .k f"{x:.6f}"
类型 d b o x X f e g % s f"{n:b}"

竞赛里最常用的三个:

f"{x:.6f}"          # 保留 6 位小数(spj 浮点题)
f"{n:09d}"          # 补前导零
f"{n:b}"            # 转二进制串,不带 0b 前缀

f-string 内部不能出现反斜杠(3.12 之前)。 想在 f-string 里放换行要先定义 NL = "\n" 再写 f"{NL}",或者干脆别这么写。


4.6 三引号与多行字符串

para = """第一行
第二行
第三行"""

三引号保留其中的所有换行和空格,所见即所得。用途:

  1. 多行注释(本质是一个没被赋值的字符串表达式,见 01-语法与执行模型)。
  2. docstring——本教程的题解都用它写「这题考什么」。
  3. 本地造测试数据:
DATA = """3
1 2
3 4
5 6
"""
for line in DATA.strip().split("\n"):
    print(sum(map(int, line.split())))

三引号字符串的第一行如果紧跟引号换行,会多出一个开头的 \n"""\nabc""" 的第一个字符是换行。要么用 """\ 续行,要么 .strip()


4.7 字符与 ASCII:ordchr

Python 没有 char,字符和整数之间的桥梁是这两个内置函数:

ord("A")        # 65      字符 → 码点
chr(65)         # 'A'     码点 → 字符
ord("a")        # 97
ord("0")        # 48

竞赛里的标准用法——把字母映射成 0..25 的下标

idx = ord(c) - ord("a")         # 'a'→0, 'b'→1, ..., 'z'→25
idx = ord(c) - 97               # 同上,少一次函数调用,更快
c = chr(idx + 97)               # 反向

cnt = [0] * 26                  # 26 个字母的桶
for c in s:
    cnt[ord(c) - 97] += 1

记住这几个常数就不用查表了:

字符 码点
'0' 48
'A' 65
'a' 97
'a' - 'A' 32

因为大小写只差第 5 位(\(32 = 2^5\)),有个位运算小技巧:

c = chr(ord(c) ^ 32)            # 大小写互换(仅对英文字母有效)

批量转换用 map

codes = list(map(ord, s))               # 字符串 → 码点列表
s = "".join(map(chr, codes))            # 码点列表 → 字符串
vals = [ord(c) - 97 for c in s]         # 字母 → 0..25

更快的写法:直接用 bytes s.encode() 得到 bytes,而 bytes 迭代出来的就是整数

for b in s.encode():        # b 已经是 int,省掉 ord
    cnt[b - 97] += 1
sys.stdin.buffer 读进来的本身就是 bytes,连 encode 都省了。见 20-输入输出处理

字符串之间的大小比较就是按码点逐位比较(字典序):

"abc" < "abd"       # True
"Z" < "a"           # True    大写字母码点更小
"abc" < "abcd"      # True    前缀更短的更小

4.8 bytes 与编码

Python 3 里 strUnicode 码点序列bytes字节序列,两者不能混用:

s = "你好"
b = s.encode("utf-8")           # str → bytes: b'\xe4\xbd\xa0\xe5\xa5\xbd'
s2 = b.decode("utf-8")          # bytes → str: '你好'

len(s)                          # 2   ← 2 个字符
len(b)                          # 6   ← 6 个字节(UTF-8 里一个汉字 3 字节)
类型 字面量 元素类型 可变?
str "abc" 长度 1 的 str
bytes b"abc" int(0–255)
bytearray bytearray(b"abc") int
b = b"abc"
b[0]                # 97    ← 是整数,不是 b'a'!
b[0:1]              # b'a'  ← 切片才是 bytes
list(b)             # [97, 98, 99]

ba = bytearray(b"abc")
ba[0] = 120                     # 可以原地改
bytes(ba)                       # b'xbc'

竞赛里用到 bytes 的场景只有两个:

  1. sys.stdin.buffer.read() 返回 bytesint(b"123") 合法, 但要当字符串输出必须先 .decode()
data = sys.stdin.buffer.read().split()
n = int(data[0])                # ✅ 数字不用解码
s = data[1].decode()            # ✅ 字符串必须解码
print(data[1])                  # ❌ 输出 b'abc',带引号,直接 WA
  1. bytearray 当高速字节数组用。它比 list 省内存(1 字节 vs 8 字节指针), 且 find/count/rfind 都是 C 级别扫描,适合做超大的 0/1 标记数组:
vis = bytearray(n)              # n 个 0,比 [0] * n 省 8 倍内存
vis[i] = 1

Python 3 里所有字符串都是 Unicode,没有 Python 2 的 u"..." 前缀之说 (虽然为了兼容仍然允许写 u"abc")。 len(s) 数的是码点,不是字节;这跟 C++ 的 std::string::size() 完全不同。 好在竞赛数据几乎全是 ASCII,两者等价。


4.9 例题

PIO10 单组_字符串(入门)

给定长度 \(n \le 10^5\) 的小写字符串 \(s\),将其倒置输出。 题面见 PIO10 原题(牛客)

input()                             # 读掉 n,Python 用不上
print(input().strip()[::-1])

三个语言点:

  • s[::-1]反转的唯一推荐写法。它在 C 层做一次反向内存拷贝, 比 "".join(reversed(s)) 快 2–3 倍,比 for 循环快一个数量级。
  • strip() 不能省,理由见 4.1 的 \r 陷阱。
  • n 这一行必须读掉,否则第二个 input() 拿到的还是 n

PIO11 多组_字符串_T组形式(入门)

\(t \le 10^5\) 组,每组给 \(n\) 和串 \(s\)\(\sum n \le 10^5\)

数据量上来了,改用 token 流。因为串内保证无空格,一个字符串就是一个 token

import sys


def main():
    data = sys.stdin.buffer.read().split()
    t = int(data[0])
    out = []
    p = 1
    for _ in range(t):
        p += 1                      # 跳过 n
        out.append(data[p].decode()[::-1])
        p += 1
    sys.stdout.write("\n".join(out) + "\n")


main()

data[p]bytes.decode() 之后才是 str。 其实 bytes 也支持 [::-1](反转字节),对纯 ASCII 结果一样, 但 "\n".join(...) 要求元素是 str,所以解码不能省。

也可以反过来:全程不解码,最后用 b"\n".join(out) 写进 sys.stdout.buffer。 这样更快,但可读性差,数据量到 \(10^6\) 再考虑。

PIO12 单组_二维字符数组(入门)

\(n \times m\) 的字符矩阵(\(n, m \le 10^3\)),行和列都倒置后输出。

「行列都倒置」拆成两步,两步都是切片:

  • 每一行内部反转 → row[::-1]
  • 行的顺序反转 → reversed(rows)
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    # 每行不含空格,所以一行就是一个 token;data[0]、data[1] 是 n 和 m,
    # 矩阵第 i 行因此落在 data[2 + i],这个 2 就是跳过 n、m 的偏移量
    rows = [data[2 + i].decode() for i in range(n)]
    # r[::-1] 步长为 -1,从尾扫到头,即行内字符反转;
    # reversed(rows) 再把行的先后顺序翻过来,两者合起来就是「行列都倒置」
    sys.stdout.write("\n".join(r[::-1] for r in reversed(rows)) + "\n")


main()

reversed() 返回迭代器,不复制列表;配合生成器表达式喂给 join,全程不产生多余的中间列表。 注意 m 读进来也用不上——每行本身就是一个完整 token。

PIO13 多组_带空格的字符串_T组形式(入门)

\(t\) 组,每组给一个含空格的串,去掉空格后倒置输出。保证首尾不是空格。

这题是 token 流的反例split() 会把 "one space" 切成两个 token, 行边界信息丢失后无法还原,必须按行读:

import sys


def main():
    inp = sys.stdin
    t = int(inp.readline())
    out = []
    for _ in range(t):
        inp.readline()                          # 跳过 n
        s = inp.readline().rstrip("\n")
        out.append(s.replace(" ", "")[::-1])
    sys.stdout.write("\n".join(out) + "\n")


main()
  • 删空格用 s.replace(" ", "")\(O(n)\) 一遍扫描。 不要用 "".join(s.split())——那会把连续空格合并成一个再删,结果虽然一样但绕了一圈。
  • rstrip("\n") 只去换行,不能用 strip():题目保证首尾无空格, 但一旦哪天不保证,strip() 会连有意义的空格一起吃掉。
  • sys.stdin.readline 而不是 input(),快 5–10 倍。

判断口诀:串内可能含空格 → 按行读;确定无空格 → token 流。 完整讨论见 20-输入输出处理

BISHI10 小红的字符串修改(简单,字符串 / 模拟)

给定小写串 \(s\)\(t\)\(|s| \le |t| \le 10^3\))。每次操作可把某个字母换成字母表中相邻的字母 (a 可换成 bz,即字母表首尾相连)。求最少操作次数,使 \(s\) 成为 \(t\) 的子串 (这里的「子串」= 连续子串)。 题面见 BISHI10 原题(牛客)

拆成两层。

第一层,单个字符的代价。把 az 编号 \(0\)\(25\),字母表首尾相连构成一个环, 从 \(a\) 走到 \(b\) 的最少步数就是环上距离:

\[\text{cost}(a, b) = \min(|a-b|,\ 26 - |a-b|)\]

第二层,s 长度固定,所以它只能对齐到 t 的某个起点 \(i \in [0, m-n]\), 枚举起点、逐位累加即可:

\[\text{ans} = \min_{0 \le i \le m-n} \sum_{j=0}^{n-1} \text{cost}(s_j,\ t_{i+j})\]
import sys


def main():
    data = sys.stdin.buffer.read().split()
    s = data[0].decode()
    t = data[1].decode()
    n, m = len(s), len(t)

    # 预处理 26x26 代价表:字母表是环形的,a 和 z 相邻
    cost = [[min(abs(a - b), 26 - abs(a - b)) for b in range(26)]
            for a in range(26)]

    # 转成 0..25 的下标数组,循环里就不用反复调 ord
    sa = [ord(c) - 97 for c in s]
    ta = [ord(c) - 97 for c in t]

    print(min(
        sum(cost[sa[j]][ta[i + j]] for j in range(n))
        for i in range(m - n + 1)
    ))


main()

复杂度\(O(n(m-n+1))\)。乍看是 \(O(nm) = 10^6\),但 \(n(m-n+1)\)\(n = m/2\) 时取到最大值 \(\approx m^2/4 = 2.5 \times 10^5\),实测 0.06 秒,完全不用担心。

四个本章知识点在这里全用上了:

用到的点 位置
ord(c) - 97 把字母映射成下标 sata
字符串按下标随机访问是 \(O(1)\) ta[i + j]
预处理代价表避免循环内重复计算 cost
生成器表达式喂给 min/sum,全程不建中间列表 最后三行

常见错误:忘了字母表是环形的,直接写 abs(a - b)。 示例 2 里 za 只要 1 步而不是 25 步,正是用来卡这个的。


4.10 本章速查

场景 写法
反转字符串 s[::-1]
判回文 s == s[::-1]
去首尾空白 / 去 \r s.strip()
循环拼接字符串 禁止 +=,用 "".join(parts)
需要按下标改字符 a = list(s) → 改 → "".join(a)
切分输入 永远用无参 s.split()
删除某字符 s.replace(" ", "")
批量字符映射 s.translate(str.maketrans(a, b))
字母 → 0..25 ord(c) - 97
0..25 → 字母 chr(i + 97)
26 字母计数 cnt = [0]*26; cnt[ord(c)-97] += 1
保留 \(k\) 位小数 f"{x:.6f}"(严格四舍五入见第 23 章)
补前导零 s.zfill(k)f"{n:09d}"
数转二进制串 f"{n:b}",不带 0b 前缀
判合法整数 try: int(s)不要用 isdigit()
bytesstr .decode();数字可以直接 int(b"123")
大量输出 sys.stdout.write("\n".join(out) + "\n")
子串查找 s.find(t) 返回 -1s.index(t) 抛异常
重叠计数 s.count(t) 不重叠,需重叠请用 KMP
去前缀 s.removeprefix(p)(3.9+),不是 strip(p)