☰
洛谷P1319压缩技术:游程编码还原与状态机模拟的格式陷阱解析
2026/10/14 6:09:03 网站建设 项目流程

洛谷的入门题单里,P1319 压缩技术一直是我觉得“看着简单,但值得掰开揉碎讲”的一道题。好几次刷题群里有朋友问这题为什么样例能过、一提交就格式错误,最后查出来的问题几乎都集中在两处:换行没对、输入没读全。这道题题目全名叫“压缩技术”,本质上是要你把一串数字描述的游程编码,还原成一个 N×N 的 01 点阵,也就是平时说的解密过程。

先给结论:这题的标签是“模拟”,难度不高,但信息量不小。它逼你想清楚三件事——怎么维护当前该输出的字符、怎么在正确位置换行、怎么处理可能被拆成多行的输入数字。把这三个问题理顺,后面再碰任何需要输出矩阵、输出格式敏感的模拟题,你会顺手很多。如果你是刚开始刷题的新手,或者被“格式错误”折磨过,这篇内容应该能帮上忙。

1. 拿到题目先做减法:压缩技术到底在考什么

1.1 压缩规则到底怎么读

题面讲的规则可以拆成两步理解。第一步,把 N×N 的 01 点阵按行拼接成一条长串。比如下面这个 3×3 的矩阵:

000 110 000

把它拉直,就是一条长度为 9 的串:000110000。

第二步,对这条长串做游程编码。规则是固定的:先数连续 0 的个数,再数连续 1 的个数,再数连续 0 的个数……哪一段相同就数哪一段,段长用十进制数字表示,数字之间用空格隔开。上面这个例子,压缩串就是3 2 4,因为开头是 3 个 0,接着 2 个 1,最后 4 个 0。

所以读题的时候有两点特别关键。一是要把二维矩阵和一维串互相转换,很多第一次接触的人会下意识想在二维数组里做文章,其实不用,拉直处理更干净。二是压缩顺序固定从“0 段”开始,如果整个点阵开头就是 1,那压缩串第一项会是一个 0,表示“开头连续 0 的个数为 0”。这个细节在后面写状态机时非常重要。

1.2 核心考点:这其实是最简单的状态机

很多题解会直接告诉你“用一个变量记录当前该输出 0 还是 1”,但我想换一个更本质的说法:你是在维护一个只有两个状态的状态机。

状态机听起来很高端,翻译成大白话就是:你心里得清楚“现在该输出哪个字符”。压缩串里的每个数字,都是一条指令:“按当前状态连续输出这么多个字符”。每执行完一条指令,状态就翻转一次——0 变 1,1 变 0。整个程序的核心循环就两行逻辑:

按当前状态输出 x 个字符 翻转当前状态

这比用数组存下所有字符再处理要简单,也更接近游程编码的本质。游程编码本来就是“长度 + 内容”交替出现,你只是把“内容”用状态隐式表达了。

另外,这题还考察输出格式的处理。点阵最后要按 N 个字符一行来展示,这就要求你输出的同时,精确在第 N、2N、3N……个字符后面补换行。很多人在这一步翻车,不是多一个空行,就是最后一行缺换行。这些细节我会在第三部分详细说。

1.3 这题不算难,为什么值得认真写

说句实话,P1319 在洛谷的难度体系里差不多是普及-,见过的人都会写。但它在“入门模拟”这个分类里属于定位很典型的一道:数据小、逻辑简单、坑却不少。

正因为坑不少,它非常适合用来训练“防御性编程”习惯。比如输入数字可能跨行、末尾可能带一个多余的 0、点阵可能全 0 也可能全 1。这些边界情况在比赛里不会写在题目明面上,但评测数据里一定会有。花二十分钟把这道题吃透,比你闷头刷十道简单题更有价值,因为你从里面提炼出来的,是一套处理格式化输出和状态翻转的通用思路。

2. 第一版解法:边读边输出的状态翻转法

2.1 思路:把压缩串当成一条条输出指令

