ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【刷题】贪心——Huffman树:合并果子

2022-02-25 23:02:01  阅读:182  来源: 互联网

标签:结点 ch int 合并 最小 long 刷题 Huffman 贪心


基本概念

  • 路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径。下图中,从根结点到结点 a 之间的通路就是一条路径。

  • 路径长度:在一条路径中,每经过一个结点,路径长度都要加 1 。例如在一棵树中,规定根结点所在层数为1层,那么从根结点到第 i 层结点的路径长度为 i - 1 。下图从根结点到结点 c 的路径长度为 3。

  • 结点的权:给每一个结点赋予一个新的数值,被称为这个结点的权。下图中结点a的权是1。

  • 结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积。下图中结点 b 的带权路径长度为 b * 3 。

  • 树的带权路径长度:树中所有叶子结点的带权路径长度之和。通常记作 “WPL” 。下图中所示的这颗树的带权路径长度为:
    W P L = a ∗ 3 + b ∗ 3 + c ∗ 3 + d ∗ 3 + e ∗ 2 + f ∗ 2 = 24 WPL = a * 3 + b * 3 + c * 3 + d * 3 + e * 2 + f * 2 = 24 WPL=a∗3+b∗3+c∗3+d∗3+e∗2+f∗2=24
    带权路径长度的计算也可以看作结点合并:
    1、合并a和b为g、c和d为h,代价: a + b + c + d = 4 a+b+c+d = 4 a+b+c+d=4
    2、合并g和h为i,代价: g + h = a + b + c + d = 4 g+h=a+b+c+d = 4 g+h=a+b+c+d=4
    3、合并e和f为j,代价: e + f = 6 e + f = 6 e+f=6
    4、合并i和j为k,代价: i + j = a + b + c + d + e + f = 10 i + j = a + b + c + d + e + f = 10 i+j=a+b+c+d+e+f=10
    总共代价为24。

  • 哈夫曼树:当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树 / 赫夫曼树 / 哈夫曼树”。
    在这里插入图片描述


算法

如何构建huffman树呢?
只需要遵循一个原则:权重越大的结点离树根越近
具体做法:
1、先从所有结点中挑选两个最小的结点,合并成新的结点
2、去掉合并前的这两个结点,加入新的结点
3、重复1和2步直到所有结点合并

以上图为例:

  • a=1、b=1、c=1、d=1、e=3、f=3,最小的结点是a和b。
    合并a、b,新的结点g=2。
  • c=1、d=1、e=3、f=3、g=2,最小的结点是c和d
    合并c、d,新的结点h=2
  • e=3、f=3、g=2、h=2,最小的结点是g和h
    合并g、h,新的结点i=4
  • e=3、f=3、i=4,最小的结点是e和f
    合并e、f,新的结点j=6
  • i=4、j=6,最小的结点是i和j
    合并i、j,新的结点k=10,所有结点合并,停止。

每次求最小的两个结点可以用小根堆。


正确性

证明1:在huffman树中,权值最小的两个点深度一定最深,并且可以互为兄弟。
假设某个点权值是最小的,并且深度不是最深。
那么将它和最深的点交换,树的带权路径长度一定会变小。
同样以下图为例,假如交换了f和b,那么这两点的带权路径长度从 b ∗ 2 + f ∗ 3 = 1 ∗ 2 + 3 ∗ 3 = 11 b*2+f*3=1*2+3*3=11 b∗2+f∗3=1∗2+3∗3=11变成了 b ∗ 3 + f ∗ 2 = 1 ∗ 3 + 3 ∗ 2 = 9 b*3+f*2=1*3+3*2=9 b∗3+f∗2=1∗3+3∗2=9。
在这里插入图片描述因此权值最小的点都会被交换到最底下(深度最深)。
显然abcd互相交换,深度不变,树的带权路径长度仍是最小。

由证明1可知,最优解一定要先合并最小的两个点,因为二者深度一定最深且在同一层。

证明2:每次从所有所有结点中挑选最小的两个结点能保证全局最优(整棵树树的带权路径长度最小)

合并完最小的两个结点后,新的结点和剩下的n-2个结点,总共有n-1个结点。
由证明1我们知道,不管这n-1个结点的合并方案是什么,n个结点合并的最优解一定是先合并最小的两个点
因此我们只要求剩下n-1个结点合并的最小代价,再加上最小的两个结点合并的代价,就可以求出全局最优解。
而剩下n-1个结点可以重复上述过程,再找n-1个结点中的最小的两个结点合并。


代码实现 O(nlogn)

在这里插入图片描述

题目链接

这题消耗的体力值就是Huffman树的带权路径长度。

