csp信奥赛C++高频考点专项训练之贪心算法 --【哈夫曼贪心】:荷马史诗

csp信奥赛C++高频考点专项训练之贪心算法 --【哈夫曼贪心】:荷马史诗

题目背景

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

题目描述

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

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

对于任意的 1 ≤ i , j ≤ n 1\leq i, j\leq n 1≤i,j≤n , i ≠ j i\ne j i=j ,都有: s i s_i si 不是 s j s_j sj 的前缀。

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

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

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

输入格式

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

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

输出格式

输出包括 2 2 2 行。

第 1 1 1 行输出 1 1 1 个整数,为《荷马史诗》经过重新编码以后的最短长度。

第 2 2 2 行输出 1 1 1 个整数,为保证最短总长度的情况下,最长字符串 s i s_i si 的最短长度。

输入输出样例 1
输入 1
复制代码
4 2
1
1
2
2
输出 1
复制代码
12
2
输入输出样例 2
输入 2
复制代码
6 3
1
1
3
3
9
9
输出 #2
复制代码
36
3
说明/提示
【样例解释】
样例 1 解释

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

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

1 × 2 + 1 × 2 + 2 × 2 + 2 × 2 = 12 1 × 2 + 1 × 2 + 2 × 2 + 2 × 2 = 12 1×2+1×2+2×2+2×2=12

最长字符串 s i s_i si 的长度为 2 2 2 。

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

1 × 3 + 1 × 3 + 2 × 2 + 2 × 1 = 12 1 × 3 + 1 × 3 + 2 × 2 + 2 × 1 = 12 1×3+1×3+2×2+2×1=12

最长字符串 s i s_i si 的长度为 3 3 3 。与最优方案相比,文章的长度相同,但是最长字符串的长度更长一些。

样例 2 解释

一种最优方案:令 000 ( 3 ) 000(3) 000(3) 替换第 1 1 1 种单词, 001 ( 3 ) 001(3) 001(3) 替换第 2 2 2 种单词, 01 ( 3 ) 01(3) 01(3) 替换第 3 3 3 种单词, 02 ( 3 ) 02(3) 02(3) 替换第 4 4 4 种单词, 1 ( 3 ) 1(3) 1(3) 替换第 5 5 5 种单词, 2 ( 3 ) 2(3) 2(3) 替换第 6 6 6 种单词。

【数据规模与约定】

所有测试数据的范围和特点如下表所示(所有数据均满足 0 < w i ≤ 10 11 0 < w_i \leq 10^{11} 0<wi≤1011):

测试点编号 n n n 的规模 k k k 的规模 备注
1 1 1 n = 3 n=3 n=3 k = 2 k=2 k=2
2 2 2 n = 5 n=5 n=5 ^ ^
3 3 3 n = 16 n=16 n=16 ^ 所有 w i w_i wi 均相等
4 4 4 n = 1   000 n=1\,000 n=1000 ^ w i w_i wi 在取值范围内均匀随机
5 5 5 ^ ^
6 6 6 n = 100   000 n=100\,000 n=100000 ^ ^
7 7 7 ^ ^ 所有 w i w_i wi 均相等
8 8 8 ^ ^
9 9 9 n = 7 n=7 n=7 k = 3 k=3 k=3 ^
10 10 10 n = 16 n=16 n=16 ^ 所有 w i w_i wi 均相等
11 11 11 n = 1   001 n=1\,001 n=1001 ^ ^
12 12 12 n = 99   999 n=99\,999 n=99999 k = 4 k=4 k=4 ^
13 13 13 n = 100   000 n=100\,000 n=100000 ^ ^
14 14 14 ^ ^ ^
15 15 15 n = 1   000 n=1\,000 n=1000 k = 5 k=5 k=5 ^
16 16 16 n = 100   000 n=100\,000 n=100000 k = 7 k=7 k=7 w i w_i wi 在取值范围内均匀随机
17 17 17 ^ ^
18 18 18 ^ k = 8 k=8 k=8 w i w_i wi 在取值范围内均匀随机
19 19 19 ^ k = 9 k=9 k=9
20 20 20 ^ ^ ^
【提示】

