【Huffman树】【贪心】【NOI 2015】【bzoj 4198】荷马史诗

本文解析了NOI2015荷马史诗问题,介绍了使用k进制串替换不同单词以最小化编码总长度的方法。通过构建k叉Huffman树实现了最优编码方案,并给出了具体实现代码。

4198: [Noi2015]荷马史诗

Time Limit: 10 Sec  Memory Limit: 512 MB
Submit: 127  Solved: 80

Description

追逐影子的人,自己就是影子。 ——荷马

Allison 最近迷上了文学。她喜欢在一个慵懒的午后,细细地品上一杯卡布奇诺,静静地阅读她爱不释手的《荷马史诗》。但是由《奥德赛》和《伊利亚特》组成的鸿篇巨制《荷马史诗》实在是太长了,Allison 想通过一种编码方式使得它变得短一些。

一部《荷马史诗》中有 n 种不同的单词,从 1 到 n 进行编号。其中第 i 种单词出现的总次数为 wi。Allison 想要用 k 进制串 si 来替换第 i 种单词,使得其满足如下要求:

对于任意的 1≤i,j≤n,i≠j,都有:si 不是 sj 的前缀。

现在 Allison 想要知道,如何选择 si,才能使替换以后得到的新的《荷马史诗》长度最小。在确保总长度最小的情况下,Allison 还想知道最长的 si 的最短长度是多少?

一个字符串被称为 k 进制字符串,当且仅当它的每个字符是 0 到 k−1 之间(包括 0 和 k−1)的整数。

字符串 Str1 被称为字符串 Str2 的前缀,当且仅当:存在 1≤t≤m,使得 Str1=Str2[1..t]。其中,m 是字符串 Str2 的长度,Str2[1..t] 表示 Str2 的前 t 个字符组成的字符串。

Input

输入文件的第 1 行包含 2 个正整数 n,k,中间用单个空格隔开,表示共有 n 种单词,需要使用 k 进制字符串进行替换。

接下来 n 行,第 i+1 行包含 1 个非负整数 wi,表示第 i 种单词的出现次数。

Output

输出文件包括 2 行。

第 1 行输出 1 个整数,为《荷马史诗》经过重新编码以后的最短长度。
第 2 行输出 1 个整数,为保证最短总长度的情况下,最长字符串 si 的最短长度。

Sample Input

4 2
1
1
2
2

Sample Output

12
2

HINT

用 X(k) 表示 X 是以 k 进制表示的字符串。

一种最优方案:令 00(2) 替换第 1 种单词,01(2) 替换第 2 种单词,10(2) 替换第 3 种单词,11(2) 替换第 4 种单词。在这种方案下,编码以后的最短长度为:

1×2+1×2+2×2+2×2=12
最长字符串 si 的长度为 2。

一种非最优方案:令 000(2) 替换第 1 种单词,001(2) 替换第 2 种单词,01(2) 替换第 3 种单词,1(2) 替换第 4 种单词。在这种方案下,编码以后的最短长度为:

1×3+1×3+2×2+2×1=12
最长字符串 si 的长度为 3。与最优方案相比,文章的长度相同,但是最长字符串的长度更长一些。

对于所有数据,保证 2≤n≤100000,2≤k≤9。
选手请注意使用 64 位整数进行输入输出、存储和计算。

题解:

如果没有想到Huffman就跪了。。
分析题目,发现出现次数其实就是权重,我们要求的就是一种编码方案使得权重×编码长和最小,可以直接套k叉Huffman树乱搞。
类比合并果子,考虑倒着进行编码。从叶子节点开始编码,每次取出最小k个数合成一个x,然后编码长加1(实际上是倒着的,但其实是一样的),再把x扔回去,以此类推。
注意:
可能会有空节点,即可能存在一个节点儿子不足k个,这就需要补零以防出错,常用的判断方法:(n-1)%(k-1),等于0代表是满k叉树。
(因为对于一棵满k叉树,任意节点要么有k个儿子,要么没有儿子,设叶子节点有x个,则(n-x)*k=n-1,推导一下即可)