这一版的思路非常朴素,不需要额外开二维数组,更不需要把整个点阵先存下来。你只需要把压缩串读出来,然后按指令办事:

  • 读到一个数字 x,就按当前状态连续输出 x 个字符;
  • 每输出一个字符,计数器加 1;
  • 当计数器变成 N 的倍数时,说明这一行已经输出完,补一个换行;
  • 一条指令执行完,翻转当前状态。

这个思路的时间复杂度是 O(N²),因为输出量本身就是 N² 个字符,压缩串无论怎么给,你都要把它们全部还原出来;空间复杂度是 O(1),只用了几个普通变量。对于这种入门题,简单就是优势,代码越直白越不容易错。

我还想强调一点:输出阶段就要同步完成换行判断,不要等全部输出完再回去处理。因为你的数据是流进来的,压缩串可能很长,边读边写是最自然的方式,也方便你在循环里直接控制格式。

2.2 完整 C++ 实现与逐段说明

下面这段代码是我觉得最适合作为参考模板的写法:

#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; int cnt = 0; // 已经输出的字符总数 bool one = false; // false 表示当前输出 0,true 表示输出 1 int x; while (cin >> x) { for (int i = 0; i < x; ++i) { cout << (one ? '1' : '0'); ++cnt; if (cnt % n == 0) { cout << '\n'; if (cnt == n * n) { return 0; } } } one = !one; // 一段结束,切换状态 } return 0; }

逐段解释一下。

bool one = false;是整个状态机的初始状态。因为压缩规则是从连续 0 的个数开始的,所以不管压缩串第一个数字是不是 0,初始状态都必须定为“输出 0”。如果压缩串开头是0 4 5,那么第一条指令相当于“输出 0 个 0”,状态翻转为输出 1,再输出 4 个 1,逻辑完全正确。

while (cin >> x)负责持续读入所有数字。这里用流循环读取有一个隐藏好处:无论数字在同一行还是跨了多行,都能被完整读进来。你不需要关心题目数据里数字是怎么分行的,这能省掉很多麻烦。

cnt % n == 0是换行判断。每输出一个字符,cnt 先自增,然后判断是不是 N 的倍数。比如 N=3,输出第 3 个、第 6 个、第 9 个字符时都会触发换行。注意这也会在最后一行之后补一个换行,评测系统对这个末尾换行是接受的,不用担心。

cnt == n * n的时候提前返回,是一种防御性写法。题目数据正常时,输出 N² 个字符后整个点阵已经还原完毕,即便压缩串后面还跟着数字,也已经没有意义了。提前退出可以避免因为多余数据而输出杂乱内容。

2.3 用一个小例子完整跑一遍

为了让你更直观地理解流程,我构造一个例子:N=3,压缩串为3 2 4。

  • 读入3,状态为输出 0。输出000,此时 cnt=3,3%3=0,换行。
  • 状态翻转为输出 1。读入2,输出11,此时 cnt=5,5%3 不等于 0,不换行。
  • 状态翻转为输出 0。读入4,输出0000,此时 cnt 依次变成 6、7、8、9。cnt=6 时换行一次,cnt=9 时再换行一次。

输出结果就是:

000 110 000

正好和 2×3 的点阵对应回来。整个过程的重点在于:换行不是“这一行结束了”才知道,而是当计数器达到 N 的倍数时立刻处理,这样流程最自然,也最不容易漏。

3. 换行、读入与边界:三个最容易翻车的细节

3.1 换行到底怎么处理才稳

换行这个点,我见过至少三种错误写法:有人每输出一个字符就换行,有人全部输出完再统一补换行,还有人会在行尾多输出一个空格。

最稳的做法就是我上面代码里的思路:用一个计数器跟踪“已经输出的字符总数”,每输出一个字符后检查cnt % n == 0。这样换行位置完全由数据驱动,不会出现人为算错行号的问题。

另外,关于最后一行后面到底要不要换行,这里多说一句。评测系统比对的是输出文件的字符序列,绝大多数题目对末尾是否有换行并不敏感,所以你在输出完最后一个字符后补一个换行完全没问题。真正要小心的是中间行缺换行,或者行与行之间多了一个空行。用cnt % n == 0这个方法,每行之间恰好一个换行,不会多不会少。

3.2 压缩数字可能跨行,输入不能一次读完

