把ABABA变成两个可独立解码的文件
这个实验的终点是两份真正的字节文件。你应能从头读出它们的模型和长度信息,逐符号恢复同一个原文,最后解释为什么只有五字节的输入反而变成了25或30字节。另用一个LZ77回指写出七个字节,亲眼看见“刚写出的内容成为下一步的来源”。
下载原始五字节ABABA、完整HUF1文件、完整ARC1文件和Python标准库检查器。三个数据文件都由检查器中的编码函数实际生成;原文没有换行。脚本不访问网络,不调用压缩库,使用Python 3.10或更新版本即可运行:
python foundations-compression-stream-checker.py
python -O foundations-compression-stream-checker.py
两种运行均输出从状态算出的JSON轨迹与测试结果,失败会抛出异常;检查不依赖可被 -O 移除的assert。输出中的 emitted_delta 是这一步新发出的位,emitted_bits 是累计位数,依次串联增量可恢复全过程;flush后的完整有效码另列在 bits。
1. 先选阅读入口,再固定协议
只想先做Huffman部分,可从信源码、前缀码、Huffman编码到规范码长表。算术分支接理想区间与整数状态;自适应模型是完成静态文件后的扩展。最后统一检查完整封装和资源预算。
本实验的文件是自定义教学格式,不是DEFLATE。多字节整数字段均大端;payload每字节最高位先读。正文符号0至255,EOF为256;码表、频数表按符号编号升序。原文字节 41 42 41 42 41 对应A、B、A、B、A。两份文件都声明原长5,并恰好解出五字节后遇EOF。
HUF1固定头14字节:magic4、表项数2、原长4、有效位数4;每表项3字节:symbol2、length1。ARC1固定头15字节:magic4、精度1、表项数2、原长4、有效位数4;每项4字节:symbol2、frequency2。HUF1长度限1至15,唯一符号表只能是EOF长度1;ARC1精度只接受8或16,总频数不超过
2. 从码长表生成HUF1,再反向逐位读取
先把EOF计一次,频数为A3、B2、EOF1。Huffman合并1与2,再把所得3和A的3合并,长度是A1、B2、EOF2。规范首码递推给 A=0,B=10,EOF=11。
请先自己写出 0 | 10 | 0 | 10 | 0 | 11,再比对生成结果 010010011。有效码只有9位,装成字节要补7个零,所以payload为 49 80。
完整25字节十六进制是:
48 55 46 31 00 03 00 00 00 05 00 00 00 09
00 41 01 00 42 02 01 00 02
49 80
前四字节是HUF1,不属于码长表。第一个表项 00 41 01 表示编号65、长度1;EOF项 01 00 02 中的 01 00 是大端整数256。读有效位时,在第1、3、4、6、7位分别得到A、B、A、B、A,第9位到EOF。后面的7个零不得继续解为七个A。
现在改表做三个检查:长度 (1,1,1) 过订,必须在建表阶段拒绝;长度 (2,2) 虽不满,但可构成 00,01;重复符号编号即使不超Kraft容量也非法。空文件的唯一EOF必须码长1;单一正文字符仍需正文和EOF两叶,不能把重复次数藏在空码字里。
3. 算术流要连同E3和flush一起执行
固定频数A3、B2、EOF1,总量6。先用精确分数重复区间分割,得到:
A [0, 1/2)
B [1/4, 5/12)
A [1/4, 1/3)
B [7/24, 23/72)
A [7/24, 11/36)
EOF [131/432, 11/36)
终区间宽 0100110111 的整个小区间为
再换8位整数实现,初态闭区间 [0,255],四分点64、半点128。下表的“新增位”只计本符号归一化时实际发出的位:
| 符号 | 缩小后闭区间 | 归一化 | 归一化后区间 | 延迟u | 新增位 |
|---|---|---|---|---|---|
| A | [0,127] | E1 | [0,255] | 0 | 0 |
| B | [128,212] | E2 | [0,169] | 0 | 1 |
| A | [0,84] | E1 | [0,169] | 0 | 0 |
| B | [85,140] | E3 | [42,153] | 1 | 空 |
| A | [42,97] | E1 | [84,195] | 0 | 01 |
| EOF | [177,195] | E2,E3,E3 | [8,159] | 2 | 1 |
第四步没有输出,不是丢掉了一位,而是把它延期。第五步发0时,同时兑现相反的1。EOF处理完累计输出 010011;flush先将u从2加到3,因L=8<64,追加0与三个1,形成完整10位 0100110111。
文件再加8个零guard、6个字节填充零,payload为 4d c0 00。完整30字节文件是:
41 52 43 31 08 00 03 00 00 00 05 00 00 00 0a
00 41 00 03 00 42 00 02 01 00 00 01
4d c0 00
译码先取8位 01001101,V=77。每个符号缩区间前的 (L,H,V) 依次是 (0,255,77)、(0,255,155)、(0,169,55)、(0,169,110)、(42,153,92)、(84,195,184)。用
为什么8个guard足够?每次归一化贡献一个“已输出或待兑现”的位,若总轮数为r,flush后有效码长r+2;译码读到EOF至多需要w+r位,因此最多额外使用w−2位。这里保留w位零,让物理末尾可严格检查。删除文件的末字节后应该失败,不能把缺失字节自动视为无限零。
4. 让文件解码只依赖它自己
把下载的三个数据文件和检查器放到同一目录,可运行下面这段独立读取,不向解码函数额外传码表或频数:
from pathlib import Path
import runpy
c = runpy.run_path('foundations-compression-stream-checker.py')
source = Path('foundations-ababa.txt').read_bytes()
h = Path('foundations-ababa.huf1').read_bytes()
a = Path('foundations-ababa.arc1').read_bytes()
c['equal'](c['huf_decode'](h)[0], source, 'HUF1 file decode')
c['equal'](c['arc_decode'](a)[0], source, 'ARC1 file decode')
c['equal'](c['huf_encode'](source)[0], h, 'regenerated HUF1')
c['equal'](c['arc_encode'](source)[0], a, 'regenerated ARC1')
print(len(source), len(h), len(a)) # 5 25 30
检查器还有两种不同的逆向路径:Huffman参考实现枚举每种长度的空闲叶并沿树解码;算术参考实现直接枚举所有整数子区间,找出包含V的一项,不使用scaled反算公式。分别比较它们与主解码器,能发现某些共享正向公式掩盖的错误,但仍不是对任意长度输入的形式证明。
5. 把“压了多少”分成完整账本
五字节原文是40bit,HUF1是200bit,ARC1是240bit:本例都变大。
| 项目 | HUF1 | ARC1 |
|---|---|---|
| 固定头 | 112bit | 120bit |
| 模型表 | 72bit | 96bit |
| 有效码 | 9bit(正文7+EOF2) | 10bit(含EOF及flush) |
| 寄存器guard | 0 | 8bit |
| 字节填充 | 7bit | 6bit |
| 总量 | 200bit | 240bit |
ABABA正文经验分布的熵约0.970951bit/字节,乘5约4.85475bit;这不含文件模型描述,也不含本例额外的EOF。算术实际采用A1/2、B1/3、EOF1/6,对包含EOF的整串给出
无噪声编码定理的平均或渐近量词保留在旧页。一个完整无损编码器不可能把全部n位串都缩短:长度小于n的bit串总共只有
迁移题:多长的全A文件才开始省空间?
令正文为
答案:表中只有A和EOF,两者码长都为1,所以有效位数
6. 模型同步、LZ回指与最后一道预算门
自适应扩展从A、B、EOF各1次开始,阈值7,先编码再更新,总量达到7时每项向上折半。ABABA前使用的表为 (1,1,1),(2,1,1),(2,2,1),(3,2,1),(2,2,1);EOF使用 (3,2,1)。输出11位 00101111101。这段实验显式传入初态与阈值,不能用静态ARC1解码器猜测自适应模式。
随后用LZ77解析九字节ABABABABA:Lit(A), Lit(B), Match(7,2)。先写A、B,再依次从位置0、1、2、3、4、5、6复制到2、3、4、5、6、7、8。第三次复制所读的A是第一次复制刚写出的,重叠是合法语义。用复制前的历史快照AB无法一次取出七字节。
再用LZ78处理ABABA,得到 (0,A),(0,B),(1,B),(1,END)。最后一个旧短语A必须保留;对ABABABA则是 (0,A),(0,B),(1,B),(3,A),(0,END)。END是本教学token类型,原始论文的定长块边界采用另一约定。
按每token1步、每输出字节1步,LZ77上述三token应付12步。将输出预算设为8,匹配开始前剩6字节,应先拒绝长度7;将匹配长度改成
检查覆盖与自行变更
作者检查器实际覆盖511个长度0至8的二元串:两种完整文件往返、独立逆解、LZ77与LZ78恢复;340张小码长表对照Kraft条件与枚举叶;255个短自适应串及2000符号的反复缩放;另有全256字节字母表、零字节/FF、长单字母与非法输入。负测包含逐字节截断、尾随数据、过订/重复/缺EOF码表、非零guard/padding、频数超界、无效回指、巨量长度、字典和输出预算。JSON给出实际计数。
建议先改一个字节,再改频数表和阈值,观察是哪层不变量变化。有限穷举支持实现复算;对任意合法输入的结论来自规范码构造、整数区间、同步模型和向前写入归纳。压缩格式检查也不保证内容未被篡改:合法文件仍可能被替换成另一份合法文件。
BWT和FM-index保留原来的可逆变换与压缩索引接口。BWT本身不减小字符数,FM-index还承担查询与采样成本;它们不是本实验中缺失的“另一份完整bitstream”。可靠信道、零错误通信和多终端压缩是不同的后续路线,本单元不取代那些定理。