Code:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<queue>
#include<algorithm>
using namespace std;
#define LL long long
struct H{
    LL w; int l;
    bool operator < (const H &a) const {
        if (w!=a.w) return a.w<w;
        return a.l<l;
    }
};
priority_queue<H> h;
int n,k,nn; LL ans=0;
int in(){
    int x=0; char ch=getchar();
    while (ch<'0' || ch>'9') ch=getchar();
    while (ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar();
    return x;
}
LL Lin(){
    LL x=0; char ch=getchar();
    while (ch<'0' || ch>'9') ch=getchar();
    while (ch>='0' && ch<='9') x=x*10+(LL)(ch-'0'),ch=getchar();
    return x;
}
int main(){
    n=in(),k=in(); nn=n;
    for (int i=1; i<=n; i++){
        LL x=Lin();
        h.push((H){x,1});
    }
    if ((n-1)%(k-1)) nn+=(k-1)-((n-1)%(k-1));
    for (int i=n+1; i<=nn; i++)
        h.push((H){0,1});
    while (nn>1){
        LL s1=0; int s2=0;
        for (int i=1; i<=k; i++){
            H x=h.top(); h.pop();
            s1+=x.w; s2=max(s2,x.l);
        }
        ans+=s1; nn-=(k-1);
        h.push((H){s1,s2+1});
    }
    printf("%lld\n%d\n",ans,h.top().l-1);
    return 0;
}
### Huffman及其贪心算法的实现与原理 #### 什么是HuffmanHuffman,也称为最优二叉,是一种带权路径长度最短的二叉。它的构建基于贪心算法的思想,在数据压缩领域有着广泛的应用。具体来说,Huffman通过对字符频率进行统计并按照权重从小到大依次合并的方式生成一棵二叉,使得高频字符对应较短的编码,低频字符对应较长的编码[^1]。 #### 贪心算法的核心思想 贪心算法在每一步选择中都采取当前状态下最优的选择,期望通过一系列局部最优解得到全局最优解。对于Huffman编码而言,其核心在于每次都选取两个频率最小的节点进行合并,形成新的父节点,并将其重新加入待处理集合中,直至所有节点被合并为一颗完整的二叉[^2]。 #### Huffman的具体构建过程 以下是Huffman的构建步骤: 1. **初始化**:根据输入字符串中的字符频率创建一个优先队列(通常是小顶堆),其中每个节点代表一个字符以及对应的频率。 2. **迭代合并**:从优先队列中取出频率最小的两个节点,将它们作为左右子节点创建一个新的内部节点,新节点的频率等于这两个节点频率之和。随后将该新节点放回优先队列中。 3. **终止条件**:重复上述操作,直到优先队列中只剩下一个节点为止,此时这唯一剩下的节点即为Huffman的根节点[^4]。 #### Python实现示例 下面是一个简单的Python程序,演示如何使用贪心算法构建Huffman并对字符进行编码: ```python import heapq from collections import defaultdict, namedtuple Node = namedtuple("Node", ["frequency", "char", "left", "right"]) def huffman_tree(string): frequency_map = defaultdict(int) for char in string: frequency_map[char] += 1 heap = [] for char, freq in frequency_map.items(): heapq.heappush(heap, Node(freq, char, None, None)) while len(heap) > 1: node1 = heapq.heappop(heap) node2 = heapq.heappop(heap) merged_node = Node(node1.frequency + node2.frequency, None, node1, node2) heapq.heappush(heap, merged_node) return heap[0] def generate_codes(node, current_code="", code_dict=None): if code_dict is None: code_dict = {} if node.char is not None: code_dict[node.char] = current_code or "0" else: generate_codes(node.left, current_code + "0", code_dict) generate_codes(node.right, current_code + "1", code_dict) return code_dict # 测试函数 if __name__ == "__main__": test_string = "this is an example of a huffman tree" root = huffman_tree(test_string) codes = generate_codes(root) print(codes) ``` 此代码实现了Huffman的构建及字符编码功能。首先统计输入字符串中各字符出现的频率,接着利用这些频率构造Huffman,最后遍历整棵以获取各个字符的二进制编码[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值