题目描述
静态霍夫曼编码是一种主要用于文本压缩的编码算法。给定一个由NNN种不同字符组成的文本,该算法会构建一棵二叉哈夫曼树,为每个字符分配一个二进制编码。编码长度等于从根节点到对应叶节点的路径长度(边数)。
现在的问题是:已知霍夫曼算法为NNN个不同字符生成的编码长度L1,L2,…,LNL_1, L_2, \ldots, L_NL1,L2,…,LN,求文本可能的最小大小(即所有字符出现次数之和的最小值)。
输入格式
- 第一行:整数NNN(2≤N≤502 \leq N \leq 502≤N≤50),表示不同字符的数量
- 第二行:NNN个整数LiL_iLi(1≤Li≤501 \leq L_i \leq 501≤Li≤50),表示每个字符的编码长度
- 保证至少存在一棵霍夫曼树能够产生这些编码长度
输出格式
- 对于每个测试用例,输出一个整数,表示文本可能的最小大小
题目分析与解题思路
霍夫曼编码的基本原理
霍夫曼编码通过以下步骤构建:
- 为每个字符创建一个叶节点,权重等于该字符在文本中出现的次数
- 重复以下操作直到只剩一棵树:
- 选择两棵权重最小的树
- 将它们合并为一棵新树,新树的权重为两子树权重之和
- 最终树的权重就是文本总大小
逆向问题的挑战
本题是霍夫曼编码的逆向问题:已知编码长度,求最小文本大小。这比标准霍夫曼编码问题更加困难,因为:
- 编码长度与权重的关系:在霍夫曼树中,深度越大的节点(编码越长)通常权重越小
- 最小化目标:我们需要找到一组权重分配,使得:
- 每个深度的叶节点数符合给定的编码长度分布
- 总权重(文本大小)最小
- 满足霍夫曼树的构建规则
关键观察与算法设计
观察1:权重分配策略
为了最小化总权重,我们应该:
- 为深度最大的叶节点分配最小的权重(111)
- 确保父节点的权重不小于子节点的权重
- 按深度从大到小处理节点
观察2:分层处理
霍夫曼树的构建可以看作分层合并的过程:
- 深度ddd的节点由深度d+1d+1d+1的节点合并而来
- 每层的节点数必须是偶数(因为每次合并消耗222个节点)
- 处理完深度ddd后,所有节点合并为深度d−1d-1d−1的节点
观察3:权重更新规则
设当前处理的深度为ddd,我们需要:
- 为该深度的叶节点分配当前最小可用权重
- 将所有节点(包括叶节点和下层合并上来的内部节点)两两合并
- 更新最小可用权重为当前层节点的最大权重
- 深度减111,处理下一层
算法步骤
初始化
- 将编码长度按降序排序
- 设置当前深度为最大编码长度
- 设置最小可用权重为111
- 创建空列表
merged存放合并后的节点
分层处理
while 还有叶节点未处理: 创建优先队列 pq(最小堆) 将 merged 中的所有节点加入 pq 处理当前深度的所有叶节点: 为每个叶节点分配当前最小权重 weight 加入 pq 从叶节点列表中删除 清空 merged 列表 合并 pq 中的所有节点: while pq 不为空: 取出两个最小权重的节点 w1, w2 合并为新节点 w1 + w2 将新节点加入 merged 更新 weight = max(weight, max(w1, w2)) 深度减1计算结果
- 最终
merged中的节点权重之和就是最小文本大小
- 最终
算法正确性证明
偶数性保证
在霍夫曼树构建中,每层节点数必须是偶数,因为:
- 每个内部节点都有两个子节点
- 节点合并总是两两进行
- 因此算法中的
while (pq.size())循环总是能取到两个节点
权重单调性
通过weight = max(weight, max(w1, w2))的更新规则,确保:
- 下一层分配的权重不小于当前层任何节点的权重
- 父节点的权重总是大于子节点的权重
- 满足霍夫曼树的权重关系
最小性证明
通过贪心策略:
- 从最大深度开始,分配最小权重(111)
- 每次合并后,更新最小可用权重
- 为较浅深度的叶节点分配尽可能小的权重
- 这保证了总权重的最小性
时间复杂度分析
- 排序:O(NlogN)O(N \log N)O(NlogN)
- 外层循环:最多505050层(最大深度)
- 内层操作:优先队列操作O(logN)O(\log N)O(logN)
- 总复杂度:O(D⋅NlogN)O(D \cdot N \log N)O(D⋅NlogN),其中DDD为最大深度
对于N≤50,D≤50N \leq 50, D \leq 50N≤50,D≤50,完全可行。
示例分析
示例1
输入: 2 1 1 输出: 2解释:两个字符编码长度都为111,每个至少出现111次,最小总大小为222。
示例2
输入: 4 2 2 2 2 输出: 4解释:四个字符编码长度都为222,每个至少出现111次,最小总大小为444。
示例3
输入: 10 8 2 4 7 5 1 6 9 3 9 输出: 89代码实现
// Inverting Huffman// UVa ID: 12676// Verdict: Accepted// Submission Date: 2025-12-17// UVa Run Time: 0.000s//// 版权所有(C)2025,邱秋。metaphysis # yeah dot net#include<bits/stdc++.h>usingnamespacestd;intmain(){intn;while(cin>>n){vector<int>L(n);for(inti=0;i<n;i++)cin>>L[i];sort(L.begin(),L.end(),greater<int>());longlongweight=1;vector<longlong>merged;intdepth=L.front();while(L.size()){priority_queue<longlong,vector<longlong>,greater<longlong>>pq;for(autom:merged)pq.push(m);while(L.size()&&L.front()==depth){pq.push(weight);L.erase(L.begin());}merged.clear();while(pq.size()){longlongw1=pq.top();pq.pop();longlongw2=pq.top();pq.pop();weight=max(weight,max(w1,w2));merged.push_back(w1+w2);}depth--;}cout<<accumulate(merged.begin(),merged.end(),0LL)<<'\n';}return0;}代码要点说明
- 变量命名:使用驼峰命名法,变量名清晰表达含义
- 数据结构:
vector<int> L:存储编码长度priority_queue<long long, ...>:最小堆,管理节点权重vector<long long> merged:存储合并后的节点
- 关键操作:
sort(L.begin(), L.end(), greater<int>()):降序排序,从最大深度开始处理weight = max(weight, max(w1, w2)):更新最小可用权重accumulate(merged.begin(), merged.end(), 0LL):计算总权重
- 注意事项:
- 使用
long long防止权重和溢出 - 深度从最大编码长度递减处理
- 每层节点两两合并,确保霍夫曼树性质
- 使用
总结
本题是一道经典的逆向霍夫曼编码问题,考察对霍夫曼树构建过程的深入理解。解题的关键在于:
- 理解霍夫曼树的分层构建过程
- 掌握权重分配的最小化策略
- 设计合适的贪心算法模拟构建过程
通过从最大深度开始,为叶节点分配最小权重,并逐层合并节点,我们能够在O(D⋅NlogN)O(D \cdot N \log N)O(D⋅NlogN)的时间内找到最小文本大小。算法既保证了正确性,又具有较高的效率。
这种逆向思维训练对于深入理解数据压缩算法和贪心策略设计都具有重要意义。