CCF CSP 压缩编码 动态规划

问题描述
  给定一段文字,已知单词a 1, a 2, …, a n出现的频率分别t 1, t 2, …, t n。可以用01串给这些单词编码,即将每个单词与一个01串对应,使得任何一个单词的编码(对应的01串)不是另一个单词编码的前缀,这种编码称为前缀码。
  使用前缀码编码一段文字是指将这段文字中的每个单词依次对应到其编码。一段文字经过前缀编码后的长度为:
  L=a 1的编码长度×t 1+a 2的编码长度×t 2+…+ a n的编码长度×t n
  定义一个前缀编码为字典序编码,指对于1 ≤ i < n,a i的编码(对应的01串)的字典序在a i +1编码之前,即a 1, a 2, …, a n的编码是按字典序升序排列的。
  例如,文字E A E C D E B C C E C B D B E中, 5个单词A、B、C、D、E出现的频率分别为1, 3, 4, 2, 5,则一种可行的编码方案是A:000, B:001, C:01, D:10, E:11,对应的编码后的01串为1100011011011001010111010011000111,对应的长度L为3×1+3×3+2×4+2×2+2×5=34。
  在这个例子中,如果使用哈夫曼(Huffman)编码,对应的编码方案是A:000, B:01, C:10, D:001, E:11,虽然最终文字编码后的总长度只有33,但是这个编码不满足字典序编码的性质,比如C的编码的字典序不在D的编码之前。
  在这个例子中,有些人可能会想的另一个字典序编码是A:000, B:001, C:010, D:011, E:1,编码后的文字长度为35。
  请找出一个字典序编码,使得文字经过编码后的长度L最小。在输出时,你只需要输出最小的长度L,而不需要输出具体的方案。在上面的例子中,最小的长度L为34。
输入格式
  输入的第一行包含一个整数n,表示单词的数量。
  第二行包含n个整数,用空格分隔,分别表示a 1, a 2, …, a n出现的频率,即t 1, t 2, …, t n。请注意a 1, a 2, …, a n具体是什么单词并不影响本题的解,所以没有输入a 1, a 2, …, a n
输出格式
  输出一个整数,表示文字经过编码后的长度L的最小值。
样例输入
5
1 3 4 2 5
样例输出
34
样例说明
  这个样例就是问题描述中的例子。如果你得到了35,说明你算得有问题,请自行检查自己的算法而不要怀疑是样例输出写错了。
评测用例规模与约定
  对于30%的评测用例,1 ≤ n ≤ 10,1 ≤ t i ≤ 20;
  对于60%的评测用例,1 ≤ n ≤ 100,1 ≤ t i ≤ 100;
  对于100%的评测用例,1 ≤ n ≤ 1000,1 ≤ t i ≤ 10000。

———————————————————————————————————————————

在考试的时候,我觉得这是历年最难的第四题了,因为往届不会做起码还能随便深搜拿个二三十分,这题就不太容易了。然后考试时想着用哈夫曼树,改变一些规则来做,还是贪心算法,结果一个0分。不过现在看透了这题之后,又觉得是历届最好拿满分的一道题了。没几行代码,一遍过。

来看看这题的本质,先对比一下按字典序排列和哈夫曼编码的不同,下面我把两种编码方式的过程图画出来进行比较:



首先看看大部分人熟悉的哈夫曼编码。计算总长度的方法是每次汇集两个节点,都要加上该节点下所有叶子节点的权重。因为编码规则是贪心策略,即每次汇集两个最小权重的节点,所以会导致编码后的节点序列变化,编码后的序列是ADBCE。

而字典序编码则要求编码过程中节点顺序不能改变,也就是每次只能合并相邻的两个节点,并且还要取最小值,同样每次汇集两个节点,都要加上该节点下所有叶子节点的权重这就是“石子排序”问题,解释起来篇幅太长自行百度一下吧。所以这就是一个动态规划问题,用dp[i][j]表示第i个数到第j个数编码后的最小长度,初始化i==j时dp[i][j]为0,用sum[k]表示前k个数的和,sum[k]-sum[t-1]表示第t到第k个数的和。

状态转移方程为:dp[i][j] = min(dp[i][k]+dp[k+1][j] + sum[j]-sum[i-1])(i<=k<=j)

意思是要将第i到第j个数合并成一个,由于字典序只能合并相邻的,所以只能先合并成i到k,k+1到j两个,再将i到k和k+1到j合并成一个。为了得到最优解,需要把每种可能取值的k都找一遍,取最小值。

代码如下:

#include<iostream>
using namespace std;
const int INF = 1 << 30;  
int dp[1010][1010],a[1010],sum[1010];
int n;
int main()
{
	int i,j,k,t;
	cin >>n;
	for(i=1;i<=n;i++)
	{
		cin >> a[i];
		sum[i] = sum[i-1] + a[i];//用于指定区间内的计算总数 
	}
	
	//沿斜线扫描 
	for(j=2;j<=n;j++)
	{
		for(i=1,k=j;i<=n-j+1;i++,k++)
		{
			dp[i][k] = INF;
			for(t=i;t<k;t++)
			{
				if(dp[i][k] > dp[i][t]+dp[t+1][k]+sum[k]-sum[i-1])
			    {
			    	dp[i][k] = dp[i][t]+dp[t+1][k]+sum[k]-sum[i-1];
			    }
			}
		}
	}
	
	cout << dp[1][n];
	return 0;
}


  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值