列优先、从上到下、从左到右

这道题到底在做什么

这道题表面上看像“字符串题”,但本质上是两步:

第一步,把一个字符矩阵按“列优先、从上到下、从左到右”读出来,拼成一个长字符串。
第二步,把这个长字符串里“夹在两个字母数字之间的连续符号”替换成一个空格。

所以你做题时,不要一上来就盯着正则。应该先把题目拆成两个动作:

  1. 先读出隐藏字符串
  2. 再按规则整理字符串

这是这题最关键的思考方式。


先把题目翻译成人话

题目给你一个由很多行字符串组成的“字符矩阵”。

比如样例是:

7 3
Tsi
h%x
i #
sM 
$a 
#t%
ir!

它不是让你按平常那样一行一行读,而是:

  • 先读第 0 列,从上到下
  • 再读第 1 列,从上到下
  • 再读第 2 列,从上到下

也就是按“列”读,不是按“行”读。


第一步怎么思考:如何把矩阵按列读出来

平常读法

平常我们更熟悉的是按行读:

for row in range(n):
    for col in range(m):
        ...

这表示:

  • 先固定第 0 行,把这一行读完
  • 再读第 1 行
  • 再读第 2 行

这叫“按行读”。

这题需要的读法

但这题要求按列读,所以顺序要反过来:

for col in range(m):
    for row in range(n):
        ...

这表示:

  • 先固定第 0 列,把这一列从上到下读完
  • 再读第 1 列
  • 再读第 2 列

这就是这题的核心。


用样例手动走一遍

原矩阵可以看成:

行号内容
0Tsi
1h%x
2i #
3sM
4$a
5#t%
6ir!

现在按列读。

第 0 列

取每一行的第 0 个字符:

  • T
  • h
  • i
  • s
  • $
  • #
  • i

拼起来:

This$#i

第 1 列

取每一行的第 1 个字符:

  • s
  • %
  • M
  • a
  • t
  • r

拼起来:

s% Matr

第 2 列

取每一行的第 2 个字符:

  • i
  • x
  • #
  • %
  • !

拼起来:

ix#  %!

三列连起来

最终得到:

This$#is% Matrix#  %!

注意,这还不是最后答案。


第二步怎么思考:哪些东西要替换成空格

题目说:

如果两个字母数字字符之间夹着一些符号或空格,那么这些符号和空格要被替换成一个空格。

比如:

This$#is

中间 $# 两边是:

  • 左边 s,是字母数字
  • 右边 i,是字母数字

所以 $# 要被替换成一个空格,变成:

This is

再看:

Matrix#  %!

这里 # %x%! 附近要仔细看。最后真正需要替换的是那些“夹在两个字母数字之间”的连续非字母数字字符。

处理后结果是:

This is Matrix# %!

这里最后的 # %! 没被全部删掉,是因为它们后面不再满足“两边都是字母数字字符”的条件。


这题为什么适合用正则

因为第二步的规则本质上是在找一种“模式”:

“左边是字母数字”
“中间是一串不是字母数字的字符”
“右边还是字母数字”

这正是正则表达式擅长做的事。

也就是说,你先把整串读出来,再一次性替换掉满足这种模式的部分。


基本语法和做法

第一步:输入矩阵

n, m = map(int, input().split())
matrix = [input() for _ in range(n)]

这里的意思是:

  • 第一行输入两个整数 nm
  • 后面读入 n 行字符串
  • 每一行字符串长度都是 m

第二步:按列读取

decoded = ""
for col in range(m):
    for row in range(n):
        decoded += matrix[row][col]

这里的 matrix[row][col] 表示:

  • row
  • col
  • 这个位置上的字符

第三步:用正则替换

import re
result = re.sub(r'(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])', ' ', decoded)

这个正则不用一下子全记住,但你要知道它在找什么。


这条正则到底是什么意思

(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])

我们拆开看。

1. (?<=[A-Za-z0-9])

意思是:

当前位置左边,必须紧挨着一个字母或数字。

它只是“检查左边”,自己不吃掉字符。


2. [^A-Za-z0-9]+

意思是:

匹配一段连续的“不是字母也不是数字”的字符。

这里可能包括:

  • 空格
  • #
  • %
  • $
  • !
  • 其他符号

+ 表示一个或多个。


3. (?=[A-Za-z0-9])

意思是:

当前位置右边,必须紧挨着一个字母或数字。

