7-3 哈夫曼树与哈夫曼编码

分数30
原题链接:PTA
题目描述:
哈夫曼树(Huffman Tree)又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的路径长度是从树根到每一结点的路径长度之和,记为WPL=(W1L1+W2L2+W3L3+…+WnLn),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明哈夫曼树的WPL是最小的。

在数据通信中,需要将传送的文字转换成二进制的字符串,用0,1码的不同排列来表示字符。例如,需传送的报文为“AFTER DATA EAR ARE ART AREA”,这里用到的字符集为“A,E,R,T,F,D”,各字母出现的次数为{8,4,5,3,1,1}。现要求为这些字母设计编码。要区别6个字母,最简单的二进制编码方式是等长编码,固定采用3位二进制,可分别用000、001、010、011、100、101对“A,E,R,T,F,D”进行编码发送,当对方接收报文时再按照三位一分进行译码。显然编码的长度取决报文中不同字符的个数。若报文中可能出现26个不同字符,则固定编码长度为5。然而,传送报文时总是希望总长度尽可能短。在实际应用中,各个字符的出现频度或使用次数是不相同的,如A、B、C的使用频率远远高于X、Y、Z,自然会想到设计编码时,让使用频率高的用短码,使用频率低的用长码,以优化整个报文编码。

为使不等长编码为前缀编码(即要求一个字符的编码不能是另一个字符编码的前缀),可用字符集中的每个字符作为叶子结点生成一棵编码二叉树,为了获得传送报文的最短长度,可将每个字符的出现频率作为字符结点的权值赋予该结点上,显然字使用频率越小权值越小,权值越小叶子就越靠下,于是频率小编码长,频率高编码短,这样就保证了此树的最小带权路径长度效果上就是传送报文的最短长度。因此,求传送报文的最短长度问题转化为求由字符集中的所有字符作为叶子结点,由字符出现频率作为其权值所产生的哈夫曼树的问题。利用哈夫曼树来设计二进制的前缀编码,既满足前缀编码的条件,又保证报文编码总长最短。

本题要求从键盘输入若干电文所用符号及其出现的频率,然后构造哈夫曼树,从而输出哈夫曼编码。

注意:
为了保证得到唯一的哈夫曼树,本题规定在构造哈夫曼树时,左孩子结点权值不大于右孩子结点权值。如权值相等,则先选优先级队列中先出队的节点作为左孩子。编码时,左分支取“0”,右分支取“1”。

输入格式:
输入有3行。
第1行:符号个数n(2~20)。

第2行:一个不含空格的字符串。记录着本题的符号表。我们约定符号都是单个的小写英文字母,且从字符‘a’开始顺序出现。也就是说,如果 n 为 2 ,则符号表为 ab ;如果 n 为 6,则符号为 abcdef;以此类推。

第3行:各符号出现频率(用乘以100后的整数),用空格分隔。

输出格式:
先输出构造的哈夫曼树带权路径长度。
接下来输出n行,每行是一个字符和该字符对应的哈夫曼编码。字符按字典顺序输出。

字符和哈夫曼编码之间以冒号分隔。

例如:

a:10

b:110

输入样例:
在这里给出一组输入。

6
abcdef
15 19 10 6 38 12
输出样例:
在这里给出相应的输出。

240
a:101
b:111
c:1101
d:1100
e:0
f:100
提示:
以上示例数据,按题目要求建立的Huffman Tree如下图:

解法没啥好说的,优先队列贪心构建哈夫曼树,单纯就是规则不清,够恶心直接上代码

/**
 * 大聪明写不清楚题意
 * 题意错了,相等情况下先出堆得作为右子树
 * 整整交了三页
 * `(*>﹏<*)′
 */
#include <bits/stdc++.h>

using namespace std;
using ll = long long;
const int N = 100;

struct node {
    char c;
    int hd, val, l, r;

    bool operator<(const node &t) const {
        if (t.val == val) return c < t.c; //别问,问就是玄学
        return val > t.val;
    }
} tree[N];

priority_queue<node> q;
int n, tot, v;
string s;

void add(int val, int l = 0, int r = 0, char c = 0) {
    ++tot;
    tree[tot] = {c, tot, val, l, r};
    q.push(tree[tot]);
}

ll build() {
    ll sum = 0;
    while (q.size() >= 2) {
        auto h1 = q.top();
        q.pop();
        auto h2 = q.top();
        q.pop();
        if (h1.val == h2.val) swap(h1, h2); //别问,问就是玄学
        sum += h1.val + h2.val;
        add(h1.val + h2.val, h1.hd, h2.hd, h1.c + h2.c);
    }
    return sum;
}

map<char, string> res;

void dfs(int hd, string path) {
    if (tree[hd].c >= 'a' && tree[hd].c <= 'z') {
        res[tree[hd].c] = path;
        return;
    }
    dfs(tree[hd].l, path + '0');
    dfs(tree[hd].r, path + '1');
}

int main() {
    cin >> n >> s;
    for (int i = 1; i <= n; i++) {
        cin >> v;
        add(v, 0, 0, s[i - 1]);
    }
    cout << build() << "\n";
    dfs(tot, "");
    sort(s.begin(), s.end());
    for (auto c: s)
        cout << c << ":" << res[c] << "\n";


    return 0;
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