Huffman树
前言
一般的Huffman树就是为了解决一些简单的Huffman编码而产生的。都记得套路就是贪心选最小点。但作为一个重要的算法,其意义远远不是这些。
正文
简单贪心的证明还是很有必要的。
显然,这棵树的最小权值就是每个叶子节点的权值乘上深度,记作
deep×val
d
e
e
p
×
v
a
l
.
对于这样一棵树,当他呈现出他的最小权值形态时,最深的那个点一定是权值最小的。又因为那一个点最深,所以一定有一个兄弟,所以说这个兄弟应当是第二小的。
对于这个琢磨不清的长什么样地树,这应该算的上是唯一清晰地地方,也是这一类 deep×val d e e p × v a l 的树的共同点。
所以就会把这两个节点拼成一对兄弟节点,然后整棵树又陷入了搞不清楚的状态。
但由于我们坚信这两个点会在一起,所以,如果这两个点的权值是val1,val2,深度是deep,那么这棵树的权值肯定会加上一个
val1+val2
v
a
l
1
+
v
a
l
2
, 也就是这个val1+val2是明确的,由于原本这两个点是
(val1+val2)×deep
(
v
a
l
1
+
v
a
l
2
)
×
d
e
e
p
,现在就是
(val1+val2)×(deep−1)
(
v
a
l
1
+
v
a
l
2
)
×
(
d
e
e
p
−
1
)
, deep-1是其父亲的深度,那么把他的父亲权值弄成(val1+val2),把它的父亲变为叶子节点,那么对于整棵树来说,现在这么处理,相当于代价依然是每一个叶子节点的
deep×val
d
e
e
p
×
v
a
l
,只要求得现在这棵树的最小值再加上已经确定有的
(val1+val2)
(
v
a
l
1
+
v
a
l
2
)
。那么就会是最初的树的代价。这样就是一个证明。
上述的证明可以理解成这个算法的由来,不算一个很严谨的证明。但是可以看到这是解决这个题目的方法,每一步都是应该怎么做,每一步都做的是这一步应该做的,那么显然就是对的。
但是这个证明的过程同样适用于k叉Huffman树。但是,仍然有一个dp值得去了解。
dp的想法是从上往下,然后上面的节点的权值大于下面的,所以排一个序,然后dp一遍按层构树的过程。这应该是一个dp套路。 dp[i][j] d p [ i ] [ j ] 表示弄到第i个点,然后这一层有j个位置可以放。
这个j中的某一个可以放一个i+1,也可以所有的j再接一颗子树。
如果接一棵子树,那么就意味着多了一层,那么接下来放的点的代价可以提前加上 sum[n]−sum[i] s u m [ n ] − s u m [ i ] ,其中 sum s u m 是前缀和。
dp方程就是很明显,
有人也许会问,能否把这个dp优化成 O(nlog(n)) O ( n l o g ( n ) ) .但前面已经发现了,贪心是要从下往上构建的,而dp正好相反。而且该dp从状态设计与转移来看都已是最简,要优化除非重新设计状态,所以暂时无法达到。
但是,dp的意义却远远不止这些。从上面的贪心来看,贪心已经用尽了这个题目所有的性质。所以题目一旦有些变化,或者,使性质弱化,贪心往往不行。
dp更接近一种组合数学,线性数学的思维,这类数学的产生就是为了解决由特殊到一般的问题。所以dp很有必要掌握。
正文的拓展
Huffman树中有一个重要的变式就是不等距Huffman树,也就是左儿子与右儿子到父亲的距离一个是1,一个是2.
这时仍然有的性质就是越大的在越上面,越小的在越下面。只要有这一个性质就可以dp,但是却不能贪心,因为我们知道最深的是最小的,但是他的兄弟不一定是第二小的。
但是dp却显得尤为简单,参见代码
code
#include<bits/stdc++.h>
#define LL long long
using namespace std;
inline char gc(){
static char buf[1<<6],*p1=buf,*p2=buf;
return (p1==p2)&&(p2=(p1=buf)+fread(buf,1,1<<6,stdin),p1==p2)?EOF:*p1++;
}
template <class T>
inline void read(T&data){
data=0;
register char ch=0;
while(ch<'0'||ch>'9')ch=gc();
while(ch<='9'&&ch>='0'){
data=(data<<3)+(data<<1)+(ch&15);
ch=gc();
}
return;
}
const int _ = 751;
const LL INF = 9187201950435737471;
int a[_],n,sum[_];
LL dp[2][_][_];
inline bool cmp (register int a,register int b){return a>b;}
int main(){
freopen("epic.in","r",stdin);
freopen("epic.out","w",stdout);
read(n);
for(register int i=1;i<=n;++i)read(a[i]);
sort(a+1,a+n+1,cmp);
for(register int i=1;i<=n;++i)sum[i]=a[i]+sum[i-1];
memset(dp,127,sizeof(dp));
dp[0][1][1] = 0;
for(register int i=0;i<n;++i){
for(register int j=0;j<=n;++j){
for(register int k=0;k<=n-j-i;++k){
if(dp[i&1][j][k]==INF)continue;
if(j+k<=n)dp[i&1][j+k][j]=min(dp[i&1][j][k]+sum[n]-sum[i],dp[i&1][j+k][j]);
if(j)dp[i+1&1][j-1][k]=min(dp[i+1&1][j-1][k],dp[i&1][j][k]+a[i+1]);
dp[i&1][j][k]=INF;
}
}
}
cout<<dp[n&1][0][0];
return 0;
}