它也是只检查,不吃掉字符。


合起来的意思

整条规则的意思就是:

找到一段连续的非字母数字字符,并且它左边紧挨着字母数字,右边也紧挨着字母数字。

找到后,把这一整段替换成一个空格 ' '


最小可运行代码

这是最直接、最适合初学者理解的写法:

import re

n, m = map(int, input().split())
matrix = [input() for _ in range(n)]

decoded = ""
for col in range(m):
    for row in range(n):
        decoded += matrix[row][col]

result = re.sub(r'(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])', ' ', decoded)
print(result)

从思路到代码

这题真正应该这样想。

第一步,先不要想正则

先问自己:

“题目让我按什么顺序读字符?”

答案是:

  • 不是按行
  • 而是按列

所以你脑子里先出现的是:

for col in range(m):
    for row in range(n):

而不是先去想 re.sub()


第二步,把读出来的字符拼成一个完整字符串

因为题目的替换规则,是对“整个解码后的字符串”做处理,不是对某一行单独处理。

所以你需要先得到:

decoded

而不是边读边判断。


第三步,再对完整字符串做“清洗”

题目说的是:

  • 两个字母数字之间
  • 如果夹着一串符号或空格
  • 就把这一串变成一个空格

这说明第二步是“模式替换”,所以最自然就是 re.sub()


执行过程完整走一遍

程序执行顺序是这样的:

1. 读取 nm

n, m = map(int, input().split())

2. 读取矩阵每一行

matrix = [input() for _ in range(n)]

此时 matrix 是一个列表,里面每个元素都是一行字符串。

比如:

[
    "Tsi",
    "h%x",
    "i #",
    "sM ",
    "$a ",
    "#t%",
    "ir!"
]

3. 双重循环按列取字符

for col in range(m):
    for row in range(n):
        decoded += matrix[row][col]

先取第 0 列全部,再取第 1 列全部,再取第 2 列全部。


4. 正则替换

result = re.sub(..., ' ', decoded)

把满足规则的那一段连续符号替换成一个空格。


5. 输出结果

print(result)

这题里最容易错的地方

1. 把矩阵按行读了,而不是按列读

很多人会写成:

for row in range(n):
    for col in range(m):

这就错了,因为题目要的是“列优先”。


2. 以为所有符号都要删掉

不是所有符号都删。

只有“夹在两个字母数字之间”的那一段连续符号才替换成空格。

比如末尾的某些符号不一定动。


3. 用 \w 代替字母数字时不够严谨

有些人会写:

(?<=\w)\W+(?=\w)

这在很多场景也能过,但要注意:

  • \w 不只是字母和数字
  • 它通常还包括下划线 _

而题目明确说 alphanumeric 只有:

  • A-Z
  • a-z
  • 0-9

所以更严谨的写法是:

[A-Za-z0-9]

4. 没有先 import re

正则相关函数在 re 模块里,不导入就不能用:

import re

还能不能不用正则

可以,但会更绕。

因为你得自己判断:

  • 当前字符是不是字母数字
  • 一段符号的左右两边是不是字母数字
  • 要不要加空格
  • 加几个空格

这会写出很多判断逻辑。

而题目还特别提示了“不需要 if 条件”,其实就是在暗示你:用正则最合适。

所以这题的重点不是“炫技”,而是学会识别:

“什么时候适合先拼字符串,再做模式替换。”


本节小结

这题的核心不是正则本身,而是先把问题拆开:

第一步,按列把字符读出来。
第二步,把“夹在两个字母数字之间的连续符号”替换成一个空格。

你可以把整题记成一句话:

先按列解码,再用正则清洗。

对应代码结构就是:

import re

n, m = map(int, input().split())
matrix = [input() for _ in range(n)]

decoded = ""
for col in range(m):
    for row in range(n):
        decoded += matrix[row][col]

print(re.sub(r'(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])', ' ', decoded))

练习

你可以自己做一个小练习,先不要看答案。

题目

假设输入是:

3 3
a$1
b#2
c 3

请你先手动写出:

  1. 按列读出来的原始字符串是什么
  2. 替换规则处理之后的最终字符串是什么

提示

先分三列读:

  • 第 0 列是什么
  • 第 1 列是什么
  • 第 2 列是什么

拼成完整字符串后,再观察:

哪些非字母数字字符,是刚好夹在两个字母数字之间的。

为什么这条正则要用 (?<=...)(?=...)

