哈夫曼树(Huffman树) 学习日记 + 例题(ch1701 && bzoj4198)

哈夫曼树:

 一棵包含n个叶子节点的k叉树,其中第i个叶子节点带有权值w[i], l[i]为该点到根节点的距离,求所有叶子节点的w[i]*l[i]之和的最小值。

二叉哈夫曼树的求解:

对于二叉哈夫曼树的求解,我们贪心的将最小的两个节点放到最远,然后合并那两个节点,新的点的权值为那两个节点的和,放入原序列中,重复上述步骤,直到原序列只剩下一个节点,这棵树就构建好了,下面有个例子

例如: 3 4 5 8 , 设最后答案为ans

首先我们选3 4, 合并节点,新点权值为7,并加入原序列,ans+= (3+4)

然后新序列中合并5和7,新点权值为12    ans+=(5+7)

最后合并12 和 8  新节点为20,跳出循环  ans+=(12+8)

最后的哈夫曼树就是右边黑色的那棵树,答案就是ans

对于某一个节点,因为其被合并之后的值给了新的节点,而新的节点合并的时候又会加上这个值,实际上是不断为答案作贡献的,做贡献次数就等于深度(也就是路径长度)

 

k叉哈夫曼树的求解:

对于k(k>2)叉哈夫曼树,其求解思路和2叉类似。

我们合并2叉哈夫曼树是从子节点一路合并到根节点的,结束合并操作的标志就是序列中只剩下一个数。但是对于k叉哈夫曼树这会出现问题。

因为最大的节点应该优先连在根节点上,而我们求解时又是从叶子节点开始,这会造成最后根节点的子树少于k个,这是不对的。

比如: 3叉哈夫曼树,序列:3 4 5 8

第一次合并,合并3 4 5.

第二次合并,合并剩下的,最后这棵树就是:

这显然不是最优解。

因此对于k叉哈夫曼树,为了保证其根节点可以选到k个子树,假设节点个数为n,需要满足(n-1)mod(k-1)==0 的条件,假如不满足,我们为原序列补0

对于3 4 5 8 , 它就变成 3 4 5 8 0

第一步合并3 4 0 ,新节点为7,新序列: 7 5 8

然后合并7 5 8

最后生成的树:

 

例题1: ch1701

题目链接:https://www.acwing.com/problem/content/150/

这就是一棵裸的二叉哈夫曼树,每合并一次相当于最后答案计算的时候多加一份,就相当于在边长为1的哈夫曼树中更深一层

代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e4 + 7;
int heap[maxn];
int cnt;
void del() {
	swap(heap[1], heap[cnt--]);
	int now = 1, to = 2;
	while (to <= cnt) {
		if (to<cnt&&heap[to]>heap[to + 1]) to++;
		if (heap[to] >= heap[now]) break;
		swap(heap[to], heap[now]);
		now = to;
		to = now << 1;
	}
}
void insert(int x) {
	heap[++cnt] = x;
	int now = cnt;
	while (now > 1) {
		if (heap[now >> 1] <= heap[now]) break;
		swap(heap[now], heap[now >> 1]);
		now >>= 1;
	}
}
int main() {
	int n; cin >> n;
	for (int i = 1; i <= n; i++) {
		int inp;
		scanf("%d", &inp);
		insert(inp); //我用二叉堆维护每次选最小值
	}
	int ans = 0;
	int min1, min2;
	while (cnt > 1) {
		min1 = heap[1];
		del();
		min2 = heap[1];
		del();
		ans += min1 + min2;
		insert(min1+min2);
	}
	cout << ans << endl;
	return 0;
}

 

 

例题2:bzoj4198

https://www.lydsy.com/JudgeOnline/problem.php?id=4198

每种编码方式前缀不同可以联想到字典树,此时每一个叶子节点就代表一个编号。

可以将哈夫曼树看成这样的字典树,而对于每个单词最后的贡献为: 单词哈希后长度*出现次数 ,对应到哈夫曼树里面就是 子节点权值=出现次数 , 深度=单词哈希后长度。

最后还要让最长的哈希最短,也就是要这颗哈夫曼树最深的子节点最浅,因此我们在合并的时候,假如遇到权值一样的,优先和深度小的合并(不要让深度大的继续合并下去了)

具体操作的话我们可以使用优先队列(当然堆都一样),然后用一个pair存 ,pair.first= 节点的权值  pair.second=节点的深度,优先队列按照first排,first一样再按second排

这就是一棵k叉哈夫曼树

#include<bits/stdc++.h>
#define ll long long
using namespace std;
typedef pair<ll, int> P;
const int INF = 0x3f3f3f3f;
struct cmp {
	bool operator()(const P p1, const P p2) {
		if (p1.first != p2.first) return p1.first > p2.first;
		else return p1.second > p2.second;
	}
};
priority_queue<P, vector<P>, cmp> Q;
int main() {
	ll ans = 0;
	int n, k;
	cin >> n >> k;
	for (int i = 1; i <= n; i++) {
		ll inp;
		scanf("%lld", &inp);
		Q.push(P(inp,0));
	}
	while ((n - 1) % (k - 1) != 0) { //k叉哈夫曼树补0
		Q.push(P(0, 0));
		n++;
	}
	while (Q.size() > 1) {
		P p = Q.top();
		Q.pop();
		for (int i = 2; i <= k; i++) { //每次取k,然后合并
			P p1 = Q.top();
			Q.pop();
			p.first += p1.first;
			p.second = max(p.second, p1.second); 
		}
		p.second++;//深度为子节点里最深的+1
		ans += p.first;
		Q.push(p);
	}
	P p = Q.top();
	cout << ans << endl << p.second << endl;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值