【JZOJ4142】【NOI2015模拟SXK】树的编码(哈夫曼树)

Problem

  给一棵有根树的N( 105 ≤ 10 5 )个结点编码一个01串si,要求满足条件:
1. 如果u是v的祖先,那么su必须是sv的前缀
2. 如果u不是v的祖先或后代,那么su不允许是sv的前缀
  注意:允许两个点的编码相同。求所有结点编码长度之和的最小值。

Solution

心路历程

  刚做时并未发现这题可做。。。部分分都没心情时间打。。。

条件转化

  言归正传,讲讲它的正解。
  首先,第二个条件其实可以转化为令每个点所有儿子的编号不互为前缀。因为可以设u、v在a的子树内,a的两个儿子b、c分别为u、v的祖先;我们又要满足su不为sv的前缀。那么如果su、sv互为前缀,则sb、sv也互为前缀,进而sb、sc互为前缀。
  所以说,对于节点x,我们要使它的所有儿子y满足sx为sy的前缀且sy之间两两不互为前缀。

转换模型

  很(bu)容(ke)易(neng)想到用哈夫曼树。对于每个x,我们都将它的所有儿子进行一次哈夫曼编码。设有如下图所示的一棵树:
这里写图片描述
  这棵树的N=4,点1是点2、3、4的父亲。那么我们对于点2、3、4进行一次Huffman,得到下图:
这里写图片描述
  点5、6是我们新建的虚点,边权代表这条边的编码。由于对n个点做Huffman生成的是一棵2n-1个节点、n个叶子结点的树,所以对于x的儿子(这里x=1,它的儿子有点2、3、4),它们都会成为这棵哈夫曼树的叶子,故不可能互为前缀。
  同时,囿于此题的编码可以相同,所以我们在对x的儿子做Huffman时,若x仅剩一个儿子了(图中即为合并成点6了),我们就从x向此儿子连一条无编码的边,表示此儿子与x的编码相同。当然,虚点是没有编码的。

点权问题

  那么我们再想想做Huffman时那些点的点权。
  不难发现,我们选取点权最小的两个点a、b合并,其实就相当于将以a、b为根的子树整体下移一层,即使它们的深度整体+1。这样会使它们的编码长度+1(a那棵子树多了个0编码,b那棵子树多了个1编码)。而编码长度和就会增加size[a]+size[b],这就相当于做Huffman时每次合并a、b会使WPL+=w[a]+w[b]一样。
  于是,点权即为子树的size大小。

计算答案

  那么,答案的话,我们就dfs一遍,一路将编码长度求个前缀和(譬如fat[y]=x,设len表示该点的编码长度,则遍历到y时使len[y]+=len[x]),然后累计即可。

时间复杂度

  时间复杂度: O(nlog2n) O ( n l o g 2 n ) (用堆做Huffman)。

Code

#include <cstdio>
#include <set>
#define fs for(i=last[x];i;i=next[i])
#define fo(i,a,b) for(i=a;i<=b;i++)
using namespace std;
typedef long long ll;

const int N=2e5+1;
int i,n,p,f,tov[N],next[N],last[N],sz[N],len[N];
ll ans;

inline void link(int x,int y)
{
    tov[i]=y;
    next[i]=last[x];
    last[x]=i;
}
void scan()
{
    scanf("%d",&n);p=n;
    fo(i,1,n-1)scanf("%d",&f),link(f,i+1);
}

void dfs(int x)
{
    sz[x]=1;
    int i;
    fs dfs(tov[i]),sz[x]+=sz[tov[i]];
}

struct myComp  
{  
    inline bool operator()(const int &a,const int &b){return sz[a]<sz[b];}  
};
multiset<int,myComp>s;
multiset<int,myComp>::iterator it;
int a,b,l[N],r[N];
void code(int x)
{
    int i;
    fs s.insert(tov[i]);
    while(s.size()>1)
    {
        it=s.begin();a=*it;s.erase(it);
        it=s.begin();b=*it;s.erase(it);
        sz[++p]=sz[a]+sz[b];
        len[l[p]=a]=len[r[p]=b]=1;
        s.insert(p);
    }
    if(!s.empty())
    {
        it=s.begin();
        l[x]=*it;
        s.erase(it);
    }
    fs code(tov[i]);
}

void calc(int x)
{
    if(x<=n)ans+=1ll*len[x];
    if(l[x])len[l[x]]+=len[x],calc(l[x]);
    if(r[x])len[r[x]]+=len[x],calc(r[x]);
}

int main()
{
    scan();
    dfs(1);
    code(1);
    calc(1);
    printf("%lld",ans);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值