题解 UVA1720 Weather Report

· · 题解

题目传送门

更好的阅读体验

前置芝士:哈夫曼树

看到最小编码长度,当然要想起哈夫曼树(又称最优二叉树)。哈夫曼树使用变长编码表对文本进行编码。基本思想是对出现次数多的字符使用较短的编码,对出现次数少的字符使用较长的编码,使得编码后字符串的平均长度及期望值降低。可以证明哈夫曼树的 WPL 是最小的。(WPL:树的带权路径长度,即从树根到每一叶子结点的带权路径长度之和)

在二进制下,哈夫曼树主要由以下过程构造:

  1. 将每个结点看做一棵树,构成待构造的森林。
  2. 每次在待构造的森林中选取两个根结点权值最小的树合并,作为一颗新树的左、右子树。新树的根结点权值为其左、右子树根结点的权值之和。
  3. 删去选取的两棵树,将新树加入森林。
  4. 重复步骤 2、3,直到森林中只剩下一棵树为止。该树即为所求的哈夫曼树。

在本题中,注意到本题顺序无关,因此有大量重复的情况。我们枚举每种天气出现的天数作为状态,打表可知在 n=20 时,最多也仅有 1771 种不同状态,远小于 4^{20}=1099511627776(打表大法好!)。所以可以对每种状态进行编码。将以上 1771 种不同状态作为 1771 棵树构造哈夫曼树。

尽管状态顺序无关,但相同的状态出现概率不同,对答案的贡献也不同。因此我们不仅要记录状态出现的概率,还要记录状态出现的次数。记四种天气出现的次数分别为 \{i,j,k,u\}(u=n-i-j-k) 那么这种状态出现的概率 P_i=p_1^i\times p_2^j\times p_3^k\times p_4^u,不同的排布数 Cnt_i=C_n^i\times C_{n-i}^j\times C_{n-i-j}^k

在哈夫曼树的模板中,每个结点仅代表一种排布。但在这里,每个结点代表的是一种状态,其中包含不同的排布情况。因此处理方法略有不同。

我们用小根堆维护哈夫曼森林,具体的:

因为我们将概率作为结点的权值,最终构造完成时每个结点被计算的次数就是它们在哈夫曼树上的深度,也就是最终编码的长度,因此期望 E=\sum\limits_{i=1}^{tot} P_i \times Cnt_i \times deep_i。其中 tot 为总状态数,deep_i 表示结点 i 在哈夫曼树上的深度,也表示编码的长度。

为了简便,我们并不显性连边构造哈夫曼树,而在合并结点时计算对答案的贡献即可。

枚举初始状态复杂度(或者说待合并结点数)为 \mathcal{O}(n^3),堆以及内部合并复杂度都是 \mathcal{O}(\log n)。因此总时间复杂度为 \mathcal{O}(n^3\log^2n)

尽管状态没那么多,但复杂度就是这样。

Code:

#include<bits/stdc++.h>
#define ll long long
#define reg register
#define F(i,a,b) for(reg int i=(a);i<=(b);++i)
using namespace std;
bool beginning;
const int N=25;
int n;
double p[N],pa[N],pb[N],pc[N],pd[N];
ll c[N][N];
inline void init() {//预处理组合数 
    c[0][0]=1;
    F(i,1,20) {
        c[i][0]=c[i][i]=1;
        F(j,1,i-1)c[i][j]=c[i-1][j-1]+c[i-1][j];
    }
}
struct P {
    double p;
    ll cnt;
    bool operator <(const P& a)const {
        return p>a.p;//重载小于号,变小根堆 
    }
};
bool ending;
int main() {
//  system("color fc");
//  printf("%.2lfMB\n",1.0*(&beginning-&ending)/1024/1024);
    init();
    pa[0]=pb[0]=pc[0]=pd[0]=1.0;
    while(~scanf("%d",&n)) {
        F(i,1,4)scanf("%lf",&p[i]);
        F(i,1,20) {//预处理天气数的概率 
            pa[i]=pa[i-1]*p[1];
            pb[i]=pb[i-1]*p[2];
            pc[i]=pc[i-1]*p[3];
            pd[i]=pd[i-1]*p[4];
        }
        priority_queue<P>q;
        reg int u;
        F(i,0,n)F(j,0,n)F(k,0,n) {//插入初始结点 
            u=n-i-j-k;
            if(u<0)continue;
            q.push({pa[i]*pb[j]*pc[k]*pd[u],c[n][i]*c[n-i][j]*c[n-i-j][k]});
        }
        reg double ans=0;
        reg P x,y;
        while(!q.empty()) {
            x=q.top();
            q.pop();
            if(q.empty() and x.cnt==1)break;//仅剩一棵树 
            if(x.cnt>1) {//排布数大于 1 的情况 
                if(x.cnt&1)q.push({x.p,1}),--x.cnt;//奇数 
                ans+=x.p*x.cnt;//统计贡献 
                q.push({x.p*2,x.cnt/2});//内部合并 
            } else {//排布数为 1  
                y=q.top();
                q.pop();
                ans+=x.p+y.p;//统计贡献 
                q.push({x.p+y.p,1});
                if(y.cnt>1)q.push({y.p,y.cnt-1});//将第二个结点重新插入 
            }
        }
        printf("%.6lf\n",ans);
    }
    return 0;
}

AC

欢迎交流讨论,请点个赞哦~

双倍经验~