在上一题里,我们用到的正则是:

(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])

很多初学者看到这里最困惑的地方,不是中间那一段 [^A-Za-z0-9]+,而是左右两边这两个写法:

(?<=...)
(?=...)

看起来它们好像也参与了匹配,但又好像没有真正“拿走”字符。这个感觉其实是对的。

这一节就专门讲清楚:

  1. 它们到底是干什么的
  2. 为什么它们只检查左右,不把左右字符也一起匹配进去
  3. 如果不用它们,会发生什么问题

先说结论:它们是“只检查、不吞字符”的条件

你可以先把它们记成一句最直白的话:

  • (?<=...):检查左边是不是满足某个条件
  • (?=...):检查右边是不是满足某个条件

重点在“检查”两个字。

它们不是来取字符的,而是来判断“这个位置周围对不对”。

所以这两个东西虽然写在正则里面,但它们本身不占用匹配内容,不会把字符包含进最终替换范围里。


先看中间真正要替换的部分是谁

在这条正则里:

(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])

真正要匹配、要替换的核心,其实是中间这段:

[^A-Za-z0-9]+

它表示:

“连续的一段非字母数字字符”。

比如:

$#
#  %
   @@@

这些都可能被它匹配到。

但是题目并不是说“所有符号都替换”,而是说:

只有当这段符号左边是字母数字、右边也是字母数字时,才替换成一个空格。

所以左右两边就需要加“条件判断”。

这时 (?<=...)(?=...) 就派上用场了。


(?<=...) 是什么:向左看一眼

最直白理解

(?<=[A-Za-z0-9])

意思是:

“当前位置左边,紧挨着的那个字符,必须是字母或数字。”

注意,这句话里说的是“左边是”,不是“把左边这个字符也抓进来”。

它只是看一眼左边合不合格。

可以这样想象

假设字符串是:

This$#is

当正则扫描到 $ 这里时,它要判断这段 $# 能不能作为替换目标。

它会先看 $ 左边的字符是谁。

左边是:

s

s 是字母,符合 [A-Za-z0-9],所以左边条件通过。

但是这个 s 只是被“检查到了”,并没有被匹配进去。


(?=...) 是什么:向右看一眼

最直白理解

(?=[A-Za-z0-9])

意思是:

“当前位置右边,紧挨着的那个字符,必须是字母或数字。”

同样,它也只是检查右边,不把右边字符包含进匹配结果。

接着刚才的例子

还是:

This$#is

中间 $# 这段匹配完以后,正则还要检查这段后面紧挨着的是不是字母数字。

后面是:

i

i 是字母,所以右边条件也通过。

于是整段 $# 符合要求,可以被替换成一个空格。


为什么说它们“不吞字符”

这是最关键的一点。

正则里有两类东西,你可以先这样区分:

第一类:真正匹配字符的

比如:

a
\d
[A-Z]
[^A-Za-z0-9]+

这些都会真正去匹配字符串中的某些字符,匹配到的内容会进入“本次匹配结果”。

第二类:只判断位置条件的

比如:

^
$
\b
(?=...)
(?<=...)

这些不负责拿字符,只负责判断“这里是不是一个合适的位置”。

所以 (?<=...)(?=...) 有一个很重要的名字,叫:

零宽断言

你现在不用死记这个术语,但要理解“零宽”这三个字:

  • 它有作用
  • 但它本身不占字符宽度
  • 它不会把字符吃进去

“宽度”你可以暂时理解成“实际占了几个字符的位置”。

它占 0 个字符,所以叫零宽。


用一个更形象的比喻理解

你可以把整个匹配过程想成“抓取中间那段符号”。

(?<=...)(?=...) 像两个门卫:

  • 左边门卫检查:你前面是不是字母数字
  • 右边门卫检查:你后面是不是字母数字

只有两个门卫都点头,中间这一段符号才能被放进去匹配结果里。

但是门卫自己不会跟着一起被抓进去。

所以最后替换的,只有中间那段符号,不包括左右两边的字母数字。


直接看一个完整例子

看这个字符串:

A$#B

我们用这条正则:

(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])

来匹配。

第一步:看中间谁可能匹配

中间 $# 符合:

[^A-Za-z0-9]+

因为它们都不是字母数字,而且是连续的一段。

第二步:检查左边

$ 左边是 A,是字母数字,左边通过。

第三步:检查右边

