带权路径长度:$ 节点权值 \times 节点到根的距离 $。
哈夫曼树:在叶子节点的节点权值为一个给定序列的前提下,树上所有节点带权路径长度之和最小的二叉树。
构造:每次从原序列选取两个权值最小的两个点,将它们父节点的权值设为两个子节点权值之和。
性质:
-
哈夫曼树一定是满二叉树(不是完美二叉树,是每个节点要么有两个儿子,要么无儿子的二叉树)。
证明:若一个哈夫曼树只有一个儿子,则可以给它加上另一个儿子使得该儿子的节点权值为 \(0\),则该儿子不会影响整体带权路径长度之和最小。
-
离根越近,节点权值越大。
证明:从构造方式易证。
-
$ 非叶子节点的点权和 = 叶子节点的带权路径和 $。
证明:参考合并果子这题,容易发现哈夫曼树的构造方式即为合并的过程,非叶子节点的点权和 与 叶子节点的带权路径和 在这题中都表示 最小的体力耗费值,自然也就相等。
-
当叶子节点数为 \(n\) 时,总节点数为 \(2n-1\)(所有满二叉树都符合该性质)。
证明:设 \(n_x\) 表示有 \(x\) 个儿子的节点的个数,\(m\) 为二叉树的总边数,\(n\) 为二叉树的总节点数。于是有
\[\begin{cases} n=n_0+n_2\\ m=n-1\\ m=n_2 \times 2 \end{cases} \]将一式代入二式,并联立二、三式可得 \(n_2=n_0-1\)。
所以 \(n=n_0+n_2=n_0+n_0-1=2 \times n_0 -1\)。
POJ 1521
最小的压缩长度即为哈夫曼树非叶子节点的点权之和,维护小根堆模拟建哈夫曼树的过程即可。
code
#include<iostream>
#include<string.h>
#include<queue>
#include<iomanip>
using namespace std;
const int N=1e7+5,M=31;
string s;
int ans,cnt[M];
int main(){
while(1){
cin>>s;
if(s=="END") break;
memset(cnt,0,sizeof cnt);
priority_queue<int,vector<int>,greater<int> > pq;
ans=0;
for(int i=0;i<s.size();i++){
if(s[i]=='_') cnt[26]++;
else cnt[s[i]-'A']++;
}
for(char i=0;i<=26;i++)
if(cnt[i]) pq.push(cnt[i]);
if(pq.size()==1) ans=s.size();
while(pq.size()>=2){
int x=pq.top(); pq.pop();
int y=pq.top(); pq.pop();
pq.push(x+y);
ans+=x+y;
}
int last=s.size()*8;
cout<<last<<' '<<ans<<' '<<fixed<<setprecision(1)<<(double)(last)/(double)(ans)<<'\n';
}
return 0;
}
P2168
众所周知,\(k\) 叉哈夫曼树是每 \(k\) 个节点合并为一个节点,但可能最后只剩下不足 \(k\) 个节点,因此就无法合成根节点。
这说明有节点可以深度更小,带权路径更小。
于是我们考虑补叶子节点并使得它们的节点权值均为 \(0\)。
每 \(k\) 个节点合并为 \(1\) 点,相当于每次减少 \(k-1\) 个节点,因此总节点 \(n\) 必须满足 \((n-1) \bmod (k-1)=0\)。
然后构建哈夫曼树即可完成第一问。
然后小根堆维护深度,并将其作为第二关键字处理合并即可完成第二问。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,k,ans;
priority_queue<pair<int,int>> pq;
signed main(){
cin>>n>>k;
for(int i=1,x;i<=n;i++){
cin>>x;
pq.push(make_pair(-x,0));
}
while((n-1)%(k-1)!=0){
n++;
pq.push(make_pair(0,0));
}
while(pq.size()>1){
int sum=0,w=0;
for(int i=1;i<=k;i++){
sum+=pq.top().first;
w=min(w,pq.top().second);
pq.pop();
}
ans+=-sum;
pq.push(make_pair(sum,w-1));
}
cout<<ans<<'\n'<<-pq.top().second;
return 0;
}
CF884D
显而易见的,题目中的分球过程实质是三叉哈夫曼树的反构造过程。
于是将上一题的 \(k\) 令其恒为 \(3\) 即可。
code
#include<bits/stdc++.h>
using namespace std;
struct node
{
long long w, dep;
bool operator < (const node &b) const
{
if(w != b.w)
return w > b.w;
return dep > b.dep;
}
};
priority_queue<node> pq;
int n, k, cnt;
long long ans;
int main()
{
cin >> n;
k = 3;
for(int i = 1; i <= n; i++)
{
long long x;
cin >> x;
pq.push((node){x, 1});
}
if((n-1) % (k-1) != 0)
cnt = k - 1 - (n-1) % (k-1);
for(int i = 1; i <= cnt; i++)
pq.push((node){0, 1});
cnt = cnt + n;
while(cnt >= 2)
{
long long cur = 0, maxi = 0;
for(int i = 1; i <= k; i++)
{
cur += pq.top().w;
maxi = max(maxi, pq.top().dep);
pq.pop();
}
ans += cur;
pq.push((node){cur, maxi+1});
cnt -= k-1;
}
cout << ans;
return 0;
}