题目描述
原题链接:149.荷马史诗
解题思路
1.模型抽象
题目中要求编码长度最短和最长的编码的长度最短,也就是要求哈夫曼树的权值之和以及最深的深度最短。
2.二进制哈夫曼树到k进制哈夫曼树
在k进制中,如果依然按照二进制哈夫曼树的合并规则,先合并最小的k个节点,那么一直合并到了不足k个节点的时候,最后一次合并就会出现缺漏,这时的k叉哈夫曼树就不是最优哈夫曼树。
以三进制哈夫曼树为例:
上图是数组{1,1,1,1,1,1}
构成的两种哈夫曼树,可以看出,上面的哈夫曼树的权值明显大于下面的。这是因为我们可以把上面的哈夫曼树最下面的一个节点移到最上面未满三个节点的地方。这样就变成了下面的哈夫曼树了。
那么,如何算出节点是否多余呢?
一种直观的感觉是将
n
n
n个节点一直减去
k
−
1
k-1
k−1,直到最后剩余1。为什么要减去
k
−
1
k-1
k−1呢?这是因为我们每次合并
k
k
k个节点之后就少了
k
−
1
k-1
k−1个节点。
以上描述转化为数学公式就是:
n
−
m
(
k
−
1
)
=
1
n-m(k-1)=1
n−m(k−1)=1,也就是
n
−
1
≡
k
−
1
n-1 \equiv k-1
n−1≡k−1。
假如说
n
−
1
n-1
n−1模上
k
−
1
k-1
k−1为0,那么也就是说此时我每次合并的时候都能合并k个点,这个时候状态是最好的。但是,我们往往不能够得到这种状态,也就是
(
n
−
1
)
%
(
k
−
1
)
≠
0
(n-1) \% (k-1) \ne 0
(n−1)%(k−1)=0
如果我们要是想解决这种问题,有两种处理方法:
(1) 我们可以先将多余的节点合并,也就是先将
(
n
−
1
)
%
(
k
−
1
)
(n-1)\%(k-1)
(n−1)%(k−1)个节点合并。这样剩余的每次合并都能合并
k
k
k个点了。
(2) 我们可以将不够的节点用0补上,具体来说,我们需要补全
k
−
1
−
(
n
−
1
)
%
(
k
−
1
)
k-1-(n-1)\%(k-1)
k−1−(n−1)%(k−1)个点。
3.最长编码最短
当我们在构造哈夫曼树时,总会遇到以上两种情况,明明权值之和相同,但是构造出来的哈夫曼树的最长深度却不同。这是因为我们在合并
k
k
k个节点的时候没有考虑到优先合并子树深度小的节点。
我们要想解决上述的问题,就需要在用pair
存储哈夫曼树节点的权值和子树的深度。
而且优先队列的排序方式要按照先比较权值,权值小的排前面,权值相同的比较子树的深度。
C++代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
typedef pair<ll, int> PLI;
int n,k;
priority_queue<PLI, vector<PLI>, greater<PLI>> q;
int main()
{
cin >> n >> k;
for(int i = 1; i <= n; i++)
{
ll x;
cin >> x;
q.push({x,0});
}
while((n-1) % (k-1) != 0)
{
n++;
q.push({0, 0});
}
ll res = 0;
while(q.size() > 1)
{
ll t = 0;
int depth = 0;
for(int i = 1; i <= k; i++)
{
auto it = q.top();
q.pop();
t += it.first;
depth = max(depth, it.second); // 求这k个子节点的最深深度
}
q.push({t, depth+1}); // k个子节点合并成一个父节点之后深度+1
res += t;
}
cout << res << endl << q.top().second;
}