Huffman树浅谈

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)×(deep1) ( 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[i][j]=dp[i1][j+1]+a[i] d p [ i ] [ j ] = d p [ i − 1 ] [ j + 1 ] + a [ i ]

dp[i][j]+sum[n]sum[i]=dp[i][2×j] d p [ i ] [ j ] + s u m [ n ] − s u m [ i ] = d p [ i ] [ 2 × j ]

有人也许会问,能否把这个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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值