这是很多新手踩的第二个坑。题目描述里压缩串是一串数字,但评测数据并不保证所有数字都在同一行。我第一次写这道题时,用cin >> n之后直接for循环读 N² 个字符,就吃了输入顺序的亏。后来改成while (cin >> x)流派式读取,问题瞬间消失。

如果你习惯用 Python,这个坑同样存在。input().split()只能读当前行,万一数字被拆成多行,你就只能拿到前半段。正确的做法是用sys.stdin.read().split(),一次性把所有空白分隔的 token 全读进来,再逐个处理。这是处理不确定行列输入数据的标准姿势,不仅这道题能用,很多在线评测题都能用。

3.3 末尾的 0、开头是 1 等边界情况

边界情况主要有三个。

第一,压缩串末尾可能出现一个 0。这个数字对应“输出 0 个当前字符”,所以不会产生任何输出,但它会让状态翻转一次。由于后面没有更多数字了,翻转不翻转其实没有影响,你的程序不会因此出错。这里想提醒的是,不要在循环里对“x 等于 0”做特殊判断,直接让它走正常逻辑反而更安全。

第二,点阵如果以 1 开头,压缩串第一项是 0。处理方式我已经说过了:初始状态定为 false,也就是输出 0。第一条指令输出 0 个 0,状态翻转,再输出后面的 1 段,完美匹配。

第三,N=1 的情况。这时整个点阵只有一个字符,压缩串总共只会让你输出 1 次。换行判断依然有效,输出第一行后 cnt=1,触发换行。这种极端数据平常不容易想到,但评测数据里一定有,建议你写完代码后在本地手测一次。

3.4 输出性能:不用 endl,iosteam 要不要关同步

这道题的数据规模很小,用不用endl其实都能过。但我想借这个机会说一个习惯问题:竞赛代码里,endl尽量别用。因为endl不只是换行,它还会强制刷新输出缓冲区,在输出量大的时候会导致大量 flush 操作,拖慢程序。用'\n'既能换行又不刷新缓冲区,效率高得多。

同理,C++ 里cin/cout默认会和 C 标准输入输出同步,这种同步带来额外开销。如果你在代码开头加上:

ios::sync_with_stdio(false); cin.tie(nullptr);

就可以去掉这部分开销。对于 P1319 这种题,不加也完全没问题,但养成习惯后,遇到输出量大的题不至于因为 I/O 拖累超时。

4. 再往前走一步:不存结果直接输出与 Python 写法

4.1 Python 的整洁写法

虽然我是 C++ 为主,但偶尔也用 Python 写写这种小题。这里给出一份比较完整的 Python 解法:

import sys def main(): data = sys.stdin.read().split() if not data: return n = int(data[0]) nums = list(map(int, data[1:])) out = [] cur = 0 for length in nums: out.append(str(cur) * length) cur ^= 1 s = ''.join(out) for i in range(0, n * n, n): print(s[i:i + n]) if __name__ == "__main__": main()

这份代码有几个好用的点:sys.stdin.read().split()处理多行输入;cur ^= 1用来翻转 0 和 1,比写cur = 1 - cur更简洁;str(cur) * length直接生成长度为 length 的连续字符,省去了内层循环。最后把整条长串按每段 N 个字符切片输出。

要注意的是,data[1:]里的数字数量可能多于实际需要的数量,不过我们最后用n * n的切片范围控制了输出长度,多出来的字符不会进入输出。这也是一种防御性处理。

4.2 数据变大时怎么办:流式输出比攒字符串更省内存

P1319 的数据范围很小,用上面的join攒字符串完全没问题。但你可以顺着这个思路想一个问题:如果 N 很大,比如 N=10000,那么点阵有上亿个字符。这时候把所有字符先攒成一个字符串再输出,内存会很紧张。

遇到这种场景,应该回到 C++ 的流式输出方案,或者用 Python 的sys.stdout.write分批写。写一批字符就刷新一次缓冲,避免把整个结果都存在内存里。实际上模拟题常见的套路就是“边还原边输出”,空间复杂度 O(1) 是这类题最能体现的优势。

你也可以在 C++ 代码里把内层for循环换成更紧凑的写法:

