问题描述
试题编号: | 201612-4 |
试题名称: | 压缩编码 |
时间限制: | 3.0s |
内存限制: | 256.0MB |
问题描述: | 问题描述 给定一段文字,已知单词a1, a2, …, an出现的频率分别t1, t2, …, tn。可以用01串给这些单词编码,即将每个单词与一个01串对应,使得任何一个单词的编码(对应的01串)不是另一个单词编码的前缀,这种编码称为前缀码。 输入格式 输入的第一行包含一个整数n,表示单词的数量。 输出格式 输出一个整数,表示文字经过编码后的长度L的最小值。 样例输入 5 样例输出 34 样例说明 这个样例就是问题描述中的例子。如果你得到了35,说明你算得有问题,请自行检查自己的算法而不要怀疑是样例输出写错了。 评测用例规模与约定 对于30%的评测用例,1 ≤ n ≤ 10,1 ≤ ti ≤ 20; |
思路
问题有个前提:每个字符的编码按照字典序排列后的顺序与原先顺序一样。所以我们无法像哈弗曼编码一样每次取出权值最小的两个节点,而只能选择相邻的节点,至于到底选择哪两个相邻节点,这便是石子合并问题。
石子合并问题
石子合并问题是最经典的DP问题。首先它有如下3种题型:
(1)有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动任意的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费最小(或最大)。
分析:这是最简单的情况,合并的是任意两堆,直接贪心即可,每次选择最小的两堆合并。本问题实际上就是哈夫曼的变形。
(2)有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动相邻的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费最小(或最大)。
分析:我们熟悉矩阵连乘,知道矩阵连乘也是每次合并相邻的两个矩阵,那么石子合并可以用矩阵连乘的方式来解决。
设dp[i][j]表示第i到第j堆石子合并的最优值,sum[i][j]表示第i到第j堆石子的总数量。那么就有状态转移公式:
(3)问题(2)的是在石子排列是直线情况下的解法,问题(3)中N堆石子为环形排列。
分析:状态转移方程为:
其中有:
此题等同于石子合并问题(2):其编码后字符的长度等于(每个字符出现的频数 * 字符在二叉树中的层数)之和,而层数即该字符被合并的次数。
字 符 ----> 石子堆
频 数 ----> 石头数量
子树合并 ----> 石子堆合并
编码长度 ----> 每个石头堆的石子数 x 被合并的次数之和
比如:
左边:
字符 | 编码 |
---|---|
A | 0000 |
B | 0001 |
C | 001 |
D | 01 |
E | 1 |
总长 | 1x4+3x4+4x3+2x2+5x1=37 |
右边:
字符 | 编码 |
---|---|
A | 0 |
B | 100 |
C | 101 |
D | 110 |
E | 111 |
总长 | 1x1+3x3+4x3+2x3+5x3=43 |
此时算法复杂为O(n^3),这里可以利用平行四边形优化降为O(n^2):
由上面的方程式可知我们每次求dp[i][j]的关键是找到合适的k值,设p[i][j]为dp[i][j]的这个合适的k值,根据平行四边形规则有以下不等式:p[i][j-1]<=p[i][j]<=p[i+1][j]。
代码
#include<stdio.h>
#include<string.h>
const int inf=0x08080808;
int num[1001],sum[1001],dp[1001][1001],p[1001][1001];
int main()
{
int n,i,j,l,k;
while(scanf("%d",&n)!=EOF)
{
for(i=1;i<=n;i++)
scanf("%d",&num[i]);
memset(dp,inf,sizeof(dp));
sum[0]=0;
for(i=1;i<=n;i++)
{
sum[i]=sum[i-1]+num[i];
dp[i][i]=0;
p[i][i]=i; //K
}
for(l=2;l<=n;l++)
{
for(i=1;i+l-1<=n;i++)
{
j=i+l-1;
for(k=p[i][j-1];k<=p[i+1][j];k++)
{
int w=dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1];
if(w<dp[i][j])
{
dp[i][j]=w;
p[i][j]=k;
}
}
}
}
printf("%d\n",dp[1][n]);
}
return 0;
}