算法竞赛进阶指南-63.荷马史诗

题目链接

算法竞赛进阶指南-63.荷马史诗

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

达达最近迷上了文学。

她喜欢在一个慵懒的午后,细细地品上一杯卡布奇诺,静静地阅读她爱不释手的《荷马史诗》。

但是由《奥德赛》和《伊利亚特》组成的鸿篇巨制《荷马史诗》实在是太长了,达达想通过一种编码方式使得它变得短一些。

一部《荷马史诗》中有 nn 种不同的单词,从 11nn 进行编号。其中第 ii 种单词出现的总次数为 wiw_i

达达想要用 kk 进制串 sis_i 来替换第 ii 种单词,使得其满足如下要求:

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

现在达达想要知道,如何选择 sis_i,才能使替换以后得到的新的《荷马史诗》长度最小。

在确保总长度最小的情况下,达达还想知道最长的 sis_i 的最短长度是多少?

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

字符串 Str1Str1 被称为字符串 Str2Str2 的前缀,当且仅当:存在 1tm1≤t≤m,使得 Str1=Str2[1..t]Str1=Str2[1..t]

其中,mm 是字符串 Str2Str2 的长度,Str2[1..t]Str2[1..t] 表示 Str2Str2 的前 tt 个字符组成的字符串。

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

输入格式

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

2n+12 \sim n+1 行:第 i+1i+1 行包含 11 个非负整数 wiw_i,表示第 ii 种单词的出现次数。

输出格式

输出文件包括 22 行。

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

22 行输出 11 个整数,为保证最短总长度的情况下,最长字符串 sis_i 的最短长度。

数据范围

2n1000002 \le n \le 100000,
2k92 \le k \le 9
1wi10121 \le w_i \le 10^{12}

输入样例:

4 2
1
1
2
2

输出样例:

12
2

Method : 优先队列 + huffman树

这道题和上一道题合并果子一样都是huffman树,只不过合并果子是二进制编码,每次从集合中取出最小的两个树合并;而该题是kk进制编码,每次从集合中取出最小的k个数合并成1个新的结点加入到集合中,每这样操作依次,集合中会减少kk个点新增1个点,即每次操作集合会减少k1k-1个点。

但是我们并不能保证(k1)(n1)(k - 1)\mid (n - 1),因此最后一次操作很可能可合并的点数少于k个结点,而越晚合并,一个结点被结算的次数越少,因此可以填入(n1)(n - 1) % k - 1个空结点,使得(k1)(n1)(k - 1)\mid(n - 1),这些空结点会在最初就合并掉,而且对结果没有贡献。

另外,要使得合并后的树的深度最小,就需要优先合并高度较低的结点。

#include <queue>
#include <vector>
#include <iostream>

using namespace std;

typedef long long LL;
typedef pair<LL, int> PLI;

int n, k;
priority_queue<PLI, vector<PLI>, greater<PLI>> pq;

int main() {
    cin >> n >> k;
    
    for (int i = 0; i < n; i ++) {
        LL x;
        scanf("%lld", &x);
        pq.push({x, 0});
    }
    
    while ((n - 1) % (k - 1) != 0) {
        pq.push({0LL, 0});
        n ++;
    }
    
    LL res = 0;
    while (pq.size() > 1) {
        LL sum = 0; int dep = 0;
        for (int i = 0; i < k; i ++) {
            auto t = pq.top(); pq.pop();
            sum += t.first;
            dep = max(dep, t.second);
        }
        res += sum;
        pq.push({sum, dep + 1});
    }
    
    cout << res << endl;
    cout << pq.top().second << endl;
    
    return 0;
}

复杂度分析

  • 时间复杂度:O((n1k1+1)logn){O((\left \lfloor \frac{n - 1}{k - 1}\right \rfloor + 1)\log n)}, 其中logn\log n为优先队列的插入、弹出时间复杂度。

  • 空间复杂度:O(n){O(n)}

如果每轮合并的石子 可以是任意 的两堆 石子, 那么用到的就是经典的 Huffman Tree 的二叉堆模型
如果每轮合并的石子 可以是任意 的 kk 堆石子, 那么用到的就是经典的 Huffman Tree 的 kk 叉堆模型

k叉堆中,记得需要填空结点, 使得 k1n1k-1 \mid n-1
二叉堆中, 由于 k1=1,1k-1=1,1 能整除一切正整数, 所以不需要关心这一问题