选手请注意使用 64 位整数进行输入输出、存储和计算。

【评分方式】

对于每个测试点:

  • 若输出文件的第 1 1 1 行正确,得到该测试点 40 % 40\% 40% 的分数;
  • 若输出文件完全正确,得到该测试点 100 % 100\% 100% 的分数。

思路分析

本题要求构造一棵 k k k 叉 Huffman 树,使得所有叶子节点的带权路径长度之和最小,并在总长度最小的前提下让树的最大深度(最长编码长度)尽可能小。

核心步骤
  1. 补全虚节点

    对于 k k k 叉 Huffman 树,每次合并 k k k 个节点后总节点数减少 k − 1 k-1 k−1 个。从 n n n 个叶子节点开始,要合并到只剩 1 个根节点,需要减少 n − 1 n-1 n−1 个节点,因此必须满足 ( n − 1 )   m o d   ( k − 1 ) = 0 (n-1) \bmod (k-1) = 0 (n−1)mod(k−1)=0。

    若不满足,则添加若干个权值为 0 0 0 的虚叶子节点,使得等式成立。虚节点不改变总代价,但能让树更"满",有助于降低最大深度。

  2. 合并过程

    使用小根堆存储节点,节点包含两个属性:权值 w w w 和当前子树的最大深度 d d d。

    排序规则:先按权值升序,权值相同时按深度升序(深度小的先合并,使树更平衡)。

    每次从堆中取出 k k k 个节点,合并为一个新节点:

    • 新节点权值 = 这 k k k 个节点的权值之和。
    • 新节点深度 = 这 k k k 个节点的最大深度 + 1。
    • 总代价累加新节点的权值(这等价于所有叶子节点权值 × 深度的累加)。
      将新节点放回堆中,重复直到只剩一个节点。
  3. 输出结果

    • 第一行:累加的总代价。
    • 第二行:最终根节点的深度(即最长编码长度)。
复杂度
  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),堆操作每次 O ( log ⁡ n ) O(\log n) O(logn),共 O ( n ) O(n) O(n) 次。
  • 空间复杂度: O ( n ) O(n) O(n)。

代码实现

cpp 复制代码
#include<bits/stdc++.h> 
using namespace std;

typedef long long ll; 

// 节点结构体:权值 w,当前子树最大深度 d
struct Node {
    ll w;   // 权值
    int d;  // 深度
    // 重载小于运算符,用于小根堆(先按权值,权值相同按深度)
    bool operator<(const Node& o) const {
        if (w != o.w) return w > o.w; // 权值小的优先
        return d > o.d;               // 深度小的优先
    }
};

int main() {
    int n, k;
    cin >> n >> k;
    
    priority_queue<Node> pq; // 小根堆(通过重载 < 实现)
    
    // 读入 n 个叶子节点,深度均为 0
    for (int i = 0; i < n; ++i) {
        ll w;
        cin >> w;
        pq.push({w, 0});
    }
    
    // 补充虚节点,使 (n-1) % (k-1) == 0
    // 注意:当 k=1 时除数为0,但题目保证 k>=2,所以安全
    if ((n - 1) % (k - 1) != 0) {
        int need = (k - 1) - (n - 1) % (k - 1);
        for (int i = 0; i < need; ++i) {
            pq.push({0, 0}); // 权值为0,深度为0的虚节点
        }
    }
    
    ll ans = 0; // 总代价
    // 当堆中不止一个节点时,继续合并
    while (pq.size() > 1) {
        ll sum_w = 0;   // 本次合并的权值和
        int max_d = 0;  // 本次合并的最大深度
        
        // 每次取出 k 个节点(不足 k 个就全部取出,但补全后必然有 k 个)
        for (int i = 0; i < k; ++i) {
            Node cur = pq.top(); pq.pop();
            sum_w += cur.w;
            max_d = max(max_d, cur.d);
        }
        
        // 合并后新节点的深度 = 最大深度 + 1
        ans += sum_w;                 // 累加总代价
        pq.push({sum_w, max_d + 1}); // 新节点入堆
    }
    
    // 最终堆中剩下的唯一节点,其深度就是最长编码长度
    cout << ans << endl << pq.top().d << endl;
    
    return 0;
}