while (cin >> x) { while (x--) { cout << (one ? '1' : '0'); if (++cnt % n == 0) cout << '\n'; } one = !one; }

这两种写法本质一样,用哪种看你的口味。我习惯用第一种,因为多了防御性的cnt == n * n提前返回,但第二种更短。

4.3 配套的加密题 P1320:一题拆成两题练

洛谷还有一道配套题,加密版,方向正好反过来:给你一个 N 和 N 行 01 串,要求输出压缩串。做完 P1319 再去做那一题,会形成一组很舒服的往返练习。

加密版的难点和 P1319 不太一样:你需要统计连续相同字符的长度,并且保证输出顺序仍然从 0 段开始。这里最实用的技巧是“哨兵字符”,也就是在长串末尾人为追加一个不可能出现的字符,比如'2',用来在扫描时把最后一段强制顶出来。扫描过程中,遇到字符变化就把当前计数输出,然后重置计数器,这就避免了“最后一组数字没地方输出”的尴尬。

另外要注意,如果整个点阵以 1 开头,加密后的压缩串第一项应该是 0,这一点和 P1319 的初始状态是呼应的。两题对照着看,对状态翻转和扫描统计的理解会深很多。

5. 模拟题的通用套路与我自己的一点心得

5.1 做模拟题先做四步

P1319 这类题目,本质上是在考察“把人类语言翻译成程序逻辑”的能力。我总结了一个四步流程,基本适用于所有模拟题。

第一步,复述规则。用自己的话把题目压缩规则写成一段话,不急着写代码,先确保你理解“先数 0 个数、再数 1 个数”这个交替逻辑。

第二步,确认状态。想清楚整个程序运行过程中,有哪些信息是需要一直记住的。P1319 这里只有一个状态:当前输出 0 还是 1。

第三步,确定输出格式。换行、空格、末尾换行,任何一个细节都要提前想明白。我会在纸上先模拟一小段,确认第几个字符后面要换行。

第四步,选择输入方式。根据题目数据可能的分行方式,决定用流式读取还是全量读取。多行数字一律用cin >> x或sys.stdin.read().split()。

这四步走完,代码怎么写基本就清楚了,剩下的是把逻辑翻译成语法。

5.2 把常见错误列成一张速查表

我把自己和身边人在这类题上踩过的坑整理成一张速查表,写完代码后对照着检查一遍,能省下不少提交试错的时间:

错误类型典型表现应对方法
初始状态搞错开头是 1 的点阵输出错位初始状态设为输出 0,因为压缩从 0 段开始
换行位置不对所有字符连成一行维护已输出计数,cnt % n == 0时换行
输入读不全后半段数字丢失导致输出不足用流式循环读取或read().split()
末尾多余数据输出多出几行内容在cnt == n * n时提前返回
使用 endl 输出大数据量下运行变慢统一用'\n'替代
输出行尾空格格式错误而非答案错误换行前不要补空格,只输出字符

这里面最冤的就是最后两类,答案明明是对的,却因为格式问题被判错。养成先查格式的习惯,很大程度能避免这种不必要的心态爆炸。

5.3 两个让我少踩坑的小习惯

最后分享两个我自己的做题习惯。

第一,提交前在本地测几个极端输入。比如 N=1、全 0 的点阵、全 1 的点阵、以 1 开头的点阵。这些数据构造起来只要几分钟,但能帮你提前发现状态初始化和换行逻辑的问题,比反复提交让评测系统告诉你错在哪里要高效得多。

第二,一道模拟题写完后,不管对不对,我都会去看看别人的题解。不是抄代码,而是观察别人怎么处理换行和输入读取。同一个问题有各种各样的写法,有的用数组存整行,有的用流式输出,有的用递归,每种写法都代表一种思考路径。看多了以后,你再遇到新的模拟题,脑子的备选方案会多很多。

P1319 这道题本身确实不大,但它把“状态翻转、格式输出、多行输入、边界防御”这四个模拟题核心要素全占齐了。吃透它,再去做加密版、扫雷游戏、海战这类同级题目,你会明显感觉到套路都是通的。这也是我为什么愿意为一道入门题写这么多字的原因——题目是小的,方法是有复用价值的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询