# 右边是 B,是字母数字,右边通过。

最终匹配结果

最终被匹配到的是:

$#

不是:

A$#B

这就是“只检查左右,但不把左右字符一起匹配进去”。


如果它们也把左右字符算进去,会出什么问题

如果左右字符也被匹配进去,那么替换时就会把原本应该保留的字母数字一起替换掉。

比如还是:

A$#B

如果整个 A$#B 都被匹配,然后替换成一个空格,那结果就变成:

 

这显然不对。

我们真正想要的是:

A B

也就是:

  • 保留左边的 A
  • 保留右边的 B
  • 只把中间 $# 换成一个空格

所以这里必须让左右字符“参与条件判断”,但“不参与替换内容”。

这正是 (?<=...)(?=...) 最适合的地方。


为什么不能直接把左右写进普通匹配里

很多初学者会自然想到,能不能写成这种样子:

[A-Za-z0-9][^A-Za-z0-9]+[A-Za-z0-9]

这个写法表面上看也像是:

“左字母数字 + 中间符号 + 右字母数字”

但它和前面的断言写法,本质上不一样。

因为这个写法会把三部分都真正匹配进去。

也就是说,匹配结果是:

A$#B

而不是单独的:

$#

那你如果做替换:

re.sub(r'[A-Za-z0-9][^A-Za-z0-9]+[A-Za-z0-9]', ' ', s)

就会把左右字母数字也替换掉。

这不符合题意。


对比一下,两种写法到底差在哪里

写法一:普通匹配

[A-Za-z0-9][^A-Za-z0-9]+[A-Za-z0-9]

含义是:

  • 左边字母数字也要匹配
  • 中间符号也要匹配
  • 右边字母数字也要匹配

最终整段都进入匹配结果。

写法二:断言 + 普通匹配

(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])

含义是:

  • 左边字母数字只负责检查
  • 中间符号才是真正要匹配的内容
  • 右边字母数字也只负责检查

最终只有中间符号进入匹配结果。

所以第二种才是这题真正需要的。


你可以把它理解成“锚定中间,不吞两边”

这句话很适合记忆:

用前后断言,是为了锚定中间那一段的上下文,但不吞掉上下文字符。

这里的“上下文”就是:

  • 左边是什么
  • 右边是什么

而我们真正关心、真正要改掉的,是中间那段符号。


再看几个小例子

例 1

字符串:

a#$b

匹配到的是:

#$

替换后:

a b

左右的 ab 不会消失。


例 2

字符串:

#$ab

前面的 #$ 左边没有字母数字,所以不匹配。

也就是说,不能只看中间是符号,还得看左右条件。


例 3

字符串:

ab#$ 

后面的 #$ 右边没有字母数字,所以也不匹配。


例 4

字符串:

ab#$cd

这里 #$ 左右都是字母数字,所以会匹配并替换成空格:

ab cd

这和“位置”有什么关系

你可以把正则匹配想成这样:

它不是只会盯着字符看,它也会盯着“字符之间的位置”看。

比如字符串:

abc

其实可以想象成有很多位置:

|a|b|c|

(?<=...)(?=...) 经常就是在这些“位置”上做判断:

  • 这个位置左边是什么
  • 这个位置右边是什么

所以它们才不需要真的把字符拿进来,它们只是在某个位置上检查周围环境。

这个角度一旦理解了,你就会更容易明白“为什么断言不吞字符”。


在这道题里,替换时到底替换了什么

还是那条代码:

import re

result = re.sub(r'(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])', ' ', decoded)

re.sub() 会把“匹配到的内容”替换成空格。

而这条正则里,真正匹配到的内容只有:

[^A-Za-z0-9]+

左右两个断言只是提供匹配条件。

所以最后被换掉的只是中间那段符号。

这就是为什么结果刚好符合题意。


初学者最容易混淆的点

第一种误解:我都写进正则里了,为什么它不算匹配内容

因为正则里面不是所有部分都代表“拿字符”。

有些是“拿字符”,有些是“判断条件”。

(?<=...)(?=...) 就属于后者。


第二种误解:既然它检查了左边右边,为什么替换时不把左右也一起改掉

因为替换函数只替换“真正匹配到的部分”。

断言只是附加条件,不属于最终匹配文本。


第三种误解:那它到底有没有作用

当然有作用,而且作用很大。

如果没有这两个断言,正则就只会找到“任意连续符号”,那题目要求就不准确了。

