基于哈夫曼算法的思考

一.算法题干

为了装修新房,你需要加工一些长度为正整数的棒材sticks。
如果要将长度分别为X 和Y 的两根棒材连接在一起,你需要支付X + Y 的费用。由于施工需要,你必须将所有棒材连接成一根。
返回你把所有棒材sticks 连成一根所需要的最低费用。注意你可以任意选择棒材连接的顺序。

二.样例

样例1:
输入:sticks = [2,4,3]
输出:14
解释:先将2 和3 连接成5,花费5;再将5 和4 连接成9;总花费为14。
样例2:
输入:sticks = [1,8,3,5]
输出:30

三.解题思路

对于这道题,一开始我的想法是:先根据长度进行排序构造一个递增队列,然后每次取对头两个元素相加后删掉,再将得到的新元素插入队头,结果加上新元素的值,循环进行操作,直到队列中只有一个元素,该元素即为最终得到的结果。
虽然对于两个样例来说这种方法能够得到和标准输出同样的答案,但是显而易见这种想法是错误的,反例也很容易找出,如:[4,3,4,2]。经过排序后得到的序列为:[2,3,4,4]。然后按照我的思路,先取2和3,加和得到5,结果+5,再把5和4进行加和,得到9,结果+9,再把9和4进行加和,得到13,结果+13,最终得到结果为5+9+13=27。
但是显然可以找到一种方法,得到比27更小的值:
取2和3,加和得到5,再取4和4,加和得到8,取5和8,加和得到13,最后结果为5+8+13=26,小于27,由此可以看出我原先的算法逻辑是错误的。
至于为什么错误,原因可以归结为:我设计的算法看似贪心,但实则不是真正的贪心。至于为什么说并非真正的贪心,就是因为我并不是在每一步都取局部最优,也就是每一步都取最小。但是这个思路离真正的正确也就只差一步:该思路是仅在初始状态进行过一次排序,那么我们只要在每一步得到新序列后都重新进行排序,就可以实现真正的贪心了。
其中这也就是哈夫曼算法的基本流程,也是构造一棵哈夫曼树的过程。哈夫曼算法的本质就是根据已知节点序列构造一棵使得WPL(带权路径长度)最小的二叉树,基本思想就是贪心、不断取局部最优最终得到全局最优。

四.实现代码

int connectSticks(vector<int>& v) {
    priority_queue<int,vector<int>,greater<int> >q;
    int res=0;
    for(auto i:v) q.push(i);
    while(1)
    {
    	int x=q.top();
    	q.pop();
    	if(q.empty()) break;
    	int y=q.top();
    	q.pop();
    	res+=(x+y);
    	q.push(x+y);
	}
	return res;
}

五.语法知识

本题中用到了C++STL中的priority_queue这一数据结构。
首先,要使用该数据结构,必须先#include< queue>。这是因为优先队列是队列的一种,它和queue的区别在于我们可以自定义其中数据的优先级,让优先级高的排在队列前面,优先出队,其内部算法是使用堆实现的。
然后,在使用优先队列时要定义该类型。优先级以及容器的类型可以在构造时定义。优先队列的定义如下:priority_queue<Type, Container, Functional>。其中,Type就是元素的类型,Container就是容器的类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用list,因为list底层是用链表实现的),Functional是比较的方法。
Functional可以自己自定义,也可以使用std命名空间中实现的仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)。
一般的定义方式如下:

//升序队列
priority_queue <int, vector<int>, greater<int> > q;
//降序队列
priority_queue <int, vector<int>, less<int> > q;

所谓的升序队列对应顶堆,降序队列对应顶堆,记忆方法为:greater代表越来越,所以是升序,less同理。greater和less就是上面提到的仿函数。
最后,通过一系列类似于queue的函数对其进行操作。主要函数及功能如下:
top访问队头元素(这个和queue相关函数有区别,queue取对头元素是front
empty队列是否为空
size返回队列内元素个数
push插入元素到队尾并进行排序
pop弹出队头元素

五.对比分析

通过对比其他人的代码,我发现,可以通过一些小技巧将代码量进行缩减(当然不可避免的会降低代码的可读性,这是必然的),从而达到更快实现算法的目的。比如下面这段代码:

int connectSticks(vector<int>& sticks) {
	priority_queue<int> h;
	int x,y,ans=0;
	for(auto i:sticks) h.push(-i);
	for(;;)
	{
		x=h.top();
		h.pop();
		ans-=x+=y;
		h.push(x);
	}
	return ans;
}

这里就巧妙地利用了优先队列缺省容器为vector且默认构造大顶堆的特点,将所有数字取负,这样原先的找最小就变成了找负数中的最大。将-=和+=放在了一起(同级运算符且运算顺序从右向左),并将x作为和插入队列。此代码的实现方式减少了代码量、提高了代码的编写效率、缩短了代码的编写时间,值得我进行学习。

六.思维扩展

从这题中我知道了,一般的贪心算法就是要求在每一步中都取局部最优而并非仅在第一步中取得最优,且每步不能依赖于之前的步骤,每步都是一个独立的状态,否则就需要考虑使用动态规划进行实现。

七.参考资料

[1] 连接棒材的最低费用
[2] c++优先队列(priority_queue)用法详解

发布了22 篇原创文章 · 获赞 0 · 访问量 1298

猜你喜欢

转载自blog.csdn.net/qq_35238352/article/details/101169141