功能分析

  1. 输入处理

    读取单词种数 n n n 和进制 k k k,以及每种单词的出现次数 w i w_i wi。

  2. 虚节点补全

    计算需要补充的虚节点个数,确保后续每次合并都能恰好取出 k k k 个节点,从而构造出满 k k k 叉 Huffman 树。

  3. Huffman 合并

    • 使用小根堆维护当前所有节点,按 (权值, 深度) 升序排列。
    • 每次取出最小的 k k k 个节点,合并后重新入堆,同时累加合并后的权值和到答案中(该累加和等于所有叶子带权路径长度之和)。
    • 记录合并过程中的最大深度,用于输出最长编码长度。
  4. 输出结果

    第一行输出总长度(64 位整数),第二行输出最长编码长度。

  5. 正确性保证

    • 贪心策略:每次合并权值最小的 k k k 个节点,得到最小总代价(Huffman 树性质)。
    • 深度优先选择:权值相同时优先合并深度小的节点,避免深度过大,从而在总代价最小的情况下让最大深度最小。
    • 补全虚节点:保证树结构为满 k k k 叉树,避免最后一次合并不足 k k k 个节点导致的非最优解。

各种学习资料,助力大家一站式学习和提升!!!

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	cout<<"##########  一站式掌握信奥赛知识!  ##########";
	cout<<"#############  冲刺信奥赛拿奖!  #############";
	cout<<"######  课程购买后永久学习,不受限制!   ######";
	return 0;
}

【秘籍汇总】(完整csp信奥赛C++学习资料):

1、csp/信奥赛C++,完整信奥赛系列课程(永久学习):

https://edu.csdn.net/lecturer/7901 点击跳转

2、CSP信奥赛C++竞赛拿奖视频课:

https://edu.csdn.net/course/detail/40437 点击跳转

https://edu.csdn.net/course/detail/41081 点击跳转

3、csp信奥赛高频考点知识详解及案例实践:

CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转

CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转

信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html 点击跳转

4、csp信奥赛冲刺一等奖有效刷题题解:

CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新): https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转

信奥赛C++提高组csp-s初赛&复赛真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13125089.html 点击跳转

5、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html 点击跳转

· 文末祝福 ·

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	cout<<"跟着王老师一起学习信奥赛C++";
	cout<<"    成就更好的自己!       ";
	cout<<"  csp信奥赛一等奖属于你!   ";
	return 0;
}
相关推荐
样例过了就是过了5 小时前
LeetCode热题100 最小路径和
c++·算法·leetcode·动态规划
Aaron15885 小时前
RFSOC+VU13P+GPU 在6G互联网中的技术应用
大数据·人工智能·算法·fpga开发·硬件工程·信息与通信·信号处理
迷途之人不知返6 小时前
Stack & Queue
c++·算法
(Charon)6 小时前
【C++/Qt】Qt 实现 MQTT 测试工具:连接 Broker、订阅主题与发布消息
开发语言·c++·qt
春蕾夏荷_7282977256 小时前
1、c++ acl udp服务器客户端简单实例-服务器端(1)
服务器·c++·udp
没文化的阿浩6 小时前
【数据结构】排序(2)——直接选择排序、堆排序
数据结构·算法·排序算法
誰能久伴不乏6 小时前
Qt/C++ 架构之美:用一个“水龙头”隐喻,讲透面向接口编程与彻底解耦
c++·qt·架构
ytttr8736 小时前
基于libusb的用户空间UVC相机库
算法
bybitq6 小时前
Reactor 模型 vs Proactor 模型:区别与代码示例
算法