洛谷 P1319 / P1320 压缩技术——同一枚硬币的正反面

洛谷 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,看不到 cincout,看不到那些让你以为"我会了"的语法细节。你能看到的只有:这一步做什么、下一步做什么、为什么这么做。

如果你是路过的友友,已经在这道题上挣扎了很久------先去喝杯水,回来重新看看自己卡在哪一步。是没读懂题意?是思路方向偏了?还是代码有 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 大模型理解数据规律

每一行都是在前一行的基础上"多找到了一种规律"。你今天做的洛谷题,是这张表的第一行。


📚 延伸阅读文献

论文与技术规范
  1. 洛谷. P1319 压缩技术 . https://www.luogu.com.cn/problem/P1319
  2. 洛谷. P1320 压缩技术(续集版) . https://www.luogu.com.cn/problem/P1320
  3. D. A. Huffman. A Method for the Construction of Minimum-Redundancy Codes . Proceedings of the IRE, 40(9):1098--1101, 1952. IEEE Technav
  4. J. Ziv, A. Lempel. A Universal Algorithm for Sequential Data Compression . IEEE Transactions on Information Theory, 23(3):337--343, 1977. IEEE Xplore
  5. T. A. Welch. A Technique for High-Performance Data Compression . Computer, 17(6):8--19, 1984. PDF
  6. P. Deutsch. DEFLATE Compressed Data Format Specification version 1.3 . RFC 1951, 1996. IETF Datatracker
  7. M. Burrows, D. J. Wheeler. A Block-sorting Lossless Data Compression Algorithm . SRC Research Report 124, 1994. Semantic Scholar
  8. J. Duda. Asymmetric Numeral Systems . arXiv:0902.0273, 2009. arXiv
  9. Google. Brotli Compressed Data Format . RFC 7932, 2016. IETF Datatracker
  10. Y. Collet, Facebook/Meta. Zstandard Compression and the application/zstd Media Type . RFC 8478, 2018. IETF Datatracker | GitHub
  11. 鹏城实验室. 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

相关推荐
小小晓.1 小时前
C++单例模式万字详解:6种实现演进+线程安全彻底解决+CRTP终极模板
c++·单例模式
λqaq71 小时前
MongoDB数据库基础 :非关系型数据库入门与常用操作
数据库·学习·mongodb·nosql
凯尔萨厮2 小时前
Java学习笔记十四(网络通信)
笔记·学习
AAA代码批发商2 小时前
DAYS 38 TCP并发服务器模型详解
linux·网络·笔记·学习
j7~2 小时前
【C++11】C++11 新特性全套详解(列表初始化、右值引用、lambda 表达式、function 与 bind 包装器等)
c++·右值引用·lambda表达式·可变参数模板·移动语义·包装器·c++11发展史
zyf1044162 小时前
暑期实践日志 Day46:复盘第六章,规整对应实践成果
学习·计算机网络·剪辑·暑期实践·课题任务
wdfk_prog2 小时前
canopennode-rtt推荐,不只可以做从站,也可以承担主站角色
c语言·开发语言·数据库·学习·算法·深度优先
无小道3 小时前
C/C++——可变参数
c语言·开发语言·c++·可变参数
BioRunYiXue3 小时前
科研干货 | IC50全面解读:概念解析、实验设计与数据分析要点
java·开发语言·javascript·人工智能·算法·数据挖掘·数据分析