它们的作用不是“提供字符”,而是“限制匹配范围”。


本节小结

这一节最重要的不是背术语,而是抓住下面这句话:

(?<=...)(?=...) 是在检查中间这段字符的左右环境,而不是把左右字符一起拿来替换。

所以在这道题里:

(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])

可以理解成:

  • 左边先检查:前面得是字母数字
  • 中间再匹配:抓一段连续符号
  • 右边再检查:后面也得是字母数字

最终替换掉的,只有中间这段连续符号。

你把它记成一句更短的话也可以:

前后断言只看两边,中间表达式才是真正被替换的内容。


练习

你可以自己先想,不要急着看答案。

题目 1

字符串是:

A@@B

用这条正则:

(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])

匹配到的到底是:

A@@B

还是:

@@

请你先自己判断原因。

提示

想清楚两件事:

  1. 哪一部分是真正“吃字符”的
  2. 左右两边那两个括号是不是只在检查条件

题目 2

为什么下面这个写法不适合这道题:

[A-Za-z0-9][^A-Za-z0-9]+[A-Za-z0-9]

提示

试着把字符串 A@@B 代进去,想想它真正匹配到的整段是什么。然后再想:re.sub() 会替换掉哪一部分。

这段模板在这道题里是干什么的

这段模板的作用,其实很简单:

先把输入读进来,整理成我们后面要处理的数据。

在这道题里,真正重要的数据有两个:

  1. nm,表示矩阵有多少行、多少列
  2. matrix,表示整个字符矩阵

也就是说,这段模板还没有开始“解码”,它只是先把题目给的数据接住。

你可以把它理解成:

  • 前面这部分:负责“收材料”
  • 后面你自己写的代码:负责“加工材料”

先把整段模板从头到尾看懂

题目模板是:

#!/bin/python3

import math
import os
import random
import re
import sys

first_multiple_input = input().rstrip().split()

n = int(first_multiple_input[0])

m = int(first_multiple_input[1])

matrix = []

for _ in range(n):
    matrix_item = input()
    matrix.append(matrix_item)

下面我们一行一行解释。


#!/bin/python3 是什么

#!/bin/python3

这一行一般叫 shebang。

对于你现在做题来说,可以先当成平台环境的一部分,不用太纠结。它不是 Python 核心语法重点,也不是这题解题的关键。

你现阶段可以简单理解为:

“告诉系统,这个文件用 Python 3 来运行。”

在在线刷题平台里,它通常已经写好了,你不用改。


这些 import 是什么

import math
import os
import random
import re
import sys

意思是导入一些模块。

但注意,在这道题里,不是每个模块都会真的用到。

比如这题真正会用到的,通常是:

import re

因为我们要用正则替换。

而像:

import math
import os
import random
import sys

很多时候只是平台模板顺手带上的,不一定非用不可。

所以你看到模板里导入很多东西,不要紧张。
不是说每一行都必须在你的代码里派上用场。


first_multiple_input = input().rstrip().split() 是什么意思

这是这段模板里最值得重点理解的一句。

first_multiple_input = input().rstrip().split()

我们拆开看。

input()

表示读入一整行输入。

假设样例第一行是:

7 3

那么:

input()

读到的就是这一整行字符串:

"7 3"

注意,它读到的是字符串,不是整数。


.rstrip()

input().rstrip()

rstrip() 的作用是:去掉字符串右边末尾的空白字符。

比如:

"7 3\n"

经过 rstrip() 后,会变成:

"7 3"

在很多在线题目里,input() 本身已经够用了,rstrip() 往往是一个比较稳妥的习惯写法。

你当前可以先把它理解成:

把这行末尾多余的换行、空格处理掉。


.split()

input().rstrip().split()

split() 的作用是:按空白字符拆分成列表。

比如:

"7 3".split()

结果就是:

["7", "3"]

所以这一整句:

first_multiple_input = input().rstrip().split()

最终得到的是:

["7", "3"]

为什么它叫 first_multiple_input

这个变量名你不用太在意。

first_multiple_input

你可以把它翻译成:

“第一行里读到的多个输入项”

因为第一行不是一个值,而是两个值:

  • n
  • m

所以先把它们读成一个列表。

变量名本身不是重点,重点是它里面装了什么。


n = int(first_multiple_input[0]) 是什么意思

n = int(first_multiple_input[0])

前面我们已经知道:

