
如果你刷过PTA天梯赛的L1题大概率会遇到L1-039古风排版这道题。它的难度标签是“模拟”看起来不过是一个二维数组的填充但在真实的比赛环境里前期靠它拉开差距的人非常多。不是算法有多难而是输入残渣、列数边界、数组初始化、行首空格这四个细节里总有一个会让你在某个测试点上翻车。这篇文章会把整道题从题目模型、完整AC代码到我在实战中踩过的坑全部分享出来给你一份可以直接复现的解题模板。1. 题目到底在考什么——先读懂“古风排版”的映射规则1.1 古风排版的本质不是“右对齐”而是“先列后行”古人写字是从右往左、从上到下排的。你拿到一段字符串以后要把它变成“每列N个字符”的竖排效果。注意这里的N是“每一列的字符数”不是说输出N列而是每一列有N个字符。这个理解一错后面全错。很多人拿到题以为N是列数结果把数组开成N列、每行放一串字符输出自然不对。实际上N决定的是矩阵的行数N行组成一列列数由字符串长度除以N之后再向上取整决定。举个例子。如果输入N4字符串是“This is a test case”这个字符串长度是19。19除以4得到4.75所以要5列。整个输出就是一个4行5列的字符矩阵矩阵里一共20个格子而字符串只有19个字符所以有1个格子是空的用空格补上。1.2 字符串下标到二维坐标的映射关键点在于填充顺序。从原始字符串的第一个字符开始应该先填最右边那一列自上而下填满N个格子然后向左移动一列继续自上而下。填完之后的效果是最右列T、h、i、s也就是字符串的前4个字符倒数第二列空格、i、s、空格也就是字符串的第5到第8个字符倒数第三列a、空格、t、e继续往左填直到所有字符都落到矩阵里。当你按行输出这个矩阵时看到的画面是下面这样的asa T st ih e tsi ce s注意最后一行开头是一个空格这是很正常的因为最左侧那列只有3个字符被填充了第4个位置是空白。输出的时候这个空格必须原样输出不能因为“看起来多余”就删掉。这个矩阵能还原原始字符串吗可以。从最右列往左读每列从上往下读T h i s 空格 i s 空格 a 空格 t e s t 空格 c a s e拼起来正好是“This is a test case”。这就是古风排版的本质一维字符串按列重新排列而不是简单的右对齐。1.3 列数为什么必须向上取整列数的计算公式是col (len n - 1) / n比如len19n4那么(194-1)/422/45整数除法得到5列。如果len16n4(164-1)/419/44得到4列正好整除。用浮点运算也可以但不推荐。ceil((double)len / n)在绝大多数情况下能用可一旦涉及浮点精度误差边界值可能出现“明明刚好整除却多算一列”的情况。竞赛里没必要冒这个险直接用整数运算处理向上取整更安全也更省心。这里还有一个很多人容易忽略的前提题目保证字符串非空所以lenn-1不会被0除。但如果哪天真遇到了空字符串这个公式会得到1而不是0需要单独处理。好在PTA的原题不会出这种数据。2. 环境准备与输入输出的三个致命细节天梯赛最容易丢分的地方2.1 scanf读完数字后的换行残留问题这道题第一行输入一个整数N第二行输入一整行字符串。很多人的第一反应是用scanf(%d, n);读N然后用gets(s);读第二行结果字符串读出来是空的。原因很简单scanf(%d, n)只读取数字不会消费数字后面的换行符。这个换行符留在输入缓冲区内紧接着的gets一执行立刻读到一个空行。正确做法是在scanf之后先getchar()把那个换行符吃掉再读字符串。这也是L1题里出现频率最高的输入坑。不只是这道题任何“先读一个数、再读一行含空格字符串”的题目都要记得这一步。2.2 字符串里带空格不能用%s读原始字符串“This is a test case”中间有空格。如果使用scanf(%s, s)只读走了“This”后面的空格和单词全部留在缓冲区。这样后面无论你怎么填数据都是残缺的样例都过不了。正确做法是读取整行。C语言里可以用fgets(s, sizeof(s), stdin)C里可以用getline(cin, s)。fgets会连带换行符一起读进来所以读完以后要把末尾的换行符去掉。老教材里常用的gets在C11标准里已经被移除了PTA的GCC编译器虽然还能侥幸编译过但会报警告部分严格环境下直接编译失败。从个人习惯来说尽量养成用fgets的习惯不要依赖gets。2.3 fgets读入后如何去掉末尾换行fgets读入“This is a test case\n”到数组后字符串末尾会多一个\n。如果不去掉这个换行符会被当成正常字符填充进矩阵输出结果会比预期多出一列甚至导致整个排版错位。去掉的办法很直接从字符串开头遍历找到第一个\n或\0把\n替换成\0。标准写法int len 0; while (s[len] ! \0 s[len] ! \n) len; s[len] \0;这样len得到的就是纯字符串长度后续所有列数计算、填充判断都是基于这个长度。2.4 二维数组初始化空格一个总被漏掉的前提题目要求空缺的位置输出空格。但在创建二维字符数组之后数组里的元素是随机值不是空格。如果你只给有效字符赋值没让数组先整体变成空格最后输出的时候就会出现乱码。我见过的最离谱的情况是程序在本地跑得好好的交到PTA上就输出一堆“烫烫烫”或者莫名其妙的符号原因就是局部数组没有初始化。正确做法是在填充之前先对所有格子赋空格for (int i 0; i n; i) for (int j 0; j col; j) a[i][j] ;这一步虽然简单却决定着你输出矩阵的底线。3. 核心实现从“按列填充”到“按行输出”的完整推演3.1 先画一张N行×col列的稿纸思路可以类比为你在纸上画方格稿纸。N决定稿纸有多少行col决定有多少列。先把整张稿纸用空格填满相当于已经提前把“空白位置”定好了之后只需要往里面填字符。这一步千万不要省。很多人试图在输出的时候判断“这个位置有没有字符有就输出字符没有就输出空格”逻辑上虽然可行但容易出现索引判断混乱。不如直接建好矩阵让每个位置都有确定的值输出时无脑打印即可。数组大小建议开得宽松一点。题目说N100字符串长度不超过1000按最坏情况算行数最大99列数最多约11列。开char a[105][105]完全够用。今后遇到类似题目养成开数组多留一点余量的习惯能省去很多越界调试时间。3.2 填充顺序从最右列开始逐列向下核心循环是这样的int k 0; for (int j col - 1; j 0; j--) { for (int i 0; i n; i) { if (k len) { a[i][j] s[k]; } } }外层循环控制列从col-1到0也就是从最右列到最左列内层循环控制行从0到n-1也就是从上到下。这样读入字符串的顺序和古风的阅读顺序一致从右往左、从上到下。填充结束后输出时就是简单地从左到右、从上到下打印整个矩阵。因为在填充阶段已经按古风顺序放置了字符输出阶段只需要还原矩阵即可。3.3 C语言参考代码下面是一份完整的C语言版本带注释#include stdio.h #include string.h int main() { int n, len; char s[1005]; char a[105][105]; scanf(%d, n); getchar(); // 吃掉scanf留下的换行 fgets(s, 1005, stdin); // 读一整行可能含空格 len 0; while (s[len] ! \0 s[len] ! \n) len; s[len] \0; // 去掉末尾换行 int col (len n - 1) / n; // 向上取整计算列数 // 二维数组全部初始化为空格 for (int i 0; i n; i) for (int j 0; j col; j) a[i][j] ; // 从最右列开始逐列向下填充 int k 0; for (int j col - 1; j 0; j--) { for (int i 0; i n; i) { if (k len) { a[i][j] s[k]; } } } // 按行输出 for (int i 0; i n; i) { for (int j 0; j col; j) { printf(%c, a[i][j]); } printf(\n); } return 0; }这段代码的思路足够直接适合作为这道题的标准答案基础版。所有关键步骤都拆成了独立逻辑每一步都可以单独调试。3.4 Python版本参考代码Python处理字符串和列表更灵活不必显式创建二维数组也能解决。如果按照二维数组的思路写代码是这样的n int(input()) s input() length len(s) col (length n - 1) // n # 把字符串补空格补到 n * col 的长度 s * (n * col - length) # 按列切分每列n个字符 cols [s[i * n:(i 1) * n] for i in range(col)] # 按行输出最右列先输出 for row in range(n): print(.join(cols[col - 1 - j][row] for j in range(col)))这里有一点要注意input()在Python里本来就读取一整行不会像C语言那样被%s截断。但如果你用了sys.stdin.readline()记得用strip()或rstrip(\n)去掉末尾换行否则长度会多1。用列表切片把字符串按每n个字符切成若干列再按列号倒序取第row个字符逻辑上和C语言版本的二维数组是一致的。这种写法虽然创建了一个列表但因为总字符数很少内存可以忽略不计。3.5 用样例手工模拟一遍验证“从右向左读回来就是原文”我再用N4和“This is a test case”完整走一遍。原始字符串下标0到18依次是0:T 1:h 2:i 3:s 4:空格 5:i 6:s 7:空格 8:a 9:空格 10:t 11:e 12:s 13:t 14:空格 15:c 16:a 17:s 18:e建一个4行5列的矩阵全部填空格。从最右列开始第4列填T、h、i、s第3列填空格、i、s、空格第2列填a、空格、t、e第1列填s、t、空格、c第0列填a、s、e、空格。按行输出得到asa T st ih e tsi ce s现在从最右列往左读每列从上往下读得到T h i s 空格 i s 空格 a 空格 t e s t 空格 c a s e拼起来正是原文。这说明填充逻辑是对的。很多人写完后不会验证其实这个还原法是最好用的自查方法把你输出的矩阵按“从右向左、从上到下”读一遍看能不能拼回原字符串。能拼回基本就对了。4. 我在天梯赛实战中踩过的坑与排查思路4.1 样例能过、提交全挂的真凶列数算成了len/n我第一次做这道题的时候列数用的是len / n忘记向上取整。当时选的样例刚好是能被整除的数据比如字符串长度16、N4样例直接AC。但交上去之后后面一堆测试点全挂。原因很简单一旦len不能被n整除列数少算一列矩阵装不下所有字符。最后的几个字符要么被丢弃要么越界写到数组外面行为完全不可预测。排查方式很朴素用不能被整除的样例本地跑一遍。比如字符串长度19、N4立刻就能看到输出列数只有4列最后一列字符没地方放。这种问题只要构造一个手算样例就能发现千万不要只依赖平台给的样例。4.2 输出“烫烫烫”和随机乱码数组没有全量初始化数组未初始化的问题在产品代码里很难被发现因为栈上的随机值可能碰巧就是空格或合法字符。但在PTA这种严格比对输出的环境里一个随机字符就会让整行输出判错。我遇到的真实场景是本地跑一次对一次换台电脑跑就乱码最后发现是数组声明后没有统一填空格。初始化这步放在填充之前而且要对整块区域做双重循环填充不能只把用到的格子填了。调试时不建议直接看输出结果因为乱码肉眼容易忽略。推荐在输出完一行之后额外用一个可见的分隔符来定位比如临时把空格替换成下划线来看矩阵形状asa_T st_ih e_tsi _ce_s这样一眼就能看出每行每个位置放的是什么字符比盯着原始输出猜半天快得多。4.3 行首空格究竟算不算数这个问题我被问过很多次“最后一行开头那个空格输出的时候真的需要吗”需要。因为矩阵是一个整体缺失位置的空格也是排版的一部分。PTA的输出比对是逐字符进行的行首空格是有效字符。如果因为觉得“多余”而删掉输出长度就不对了整行判错。但也要注意不要在每行末尾额外加空格。矩阵是什么样就输出什么样。矩阵里的空格来自字符串本身和补位位置不需要你人工增加额外的空格。4.4 从gets迁移到fgets的注意事项早期很多题解用gets(s)因为PTA的GCC长期允许这种写法。但C11之后gets被正式移除在部分编译环境下会直接报错。与其依赖编译器旧版本不如一开始就写fgets。fgets(s, 1005, stdin)会读取最多1004个字符。如果字符串长度小于这个值换行符也会被读进来如果恰好没有换行说明字符串超长或输入到文件末尾。处理方式就是前面写过的遍历找\n并替换为\0的小函数。这段代码看似多写了3行却让程序在不同环境下的稳定性高出很多。4.5 排查技巧用分隔符把矩阵打出来看如果填充逻辑正确但输出不对最有效的排查方式不是猜而是把矩阵可视化。把空格临时替换成_、把每行用[和]包起来输出一下矩阵的边界就清楚了。比如[asa_T] [st_ih] [e_tsi] [_ce_s]看到这个结构就能立刻明白最后一行最左侧是空格不是漏了字符每一行都是5个字符没有多也没有少。确认矩阵本身没问题之后再把_改回空格拿去提交。5. 这道题背后的通用能力矩阵重排的思维模板5.1 把二维矩阵重排抽象成坐标映射公式到了第五部分我想把这道题拔高一点。古风排版看起来只是一个二维数组填充题但它本质上是在做一件事把一维序列重新映射到二维坐标再按另一种顺序读取。输出矩阵的每个位置(row, col)都对应原始字符串的某一个下标。这个下标可以不用二维数组临时存直接用公式算src row (col_count - 1 - col) * n其中n是每列字符数col_count是总列数。如果src小于len输出s[src]否则输出空格。基于这个公式C语言可以不建二维数组直接双重循环输出for (int i 0; i n; i) { for (int j 0; j col; j) { int idx i (col - 1 - j) * n; if (idx len) printf(%c, s[idx]); else printf( ); } printf(\n); }这道“无数组版”的写法只要一二十分钟不吃透建议先看懂。很多矩阵类题都能套类似的坐标映射思维。5.2 不建二维数组的一行流写法如果还想再偷懒一点可以用“先把字符串补齐再按列切片”的思路。Python里尤其流畅n int(input()) s input() col (len(s) n - 1) // n s s.ljust(n * col) for i in range(n): print(.join(s[i (col - 1 - j) * n] for j in range(col)))这串代码的核心就是那张坐标映射表。ljust把字符串右侧补满空格保证每个坐标都有值可取。接着每一行输出时按照从右列到左列的规律提取字符。这样写的好处是不需要维护二维数组代码量短也不容易把行和列搞混。坏处是理解起来比二维数组稍绕一点适合你已经完全吃透这道题之后再看。5.3 这套思维还能用在哪些地方图像旋转、Z字形打印、蛇形矩阵把一维数据按规则映射到二维坐标再按另一种顺序读取这个套路在很多题里反复出现图像旋转90度本质是坐标变换把(row, col)映射到新矩阵的另一个位置。Z字形打印矩阵需要找到对角线上的下标变化规律。蛇形填充矩阵同样是按顺序给二维矩阵的每个格子赋值。螺旋矩阵按顺时针方向给矩阵填充数字。它们的共同点都是先确定坐标变化的规律再确定填充或读取的顺序。古风排版恰好是这类题目中约束最清晰、最容易理解的一个。做完这道题之后再去做旋转图像类题目你会觉得思路特别顺。5.4 复杂度与稳定性分析从复杂度上看无论建不建二维数组时间复杂度都是O(n * col)也就是O(len)。因为字符串长度至多1000n小于100双重循环的总次数不会超过100000次运行时间在毫秒级完全不用担心超时。空间复杂度方面二维数组版是O(n * col)无数组版是O(len)或O(1)两者对本题来说都毫无压力。做题时优先选更不容易写错的版本而不是更省空间的版本。竞赛里稳定拿分比炫耀优化重要得多。如果你在做题过程中发现输出始终差一个字符或者多一个空格我建议不要急着放大看代码先自己构造一个长度不能被n整除的样例把矩阵画在纸上一步步填一遍。所有古风排版相关的坑几乎都能用一张手写矩阵暴露出来。