洛谷 P1319 / P1320 压缩技术------同一枚硬币的正反面
📌 摘要
P1319 和 P1320 是一对互逆操作:前者把压缩码还原成 N×N 点阵(游程解码),后者把点阵重新压回压缩码(游程编码)。P1319 只需一个布尔变量交替切换、边读边输出;P1320 需要用两个计数器追踪连续 0 和 1,通过"延迟一步"检测字符切换来输出游程长度。两题合在一起看,恰好是"游程编码"这项技术的正反两面。文末附有压缩技术从 1960 年代到 2025 年的完整进化脉络。
题目链接 :P1319 压缩技术 | P1320 压缩技术(续集版)
📚 目录
-
[📝 前言](#📝 前言)
-
[🔍 题目在考什么](#🔍 题目在考什么)
-
[📦 P1319:拆信封(游程解码)](#📦 P1319:拆信封(游程解码))
-
[💡 思路](#💡 思路)
-
[📝 伪代码](#📝 伪代码)
-
[🎯 关键点](#🎯 关键点)
-
-
[📨 P1320:装回去(游程编码)](#📨 P1320:装回去(游程编码))
-
[💡 思路](#💡 思路)
-
[📝 伪代码](#📝 伪代码)
-
[🎯 关键点](#🎯 关键点)
-
-
[⚖️ 正反对比](#⚖️ 正反对比)
-
[⚠️ 注意事项](#⚠️ 注意事项)
-
[🌳 延伸:从你的伪代码到 ZIP 文件------压缩技术的进化树](#🌳 延伸:从你的伪代码到 ZIP 文件——压缩技术的进化树)
-
[📊 看频率------哈夫曼编码(1952)](#📊 看频率——哈夫曼编码(1952))
-
[🔁 找重复------LZ 系列(1977---1984)](#🔁 找重复——LZ 系列(1977—1984))
-
[🔗 合体------DEFLATE(1991)与 ZIP](#🔗 合体——DEFLATE(1991)与 ZIP)
-
[🏷️ 领域专精------不同数据,不同打法](#🏷️ 领域专精——不同数据,不同打法)
-
[🚀 现代通用压缩------Brotli 与 Zstandard(2015)](#🚀 现代通用压缩——Brotli 与 Zstandard(2015))
-
[🔮 前沿------理解即压缩(2025)](#🔮 前沿——理解即压缩(2025))
-
[📅 进化时间线](#📅 进化时间线)
-
-
[📚 延伸阅读文献](#📚 延伸阅读文献)
📝 前言
这篇题解没有源代码,只有伪代码。
作为一名信奥教练,我不提倡复制粘贴。我见过太多学生搜到题解、复制、粘贴、提交、AC------代码跑通了,脑子没跑通。下次遇到变体题,还是不会。
伪代码剥掉了语言的壳,只留算法的骨架。你看不到 #include,看不到 cin、cout,看不到那些让你以为"我会了"的语法细节。你能看到的只有:这一步做什么、下一步做什么、为什么这么做。
如果你是路过的友友,已经在这道题上挣扎了很久------先去喝杯水,回来重新看看自己卡在哪一步。是没读懂题意?是思路方向偏了?还是代码有 bug 但逻辑其实对?大多数时候不是不会,是走偏了。偏了不可怕,可怕的是偏了之后直接放弃,去抄一份能 AC 的代码。抄完你以为你懂了,其实你只是搬了别人的结论。
除非你时间真的紧张------比赛临近、作业要交------那种情况先 AC 再说,能理解。但平时练习,给自己一点耐心。先自己想、自己写、自己调,跑不过了再来看伪代码:你的思路和这里差在哪一步。那一步,就是你真正学到的东西。
🔍 题目在考什么
压缩码的规则:第一个数是 N(矩阵边长),后面的数交替表示"连续几个 0"和"连续几个 1",从左到右、从上到下铺满 N×N 的格子。
比如压缩码 7 3 1 6 1 6 4 3 1 6 1 6 1 3 7,还原出来的点阵是:
text
0001000
0001000
0001111
0001000
0001000
0001000
1111111
-
P1319 :给你压缩码,让你还原点阵。方向是拆 ------把"连续计数"展开成原始字符序列。这叫游程解码(RLE Decoding)。
-
P1320 :给你点阵,让你输出压缩码。方向是装 ------把原始字符序列压缩成"连续计数"。这叫游程编码(RLE Encoding)。
同一套规则,一个往拆,一个往装。先从简单的方向入手。
📦 P1319:拆信封(游程解码)
💡 P1319 思路
这道题不需要复杂的算法,核心只有三件事。
第一,交替切换。 压缩码的规则决定了:第一个数一定是 0 的个数,第二个数一定是 1 的个数,第三个数又是 0......读完一个数之后,把当前字符翻转一下(0 变 1,1 变 0)。一个布尔变量搞定。
第二,换行控制。 矩阵是 N×N 的,每 N 个字符换一行。用一个计数器 total 记录已经输出了多少个字符,当 total 是 N 的整数倍时换行。换行跟着字符计数走,不跟着压缩数走------因为一个压缩数可能跨行。
第三,终止条件。 不用管压缩码有几个数------总共只需要输出 N×N 个字符。每读一个数、输出对应数量的字符,total 就增加。total 到了 N×N,循环自然结束。
📝 P1319 伪代码
text
读取整数 N // 第一个数:矩阵边长
total = 0 // 已输出字符计数
当前字符 = '0' // 从 0 开始交替
当 total < N × N:
读取一个整数 count // 连续多少个当前字符
重复 count 次:
输出 当前字符
total = total + 1
如果 total 能被 N 整除: // 凑满一行
输出换行
切换 当前字符 // 0 → 1,1 → 0
🎯 P1319 关键点
为什么不用提前知道有几个数。 压缩码的长度没有直接给出,但你不需要它------你知道总字符数是 N×N。每读一个数、输出对应数量的字符,total 就增加,到了 N×N 循环自然停。这个思路的精髓在于:用输出量而不是输入量控制循环。
换行跟着字符计数,不跟着压缩数。 一个压缩数可能跨行。比如 N=7,某个数是 10,它输出的 10 个字符会占满当前行的剩余位置和下一行的开头。换行必须跟着 total 走------每 N 个字符换一次,不管这些字符来自哪个压缩数。
交替切换的时机。 翻转发生在内层循环(重复 count 次)结束之后,不是在循环里面。同一个压缩数对应同一个字符,只有读下一个数时才切换。
用压缩码 7 3 1 6 ... 的前几个数追踪:
| 读取的数 | 当前字符 | 输出 | total | 切换后 |
|---|---|---|---|---|
| 7(N) | --- | --- | 0 | --- |
| 3 | '0' | 000 | 3 | '1' |
| 1 | '1' | 1 | 4 | '0' |
| 6 | '0' | 000000 | 10 | '1' |
| 1 | '1' | 1 | 11 | '0' |
total=7 时换行(第一行满了),total=14 时换行。一切跟着 total 走。
📨 P1320:装回去(游程编码)
💡 P1320 思路
反过来呢?给你点阵,让你压回压缩码。方向倒过来了,但难度不是对称的------装回去比拆开难。
难在三个地方:
-
N 没有直接给,要从第一行字符串长度推断
-
压缩码跨行连续------上一行末尾和下一行开头如果字符相同,属于同一个游程,不能断开
-
以 1 开头时要补 "0"------压缩码第一个数一定是 0 的个数,矩阵以 1 开头时没有前置的 0,得补一个 "0" 占位
核心机制是双计数器 + 延迟一步检测切换 :用 计数_0 和 计数_1 分别累加连续的 0 和 1,当某一方的计数恰好变成 1 时(意味着刚刚发生了字符切换),就输出另一方的累计值并清零。
为什么是"延迟一步"? 因为检测发生在处理下一个字符之前 ------此时 当前是零 还保留着上一字符的状态。切换发生在第 i 个字符,但检测在第 i+1 个字符时才触发。
具体来说,每次处理一个字符之前,先做两个检查:
-
检查 ① :
计数_1 == 1且上一字符是 1 且计数_0 > 0→ 刚从 0 切到 1,输出计数_0 -
检查 ② :
计数_0 == 1且上一字符是 0 且计数_1 > 0→ 刚从 1 切到 0,输出计数_1
最后一行结束后没有"下一个字符"来触发检测,剩余计数要手动收尾。
📝 P1320 伪代码
text
// ===== 初始化 =====
读取第一行 row
N = row 的长度
答案 = N + " " // 压缩码以 N 开头
当前是零 = true // 上一字符是否为 0
计数_0 = 0 // 当前游程中 0 的个数
计数_1 = 0 // 当前游程中 1 的个数
// 矩阵以 1 开头时,补一个 "0"(0 个零)
如果 row[0] == '1':
答案 += "0 "
当前是零 = false
// ===== 逐行扫描 =====
对第 r 行 (r = 1 到 N):
对该行第 i 个字符 ch (i = 0 到 N-1):
// ① 延迟检测:刚从 0 切到 1
如果 计数_1 == 1 且 当前是零 == false 且 计数_0 > 0:
答案 += 计数_0 + " "
计数_0 = 0
// ② 延迟检测:刚从 1 切到 0
如果 计数_0 == 1 且 当前是零 == true 且 计数_1 > 0:
答案 += 计数_1 + " "
计数_1 = 0
// ③ 更新当前字符状态
当前是零 = (ch == '0')
// ④ 累加对应计数
如果 当前是零:
计数_0 += 1
否则:
计数_1 += 1
如果 r < N:
读取下一行 row
// ===== 收尾:输出剩余计数 =====
// 输出顺序保持 0-count、1-count 交替
如果 当前是零: // 最后一段是 0
如果 计数_1 == 0: // 之前没有未输出的 1
答案 += 计数_0
否则: // 有未输出的 1,先输出 1 再输出 0
答案 += 计数_1 + " " + 计数_0
否则: // 最后一段是 1
如果 计数_0 == 0: // 之前没有未输出的 0
答案 += 计数_1
否则: // 有未输出的 0,先输出 0 再输出 1
答案 += 计数_0 + " " + 计数_1
输出 答案
🎯 P1320 关键点
延迟一步检测:用"计数恰好为 1"代替"字符变了"。 大多数人写游程编码会用"当前字符 ≠ 上一字符"来判断切换。这里换了个思路:用计数器的"0→1 跳变"来检测。好处是不需要额外存"上一字符是谁"------计数器从 0 变成 1 本身就是切换信号。代价是检测延迟一个字符。
用第一行 0001000 追踪,初始 计数_0=0, 计数_1=0, 当前是零=true:
| i | 字符 | ①检测 | ②检测 | ③当前是零 | ④计数变化 | 说明 |
|---|---|---|---|---|---|---|
| 0 | '0' | 否 | 否 | true | 计数_0=1 | 第一个 0 |
| 1 | '0' | 否 | 否 | true | 计数_0=2 | |
| 2 | '0' | 否 | 否 | true | 计数_0=3 | |
| 3 | '1' | 否(计数_1=0) | 否 | false | 计数_1=1 | 切换发生,但没检测到 |
| 4 | '0' | 是(输出"3 ") | 否 | true | 计数_0=1 | 延迟一步,输出之前的 0 计数 |
| 5 | '0' | 否 | 是(输出"1 ") | true | 计数_0=2 | 延迟一步,输出之前的 1 计数 |
| 6 | '0' | 否 | 否 | true | 计数_0=3 |
i=3 时字符从 0 切到 1,但检测在 i=4 才触发------此时 计数_1 恰好为 1,当前是零 还是 false,于是输出 计数_0=3。延迟一步,但结果正确------因为输出的是"之前那一整段的计数"。
跨行不重置计数器。 计数_0 和 计数_1 在行间不归零。第一行以 0 结尾、第二行以 0 开头时,它们属于同一个游程,计数继续累加。用样例前两行(都是 0001000)追踪:第一行结束时 计数_0=3,第二行开头的三个 0 继续累加到 计数_0=6,遇到 '1' 时才触发检测输出 "6 "。不跨行累加就会把一个游程错误地拆成两段。
以 1 开头时补 "0"。 压缩码第一个数一定是 0 的个数。矩阵以 1 开头时没有前置的 0,输出 "0" 占位告诉解压方"0 个零之后开始 1",保证交替顺序不错位。
最后一行收尾。 没有下一个字符来触发延迟检测了,剩余计数手动输出。原则是保持 0-count、1-count 的交替顺序:最后一段是 0 就先输出之前未输出的 1-count 再输出 0-count;最后一段是 1 就先输出之前未输出的 0-count 再输出 1-count。
用最后一行 1111111 追踪,进入时 计数_0=3, 计数_1=0, 当前是零=true:
| i | 字符 | ①检测 | ②检测 | ③当前是零 | ④计数变化 |
|---|---|---|---|---|---|
| 0 | '1' | 否 | 否 | false | 计数_1=1 |
| 1 | '1' | 是(输出"3 ",清零计数_0) | 否 | false | 计数_1=2 |
| 2--6 | '1'×5 | 否 | 否 | false | 计数_1=7 |
收尾:当前是零=false, 计数_0=0, 计数_1=7,走"最后一段是 1 且计数_0==0"分支,输出 7。最终答案 7 3 1 6 1 6 4 3 1 6 1 6 1 3 7,和期望一致。
⚖️ 正反对比
两道题做完了,放在一起看会发现一个有意思的不对称:
| P1319(解码) | P1320(编码) | |
|---|---|---|
| 方向 | 压缩码 → 点阵 | 点阵 → 压缩码 |
| N 的来源 | 压缩码第一个数直接给 | 第一行字符串长度推断 |
| 核心变量 | 一个布尔变量(当前字符) | 两个计数器 + 一个状态标记 |
| 切换检测 | 不需要检测------读完一个数直接翻转 | 延迟一步检测------靠"计数恰好为 1"判断 |
| 跨行处理 | 不需要关心------换行跟着 total 走 | 必须跨行累加------计数器不在行末重置 |
| 边界处理 | 几乎没有 | 以 1 开头补 "0"、最后一行手动收尾 |
| 难度 | 直来直去 | 绕了一圈 |
为什么拆比装简单? 因为拆的时候,压缩码已经告诉你每个游程有多长、是什么字符------你只需要照着展开。装的时候,你得自己发现游程的边界在哪里,而且游程可能跨行,你还得处理开头和结尾的边界情况。
这其实是信息论里的一个普遍规律:解码比编码简单,因为解码时信息已经显式给出了,编码时你得自己从原始数据里提取。 游程编码如此,哈夫曼编码如此,很多压缩算法都如此。
⚠️ 注意事项
-
数据范围 N≤200,总字符数最多 40000,O(N²) 的朴素做法没有任何性能压力,两题都不需要优化。
-
P1319 的差一错误 :
total从 0 开始、输出后递增,total % N == 0就是换行时机。如果把total从 1 开始,换行判断要相应调整------这是最常见的丢分点。 -
P1320 的行末空格 :代码在
!isZero 且 计数_0==0这个分支下会多输出一个尾部空格。洛谷评测机对行末空格宽容,能 AC,但如果追求格式严谨可以在输出前裁掉。 -
P1320 的跨行计数器 :
计数_0和计数_1在行间不重置。很多人会在每行开头重置,导致压缩码被错误拆分------这是 P1320 最容易踩的坑。 -
P1320 切换检测条件 里的
> 0判断保证了第一次切换不会误触发------因为还没有积累值可输出。
🌳 延伸:从你的伪代码到 ZIP 文件------压缩技术的进化树
你刚才手写了一遍游程编码。不管你意识到没有,你已经站在了压缩技术的起点上。
游程编码是最古老的压缩方法之一。1960 年代,传真机用它传输文件------一张白纸大部分是连续的白、一段连续的黑,RLE 完美适配。早期的 PCX 图像格式、Windows BMP 文件,也都用 RLE 压缩色块。你做的 P1320,本质上和半个世纪前的传真机干的是同一件事。
但 RLE 有一个明显的短板:它只对"连续重复"的数据有效。如果数据里没有长串相同值------比如一段英文文本,每个字符几乎都不同------RLE 不仅压缩不了,还会把数据变大。所以压缩技术从 RLE 出发,往两个方向进化了。
📊 看频率------哈夫曼编码(1952)
哈夫曼的思路是:出现频率高的字符用短编码,频率低的用长编码。就像摩尔斯电码里 "E" 是一个点(最短),"Q" 是 "−−·−"(最长)------因为 E 出现得最多。
如果 'e' 出现 100 次、'z' 出现 2 次,给 'e' 分配 3 位编码、给 'z' 分配 10 位,总长度比给每个字符都分配 8 位要短得多。哈夫曼编码至今还在用------JPEG 里有它,DEFLATE 里有它,几乎所有现代压缩算法的"最后一道工序"都是它。
🔁 找重复------LZ 系列(1977---1984)
Lempel 和 Ziv 在 1977 年提出 LZ77:不要逐个字符编码,而是把已经出现过的片段"指回去"。比如文本 "the quick brown the quick fox",第二次出现 "the quick" 时不重复编码,而是记一个指针------"回到前面第 N 个位置,抄 M 个字符"。这就像你写作文时用"同上"两个字代替抄一遍上一段------用更短的符号替代重复的内容。
1984 年 Welch 改进了 LZ77,提出 LZW,用一张"字典"记录所有见过的片段。GIF 图像格式就是用 LZW 压缩的。
🔗 合体------DEFLATE(1991)与 ZIP
1991 年,Phil Katz 把 LZ77 和哈夫曼编码组合在一起,创造了 DEFLATE 算法。先用 LZ77 找重复片段、做第一轮压缩,再用哈夫曼编码对结果做第二轮压缩。两层叠加,效果显著。
DEFLATE 成了 ZIP 文件的标准算法。你每天打开的 .zip 文件、浏览网页时服务器用 gzip 压缩的 HTML、PNG 图片里的像素数据------底层都是 DEFLATE。
从你的伪代码到 ZIP 文件,进化路径是:RLE → 找频率(哈夫曼)+ 找重复(LZ)→ 两层叠加(DEFLATE)。
🏷️ 领域专精------不同数据,不同打法
通用压缩算法什么数据都能压,但不是什么数据都压得好。各个领域发展出了自己的专精算法:
| 领域 | 代表算法 | 核心思路 |
|---|---|---|
| 文件归档 | ZIP(DEFLATE)、7z(LZMA) | LZ + 熵编码 |
| 静态图像 | JPEG(DCT 变换)、WebP、AVIF | 频域变换 + 量化 |
| 视频 | H.264 / H.265 / AV1 | 帧间预测 + 变换 + 熵编码 |
| 音频 | MP3、AAC、Opus | 心理声学模型 + 变换 |
| 无损音频 | FLAC | 线性预测 + 残差编码 |
视频压缩是这里面最复杂的。一秒 30 帧的视频,相邻帧之间大部分画面是相同的------第 2 帧和第 1 帧可能只差一个人眨了一下眼。H.264 用"帧间预测"把这个差异压到极致:不存完整的每一帧,只存"和上一帧相比变了什么"。这就是为什么一个 1080p 视频原始大小可能上百 GB,压完只要几百 MB。
🚀 现代通用压缩------Brotli 与 Zstandard(2015)
2015 年是通用压缩的一个大年,两个重要算法同年诞生。
Brotli (Google)专为 Web 流量优化,内置一张静态字典,对 HTML/CSS/JS 比 gzip 多压缩 15%---30% (Brotli on a CDN: Why It's Better and How to Configure It on Cloudflare)。Cloudflare 等 CDN 默认开启。
Zstandard (Facebook/Meta)在压缩率和速度之间灵活调节,解压速度可达 1550 MB/s,压缩率还能超过 zlib (Zstandard --- Fast real-time compression algorithm)。数据库(RocksDB、Cassandra、MongoDB)和实时系统大量采用。
两者的底层都用了一个 2009 年提出的新熵编码方法------ANS (非对称数字系统),由 Jarek Duda 设计并公开。ANS 的压缩率接近算术编码,但速度快 1.5---2 倍,而且无专利限制 (Compression Algorithms Deep Dive --- LZ77, Huffman, Arithmetic, ANS, Zstandard, Brotli)。Zstandard、AV1 视频编码器都集成了它。
🔮 前沿------理解即压缩(2025)
2025 年,鹏城实验室联合团队在《自然·机器智能》上发表了一项研究:用大语言模型压缩数据 (《自然·机器智能》刊发鹏城实验室"理解即压缩"理论)。核心思想是------模型"理解"了数据的规律,就能用更短的表示来编码它。这项名为 LMCompress 的方法在文本、图像、视频和音频四种媒体类型上都表现出了压倒性优势。
这听起来抽象,但原理和你的游程编码是一脉相承的:RLE 压缩的是"连续重复"这一种规律,哈夫曼压缩的是"频率不均"这一种规律,LZ 压缩的是"片段重现"这一种规律。大模型压缩的是------数据中所有可被语言描述的规律。
你能找到的规律越多,压缩率就越高。 这条线索从你手写的伪代码开始,一直延伸到 2025 年的前沿论文。
📅 进化时间线
| 年份 | 里程碑 | 压缩的规律 |
|---|---|---|
| 1960s | RLE(传真机) | 连续重复 |
| 1952 | 哈夫曼编码 | 字符频率不均 |
| 1977 | LZ77 | 片段重现 |
| 1984 | LZW(GIF) | 字典匹配 |
| 1991 | DEFLATE(ZIP/PNG/gzip) | LZ + 哈夫曼两层叠加 |
| 1994 | BWT(bzip2) | 可逆变换后更易压缩 |
| 1998 | LZMA(7z) | 更大窗口的 LZ + 区间编码 |
| 2009 | ANS | 无专利的高效熵编码 |
| 2015 | Brotli / Zstandard | Web 优化 / 通用高性能 |
| 2025 | LMCompress | 大模型理解数据规律 |
每一行都是在前一行的基础上"多找到了一种规律"。你今天做的洛谷题,是这张表的第一行。
📚 延伸阅读文献
论文与技术规范
- 洛谷. P1319 压缩技术 . https://www.luogu.com.cn/problem/P1319
- 洛谷. P1320 压缩技术(续集版) . https://www.luogu.com.cn/problem/P1320
- D. A. Huffman. A Method for the Construction of Minimum-Redundancy Codes . Proceedings of the IRE, 40(9):1098--1101, 1952. IEEE Technav
- J. Ziv, A. Lempel. A Universal Algorithm for Sequential Data Compression . IEEE Transactions on Information Theory, 23(3):337--343, 1977. IEEE Xplore
- T. A. Welch. A Technique for High-Performance Data Compression . Computer, 17(6):8--19, 1984. PDF
- P. Deutsch. DEFLATE Compressed Data Format Specification version 1.3 . RFC 1951, 1996. IETF Datatracker
- M. Burrows, D. J. Wheeler. A Block-sorting Lossless Data Compression Algorithm . SRC Research Report 124, 1994. Semantic Scholar
- J. Duda. Asymmetric Numeral Systems . arXiv:0902.0273, 2009. arXiv
- Google. Brotli Compressed Data Format . RFC 7932, 2016. IETF Datatracker
- Y. Collet, Facebook/Meta. Zstandard Compression and the application/zstd Media Type . RFC 8478, 2018. IETF Datatracker | GitHub
- 鹏城实验室. LMCompress: Large Language Model based Data Compression . Nature Machine Intelligence, 2025. 链接
推荐教材
-
K. Sayood. Introduction to Data Compression (5th Edition). Morgan Kaufmann, 2017. ------ 数据压缩领域最经典的入门教材,从 RLE 到现代算法都有覆盖。
-
T. M. Cover, J. A. Thomas. Elements of Information Theory (2nd Edition). Wiley, 2006. ------ 信息论圣经,理解"压缩率的信息论下界"为什么是这个值。
-
D. Salomon, G. Motta. Handbook of Data Compression (5th Edition). Springer, 2010. ------ 压缩算法百科全书,几乎所有已知压缩方法都有条目。
本文标签:#算法 #游程编码 #压缩技术 #洛谷题解 #信奥 #C++ #入门
本文首发于 CSDN,作者:HugoStudio_SWAN