作业三:Huffman、搜索、动态规划与博弈
本次作业跨度很大:从真实文件压缩,到网格可达与区间覆盖、分层树形动态规划,再到 Nim 异或。共同点是先把题意压缩成一个结构清楚的数学模型。
1. Huffman 文件压缩实验
作业压缩程序的核心流程是:
- 统计每种符号出现频率;
- 用小根堆反复合并频率最小的两个结点;
- 从 Huffman 树生成前缀编码;
- 把编码位流打包成字节;
- 在压缩文件中保存解码所需的树或频率信息;
- 解压时逐位沿树行走,到叶子输出符号。
提交报告记录的示例从约 700 KB 压缩到约 420 KB,约为原大小的 60%。这个数字只能说明该样本的效果,不能当成 Huffman 对所有文件的固定压缩率。
压缩率为什么因文件而异
符号分布越不均匀,高频符号越能获得短码,收益越明显。若各字节频率接近,Huffman 码接近定长码;再加上码表头部,压缩文件甚至可能更大。
工程实现要补齐的边界
- 统计的是原始字节还是 Unicode 字符,编码端和解码端必须一致;
- 末字节的补零不能被误解为正文,要保存有效位数或原文长度;
- 空文件和只有一种符号的文件需要专门处理;
- 必须把码表、树或规范码信息写入文件,否则换一个进程无法解码;
- 计算压缩率要把头部也算进去。
Huffman 的理论最优性针对给定符号频率下的二进制前缀码,不代表它优于字典压缩、算术编码或针对图片音频的专用编码。
2. 引水入域:可达性加区间覆盖
题目给出 高度网格,水只能从高处流向相邻的严格低处。需要从第一行选择若干蓄水站,让最后一行全部可达;若做不到,输出不可达位置数量。
第一步:判断是否全可达
从第一行所有格子同时 DFS/BFS,沿严格下降边移动。统计最后一行未访问的格子。若有不可达点,直接输出失败和数量。
一次多源搜索复杂度为 。
第二步:每个源点对应底部区间
若底行全部可达,再分别从每个顶行位置搜索,得到它能到达的底行最左列 与最右列 。在该地形条件下,可达底部位置形成连续区间,于是问题变成:用最少区间覆盖 。
第三步:贪心覆盖
按左端点排序,设下一个待覆盖位置为 pos。在所有 的区间中,选择右端点最大的一个,把 pos 推到 。这是标准的最少区间覆盖贪心。
原程序为每个顶行格子单独 DFS,最坏约 。若数据更大,可以用动态规划直接计算每个格子的底部可达区间,避免重复搜索。
3. 树上选点:按深度分层的 DP
从提交程序可确定的约束是:每一层至多选择一个顶点,而且相邻两层所选顶点不能是父子;每个顶点有价值,目标最大化总价值。
先 BFS 求每个顶点深度并按层分组。处理到深度 时,状态有:
none[d]:第 层不选点的最大价值;choose[d][v]:第 层选择顶点 的最大价值。
转移为:
若对每个 都扫描上一层会平方级。程序维护上一层状态的最大值、次大值及最大值对应顶点:若最大值正好来自 ,改用次大值;否则直接用最大值。于是每个顶点只处理常数次,总时间 。
这个技巧适用于“从全集最大值中排除至多一个禁用候选”的转移。
4. 永夜的报应:Nim 异或
提交程序把所有输入数做按位异或:
这对应经典 Nim 博弈。若每个数表示一堆石子,玩家每次从一堆取走任意正数,最后无法行动者输,则:
- 异或和为 0:当前局面必败;
- 异或和非 0:当前局面必胜,并能一步把异或和变成 0。
证明关键是两点:从异或和 0 的状态走一步一定变成非 0;从非 0 状态总能找到一堆,把其最高差异位消掉并变成 0。
程序输出异或和本身,说明原题可能直接要求计算 Nim 和或以它作为答案,而不一定只输出胜负。这里保留源码能确认的算法,不猜测缺失题面中的输出文案。
5. 本次作业的共性
- Huffman:把频率变成加权树路径长度;
- 引水入域:把网格搜索结果变成区间覆盖;
- 树上选点:把树按深度压成相邻层状态;
- Nim:把整局博弈压成一个异或不变量。
算法设计的核心往往不是把代码写快,而是找到足以代表原问题的更小结构。