哈夫曼树以及哈夫曼编码
一、问题描述
构造一颗包含\(n\)个叶子节点的\(k\)叉树,其中第\(i\)个叶子节点带有权值\(w_i\),要求最小化\(\sum w_i*l_i\),其中\(l_i\)表示第\(i\)个叶子节点到根节点的距离。
二、算法描述
运用贪心的思想,权值大的叶子结点的深度一定要小。
先考虑\(k=2\)的情况:
我们不难想出一种贪心算法。
1.建立一个小根堆,插入这\(n\)个叶子节点的权值。
2.从堆中取出最小的两个权值\(w_1\)和\(w_2\),令\(ans+=w_1+w_2\)。
3.建立一个新的节点\(p\),权值为\(w_1+w_2\),令\(p\)成为取出的两个节点的父亲。
4.在堆中插入权值\(w_1+w_2\)。
5.重复上述\(2\)到\(4\)步,直到堆得大小为\(1\)。
Luogu P1090 合并果子
点击查看代码
import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Scanner;
/**
* @author dongyudeng
*/
public class BinaryHuffmanTree {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int num = scanner.nextInt();
ArrayList<Integer> values = new ArrayList<>();
for (int i = 0; i < num; i++) {
values.add(scanner.nextInt());
}
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(values);
Integer ans = 0;
while (priorityQueue.size() > 1) {
Integer value1 = priorityQueue.poll(), value2 = priorityQueue.poll();
ans += value1 + value2;
priorityQueue.add(value1 + value2);
}
System.out.println(ans);
}
}
接着再扩展到\(k>2\)的情况:
我们的第一个想法必定是套用上述贪心算法,只是改为每次从堆中取出最小的\(k\)个值。但是在最后一轮循环中,堆的大小在\(2~k-1\)之间,根节点的子节点数小于\(k\),显然不是最优解,因此我们补加一些权值为\(0\)的叶子节点,使得叶子节点的个数\(n\)满足\((n-1)\mod (k-1)=0\),这么做是为了让子节点数小于\(k\)的情况出现在最底层(权值为\(0\)的叶子结点显然是在最底层的),从而保证正确性。
注:
\((n-1)\mod (k-1)=0\)推导:
设度为\(k\)的节点数为\(m\),总节点数为\(l\)。
补了权值为\(0\)的节点后,除了叶子结点外的所有节点都满度,所以\(n+m=l\)。
又因为\(l-1=k*m\)(入边\(=\)出边),可得\(n-1=(k-1)m\),得证。
哈夫曼树的应用:哈夫曼编码
Luogu P2168 荷马史诗
本题所构建的即为哈夫曼编码,我们将\(n\)个值作为叶子结点的权值,求出哈夫曼树,将每个节点的出边标上\(0\)到\(k-1\),即可构成一颗\(trie\)树。
观察这颗\(trie\)树,单词\(i\)的编码为从根节点到叶子结点\(i\)的路径,又因为单词都是叶子结点,恰好满足了一个单词不是另一个的前缀。
此外,本题要求\(trie\)的深度最小,我们在合并时优先合并已合并次数最少的节点即可。
点击查看代码
import java.util.ArrayList;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.Scanner;
/**
* @author dongyudeng
* Luogu P2168 荷马史诗
*/
public class HuffmanTree {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int num = scanner.nextInt(), k = scanner.nextInt();
ArrayList<Long> values = new ArrayList<>();
for (int i = 0; i < num; i++) {
values.add(scanner.nextLong());
}
while ((num - 1) % (k - 1) != 0) {
values.add(0L);
num++;
}
PriorityQueue<TreeNode> priorityQueue = new PriorityQueue<>();
for (Long value : values) {
priorityQueue.add(new TreeNode(0, value));
}
long ans = 0;
while (priorityQueue.size() >= k) {
TreeNode[] nodes = new TreeNode[k];
for (int i = 0; i < k; i++) nodes[i] = priorityQueue.poll();
int maxCnt = 0;
long value = 0;
for (TreeNode node : nodes) {
value += node.getValue();
maxCnt = Math.max(maxCnt, node.getCnt());
}
ans += value;
priorityQueue.add(new TreeNode(maxCnt + 1, value));
}
System.out.println(ans);
int maxCnt = 0;
for (TreeNode node : priorityQueue) maxCnt = Math.max(maxCnt, node.getCnt());
System.out.println(maxCnt);
}
}
class TreeNode implements Comparable<TreeNode> {
private int cnt;
private long value;
public TreeNode(int cnt, long value) {
this.cnt = cnt;
this.value = value;
}
public int getCnt() {
return cnt;
}
public void setCnt(int cnt) {
this.cnt = cnt;
}
public long getValue() {
return value;
}
public void setValue(long value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TreeNode treeNode = (TreeNode) o;
return cnt == treeNode.cnt && value == treeNode.value;
}
@Override
public int hashCode() {
return Objects.hash(cnt, value);
}
@Override
public int compareTo(TreeNode o) {
//equal
if (this.value == o.value && this.cnt == o.cnt) return 0;
//less or greater
boolean isLess = this.value < o.value || (this.value == o.value && this.cnt < o.cnt);
if (isLess) return -1;
else return 1;
}
}
扩展:
关于Luogu P6033 合并果子(加强版):
桶排序+队列模拟(java过不了)。
代码地址:
码云