堆的应用
虽然可以把堆画成二叉树的形式,但是其本质依然是一维数组
拓展:树状数组也可以画成树的形式,它与堆一样,本质依然是一个一维数组
堆的例题:NOIP 2004 合并果子
非常妙的一道例题,即是堆,也是贪心
分析:
假设目前有四堆果子分别是2、3、5、6。
合并果子方案一:
如果先将2与3合并,会耗费体力值5;
再将5与6合并,会耗费体力值11;
最后将5与11合并,会耗费体力值16;
采取以上合并方案,一共需要耗费体力5 + 11 + 16 = 32
合并果子方案二:
先将耗费体力最小的2与3合并,会耗费体力值5;
再选择耗费体力最小的5与5合并,会耗费体力值10;
最后将10与6合并,会耗费体力值16;
采取以上合并方案,一共需要耗费体力5 + 10 + 16 = 31
还可以继续列举其他方案,但是会发现只有第二种方案最节省体力…
解题思路:每次寻找耗费体力最小的两个堆合并即可(贪心的思想)
解题步骤:
- 建立小根堆
- 取出堆顶元素(最小值a)
- 再执行一次以上操作(再取出一个最小值b)
- 将两次取出的最小值相加得到sum,先将sum加入答案之中,然后再将sum入堆
- 依次循环,得出本题的最小体力值
AC代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 10010;
int heap[N];
int heapSize;
int n;
void put(int d)//建堆模板
{
int now,next;
heap[++heapSize] = d;
now = heapSize;
while (now > 1)
{
next = now >> 1;
if (heap[now] >= heap[next]) break;
else swap(heap[now],heap[next]);
now = next;
}
}
int get()//取出堆顶元素
{
int now = 1,res = heap[1];
int next;
heap[1] = heap[heapSize];
heapSize --;
while (now * 2 <= heapSize)
{
next = now * 2;
if (next < heapSize && heap[next + 1] < heap[next]) next++;
if (heap[now] <= heap[next]) break;
else swap(heap[now],heap[next]);
now = next;
}
return res;
}
int main()
{
int x;
cin >> n;
//O(nlogn)
for (int i = 1;i <= n;i++)//O(n)
{
scanf("%d",&x);
put(x);//O(logn)
}
int ans = 0;
//O(2nlogn)
for (int i = 1;i < n;i++) //n堆果子只需要合并n - 1次即可 O(n)
{
int a = get();//O(logn)
int b = get();//O(logn)
ans += (a + b);
put(a + b);
}
printf("%d",ans);
return 0;
}
代码整体时间复杂度:O(nlogn)
STL之优先队列priority_queue
合并果子写法二:使用STL
priority_queue <int> q;
建立大根堆,每次取出堆顶元素为最大值priority_queue<int, vector<int>,greater<int> > q
;
建立小根堆,每次取出的堆顶元素为最小值
基本操作:
q.empty()
— 如果队列为空返回真
q.pop()
— 出队,删除队首元素(删除后会自动调整为堆)
q.push()
— 入队,加入一个元素(加入会自动调整为堆)
q.size()
— 返回优先队列中拥有的元素个数
q.top()
— 返回优先队列中队首元素(一般为最大值或最小值)
AC代码:
#include <iostream>
#include <queue>
using namespace std;
int main()
{
priority_queue<int,vector<int>,greater<int> > q;//小根堆
int ans = 0;//存储答案
int n;
cin >> n;
for (int i = 1;i <= n;i++)
{
int x;
cin >> x;
q.push(x);
}
//时间复杂度O(nlogn)
for (int i = 1;i < n;i++)
{
int a = q.top();
q.pop();
int b = q.top();
q.pop();
ans += (a + b);
q.push(a + b);
}
cout << ans << endl;
return 0;
}
哈夫曼树 (HUFUMAN TREE)
学习哈夫曼树,需要先知道什么是树的带权路
树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作:
其中,n表示叶子结点的数目,Wi和Li分别表示叶子结点Ki的权值和树根结点到叶子结点Ki之间的路径长度。
例如:对于上图的二叉树,其叶子结点为3、4、5号结点。
3号结点到根结点的路径为1
4号结点到根结点的路径为2
5号结点到根结点的路径为2
故树的带权路径长度WPL = 1x2 + 2x2 + 4x1 = 10
哈夫曼树的定义:
由权值为W1,W2,…,Wn的n个叶子结点所构成的所有二叉树中,带权路径长度WPL最小的那棵二叉树被称为哈夫曼树或最优二叉树。
哈夫曼树的构造(算法)
1.根据给定的n个权值{w1,w2,…,wn}
构成二叉树集合F={T1,T2,…,Tn}
,其中每棵二叉树Ti中只有一个带权为Wi的根结点,其左右子树为空
2.在F中选取两棵根结点权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为左右子树根结点的权值之和
3.在F中删除这两棵树,同时将新的二叉树加入F中
4.重复2、3,直到F只含有一棵树为止.(得到哈夫曼树)
哈夫曼树不是唯一的(形态多样),但是哈夫曼树的WPL一定唯一(最小)
例1:有4 个结点 a、b、c、d,权值分别为 7、5、2、4,构造哈夫曼树
例2:假设给定a、b、c、d、e、f的权值分别为{9,12,6,3,5,15},试构造一棵哈夫曼树并算出该树的带权路径长WPL
这棵哈夫曼树的WPL为:WPL = (9 + 12 + 15) x 2 + 6 x 3 + (3 + 5) x 4 = 122
关于哈夫曼树的注意点:
1、满二叉树、完全二叉树都不一定是哈夫曼树,哈夫曼树也不一定是满二叉树或者完全二叉树
2、哈夫曼树中权越大的叶子离根越近(如果权值大,距离根结点的路径也大,那么WPL一定也很大)
3、具有相同带权结点的哈夫曼树不惟一
4、哈夫曼树的结点的度数为0或2,没有度为1的结点(考研初试性质)
5、包含 n 个叶子结点的哈夫曼树中共有 2n – 1 个结点(考研初试性质)
6、包含 n 棵树的森林要经过 n–1 次合并才能形成哈夫曼树,共产生 n–1 个新结点
有兴趣的同学可以网上查阅哈夫曼树的代码实现(代码长,非重点)