first_multiple_input = ["7", "3"]

那么:

first_multiple_input[0]

就是第 0 个元素,也就是:

"7"

但它现在还是字符串,所以要转成整数:

int("7")

结果就是:

7

所以最终:

n = 7

m = int(first_multiple_input[1]) 是什么意思

同理:

m = int(first_multiple_input[1])

这里:

first_multiple_input[1]

取到的是:

"3"

转成整数以后:

m = 3

所以到这里为止,程序已经知道:

  • 矩阵有 7
  • 矩阵有 3

matrix = [] 是什么意思

matrix = []

这表示先创建一个空列表,准备后面把每一行数据放进去。

这里的 matrix,你可以理解成:

“整个字符矩阵”

虽然名字叫矩阵,但现在它在 Python 里其实是一个列表。

后面会把每一行字符串都放进去。


for _ in range(n): 是什么意思

for _ in range(n):

这表示循环 n 次。

如果 n = 7,那就循环 7 次,也就是读 7 行。

这里的 _ 你前面也接触过了,它的意思是:

这个循环变量本身我不关心,我只是想重复这么多次。

因为这里我们只需要“读 7 行”,并不需要用循环变量本身做别的事,所以写 _ 很常见。


matrix_item = input() 是什么意思

matrix_item = input()

表示每次循环时,读入矩阵中的一行。

比如第一轮读到:

"Tsi"

第二轮读到:

"h%x"

第三轮读到:

"i #"

……

每一轮读到的一整行,都是一个字符串。

注意,这里读到的不是单个字符,也不是列表,而是一整行字符串


matrix.append(matrix_item) 是什么意思

matrix.append(matrix_item)

表示把刚刚读到的这一行,放进 matrix 列表里。

比如第一次循环后:

matrix = ["Tsi"]

第二次循环后:

matrix = ["Tsi", "h%x"]

继续加下去,最后变成:

matrix = [
    "Tsi",
    "h%x",
    "i #",
    "sM ",
    "$a ",
    "#t%",
    "ir!"
]

这就是整个矩阵在 Python 里的存法。


这里的 matrix 到底是什么数据结构

这一点很重要。

很多初学者一看到“矩阵”,脑子里就会想到“二维表”,于是以为它必须长这样:

[
    ['T', 's', 'i'],
    ['h', '%', 'x'],
    ['i', ' ', '#']
]

但这道题里,模板读出来的 matrix 其实是:

[
    "Tsi",
    "h%x",
    "i #",
    "sM ",
    "$a ",
    "#t%",
    "ir!"
]

也就是说:

它是“字符串列表”,不是“字符二维列表”。

不过这并不影响我们取字符,因为字符串本身也支持索引。

比如:

matrix[0]

得到:

"Tsi"

再比如:

matrix[0][1]

表示:

  • 第 0 行
  • 这一行中的第 1 个字符

结果就是:

"s"

所以这题虽然 matrix 不是“列表套列表”,但仍然可以像矩阵一样按“行、列”取字符。


用样例把这段模板跑一遍

假设输入是:

7 3
Tsi
h%x
i #
sM 
$a 
#t%
ir!

那么程序执行后,各变量的值就是:

n = 7
m = 3
matrix = [
    "Tsi",
    "h%x",
    "i #",
    "sM ",
    "$a ",
    "#t%",
    "ir!"
]

到这里,模板的任务就完成了。

也就是说:

题目数据已经准备好了,现在轮到你写“解码”和“替换”的逻辑。


接下来应该怎么接着写

前面模板只是把输入读好了。
后面你要做两件事:

第一件事:按列读取字符

因为题目要求:

  • 从最左边的列开始
  • 每一列从上到下读
  • 再读下一列

所以要写成:

decoded = ""

for col in range(m):
    for row in range(n):
        decoded += matrix[row][col]

这里的意思是:

  • col 先固定列
  • row 再从上到下取这一列里的字符
  • 每取一个字符,就拼到 decoded 后面

第二件事:用正则替换中间那段符号

拼完以后,decoded 会得到一个完整字符串,比如样例里会得到:

This$#is% Matrix#  %!

然后再做替换:

result = re.sub(r'(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])', ' ', decoded)
print(result)

这样就得到最后答案。


放回这个模板里,完整代码应该长什么样

下面是最适合初学者理解的完整版本,我尽量保留了题目模板原来的结构:

#!/bin/python3