#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
int n, ans;
priority_queue<int, vector<int>, greater<int> > heap;
int main() {
	
	scanf("%d", &n);
	int t;
	for (int i = 0; i < n; i ++ ) {
		scanf("%d", &t);
		heap.push(t);
	}
	while (heap.size() > 1) {
		int a = heap.top();
		heap.pop();
		int b = heap.top();
		heap.pop();
		heap.push(a + b);
		ans += a + b;
	}
	printf("%d\n", ans);
	return 0;
} 

桶排\基数排序+双队列 O(n)

题目链接
这题的n扩大到了 1 0 7 10^7 107,因此用O(nlogn)会超时。

因为每次挑的两个最小结点都越来越大,因此合并完的结点也是越来越大。

先将所有结点排序存在队列a中,从a挑最小的两个结点合并。
令开一个新队列b存合并完的结点

下次挑最小的两个结点时,因为a和b都是有序的,可以直接从a和b的队列的队头挑一个最小的结点,挑选两次。

这样就将挑选最小两个结点的时间降到O(1)

由于快排是nlogn的,因为这题数字范围很小( 1 ≤ a i ​ ≤ 1 0 5 1≤a_i​≤10^5 1≤ai​​≤105),要换成O(n)的桶排。如果数字范围较大,可以换成接近 O ( ( n + b ) l o g b n ) O((n+b)log_{b}n) O((n+b)logb​n)的基数排序。

注意要用long long和手写读入,scanf会超时。

桶排序

#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
int n;
long long ans;
int bucket[100005];
queue<long long> a, b;

inline int read(){
	int ret = 0, f = 1; char ch = getchar();
	while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
	while (ch >= '0' && ch <= '9') ret = ret * 10 + ch - '0', ch = getchar();
	return ret * f;
}
int main() {
	
	scanf("%d", &n);
	long long t, max_t = 0;
	for (int i = 0; i < n; i ++ ) {
		// scanf("%d", &t);
		t = read();
		bucket[t] ++ ;
		max_t = max(max_t, t);
	}
	for (int i = 0; i <= max_t; i ++ ) {
		while(bucket[i] -- ) a.push(i);
	}
	for (int i = 1; i < n; i ++ ) {
		long long t1, t2;
		if (b.empty() || (!a.empty() && a.front() <= b.front() )) {
			t1 = a.front();
			a.pop();
		}
		else {
			t1 = b.front();
			b.pop();
		}
		if (b.empty() || (!a.empty() && a.front() <= b.front() )) {
			t2 = a.front();
			a.pop();
		}
		else {
			t2 = b.front();
			b.pop();
		}
		b.push(t1 + t2);
		ans += t1 + t2;
	}
	printf("%lld\n", ans);
	return 0;
} 

基数排序

#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
int n;
long long ans;
int x[10000005], y[10000005], cnt[260];
queue<long long> a, b;

inline int read() {
	int ret = 0, f = 1; char ch = getchar();
	while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
	while (ch >= '0' && ch <= '9') ret = ret * 10 + ch - '0', ch = getchar();
	return ret * f;
}
int main() {
	
	scanf("%d", &n);
	long long t, max_t = 0;
	for (int i = 0; i < n; i ++ ) x[i] = read();
	
	for (int i = 0; i <= 17; i += 8) {
		memset(cnt, 0, sizeof cnt);
		for (int j = 0; j < n; j ++ ) cnt[(x[j] >> i) & 255] ++ ;
		for (int j = 1; j < 256; j ++ ) cnt[j] += cnt[j - 1];
		for (int j = n - 1; j >= 0; j -- ) { // 从后往前扫a,同时cnt从高往低减,b从后往前放,等价于正着做 
			y[ -- cnt[(x[j] >> i) & 255]] = x[j];
		}
		for (int j = 0; j < n; j ++ ) x[j] = y[j];
	}
	for (int i = 0; i < n; i ++ ) a.push(x[i]);
	
	for (int i = 1; i < n; i ++ ) {
		long long t1, t2;
		if (b.empty() || (!a.empty() && a.front() <= b.front() )) {
			t1 = a.front();
			a.pop();
		}
		else {
			t1 = b.front();
			b.pop();
		}
		if (b.empty() || (!a.empty() && a.front() <= b.front() )) {
			t2 = a.front();
			a.pop();
		}
		else {
			t2 = b.front();
			b.pop();
		}
		b.push(t1 + t2);
		ans += t1 + t2;
	}
	printf("%lld\n", ans);
	return 0;
} 

标签:结点,ch,int,合并,最小,long,刷题,Huffman,贪心
来源: https://blog.csdn.net/qq_42581685/article/details/123139216

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有