import math
import os
import random
import re
import sys

first_multiple_input = input().rstrip().split()

n = int(first_multiple_input[0])
m = int(first_multiple_input[1])

matrix = []

for _ in range(n):
    matrix_item = input()
    matrix.append(matrix_item)

decoded = ""

for col in range(m):
    for row in range(n):
        decoded += matrix[row][col]

result = re.sub(r'(?<=[A-Za-z0-9])[^A-Za-z0-9]+(?=[A-Za-z0-9])', ' ', decoded)

print(result)

从思路到代码,应该怎么连起来

这部分对你最重要,因为你现在常常是“看懂了代码,但不会自己写”。

所以这题要这样想。

第一步,先想输入最后会变成什么样

题目给你的是很多行字符。

你先不要急着写正则,而是先想:

“我要把这些输入先存成什么样?”

模板已经帮你做了这件事,它把输入变成了:

matrix = [
    "Tsi",
    "h%x",
    ...
]

也就是“每一行一个字符串”。


第二步,再想题目要我按什么顺序读

题目不是按行读,而是按列读。

所以你脑子里要先冒出这个结构:

for col in range(m):
    for row in range(n):

因为这代表:

  • 先选一列
  • 再把这一列从上到下读完

这是这题最关键的思维转换。


第三步,把读出来的所有字符拼成一个大字符串

因为后面正则处理的是“整个解码结果”,不是单独某一行。

所以你要先有一个变量:

decoded = ""

然后一边读,一边拼接进去。


第四步,再想“哪些符号该替换”

不是所有符号都要删,而是:

夹在两个字母数字之间的连续非字母数字字符,替换成一个空格。

所以正则是用来做“第二步整理”的,不是用来做“第一步解码”的。

你这样分层去想,就不会乱。


这段模板里几个最容易让初学者困惑的点

第一,为什么 matrix 里放的是字符串,不是列表

因为每一行输入本来就是一整行字符串,比如:

"Tsi"

而字符串也可以用索引取字符,所以完全能满足这题需要。

例如:

matrix[0][0]

得到的是第一行第一列的字符。


第二,为什么这里不用 split()

因为矩阵每一行不是“用空格分隔的多个数据”,而是一个完整的字符行。

比如:

Tsi

如果你写成:

input().split()

那会变成一个列表,而不是题目想要的那种原样字符行。

这题这里直接:

matrix_item = input()

就够了。


第三,为什么模板里提前 import re

因为这题后面很可能要用正则。

虽然输入阶段还没用到,但模板已经帮你导入了,后面就可以直接写:

re.sub(...)

第四,为什么要先读完全部矩阵,再处理

因为题目要求的结果,是基于“整个按列读出来的字符串”来做替换。

所以流程必须是:

先读完整个矩阵
再按列拼成大字符串
最后再整体替换

而不是边读一行边替换。


如果想写得更简洁一点,可以怎么写

如果你以后熟练一些,这段:

decoded = ""

for col in range(m):
    for row in range(n):
        decoded += matrix[row][col]

也可以写成:

decoded = ''.join(matrix[row][col] for col in range(m) for row in range(n))

但对于你现在这个阶段,我更建议先用前一种双重循环写法。

因为它更清楚地体现了:

  • 先按列
  • 再按行
  • 每次取一个字符
  • 拼接到字符串后面

先把思路写清楚,比一开始就追求短更重要。


本节小结

这段模板你可以这样整体理解:

前半部分负责把输入整理成:

n
m
matrix

其中:

  • n 是行数
  • m 是列数
  • matrix 是一个“每个元素都是一行字符串”的列表

然后你再在它后面接上自己的解题逻辑:

  1. 按列读取字符,拼成 decoded
  2. re.sub() 把中间该替换的连续符号替换成一个空格
  3. 输出结果

所以这段模板不是难点,它只是“把原材料先装好”。


练习

你可以先自己试着回答这两个问题,不要急着看答案。

题目 1

如果输入是:

3 4
abcd
efgh
ijkl

那么模板执行完以后:

  • n 等于多少
  • m 等于多少
  • matrix 里面装的到底是什么

提示

重点想清楚:matrix 里的每个元素,是“一个字符”,还是“一整行字符串”。


题目 2

如果这时你写:

print(matrix[1][2])

输出会是什么?

提示

先找:

  • 第 1 行是谁
  • 这一行的第 2 个字符是谁
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