Description
ZC一天在看计算机的书籍的时候,看到了一个有趣的东西!每一串字符都可以被编码成一些数字来储存信息,但是不同的编码方式得到的储存空间是不一样的!所以ZC 就想是否有一种方式是可以得到字符编码最小的空间值!显然这是可以的,因为书上有这一块内容--哈夫曼编码(Huffman Coding);一个字母的权值等于该字母在字符串中出现的频率。所以ZC 想让你帮忙,给你一串个字符串,并让你判断这个字符串编码之后的空间值(即01串的长度)?
Input
第一行是一个数字n,表示有n组数据,然后每一组数据是一串字符串(没有空格只有包含小写字母组成,长度不超过100).
Output
对于每组测试数据输出这个串编码之后的空间值。
Sample Input
2
aaabbbb
aaabbbbccccc
这一题在问最小的01串长度。对于每一个字符用01进行编码,这时候我们一定会想让出现频率高的编码长度短一点 ,那么怎么样安排可以让最终的总长度最短,这其实就是构造一棵哈夫曼树然后求叶子路径长度的问题。
首先先要对输入进行简单处理,先将每个字符串出现的次数统计好。这里我用了桶实现,因为只有小写字母,只要放先把字符放进26个桶里然后再取出来就可以统计每个字符的频率。
接下来就是哈夫曼树的构造。
哈夫曼树的构造规则就是每次从有效区间里面找出两个最小的树合成一棵树再放进原来的队伍中,这里用优先队列可以比较快实现。为了练习数据结构,这里采用堆这种数据结构来实现,用模拟链表来实现类似于树的构造。
定义树的权值是叶子结点权值的总和,叶子结点也就是我们的字符频率,有效区间是建哈夫曼树的考虑区间。我们每次会在有效区间内取出两课权值最小的树,然后合成一个大树放回队伍中,大树的权值就是两棵树的权值相加,接下来取出的这两棵树就不会再被用到了,被纳入无效区间,也就是删除。
举个例子,比如aabbc,统计得到a2b2c1,那么就是要对122进行哈夫曼树的构造。
如下图
初始是三个数字1 1 2,接下来找出最小也就是1和2两个合成3
然后再把3插入有效区间内, 12扔进无效区间内,如下图
接下来我们在游戏掉区间内找两个最小合成,重复这样的操作我们就能得到这样的一棵哈夫曼树
下面是如何用堆来实现这个过程
假设有len个节点,每次的合成都是找出两个最小的合成一个比较大的,那么也就是一次会减少一个节点,所以要进行len-1轮合成,会多出lenn-1个节点,所以建立一个2*len-1的数组来存放所有涉及到的节点。
对于每个节点,我们需要存储的是这个节点的权值,叶子节点存的也就是字符的频率,对于左子树和右子树,这里用的是模拟链表,所以存放的是左右子树节点下标在数组中的位置,叶子结点存0,也就是空。
struct TreeNode {
int value;
int lchild, rchild;
};
TreeNode node[100000];
先做好堆排序,也就是数组输入,然后调整为小根堆。
void adjust_heap(int x) {
int j = x * 2;//左子树
while (j <= len) {//在有效区域内
if (j + 1 <= len)//右子树在范围内,有效
if (node[j].value > node[j + 1].value)
j++;//指向小的
if (node[j].value < node[j / 2].value) {
swap(j, j / 2);//交换对应下标的结构体
}
j *= 2;//下沉
}
}
对于每一轮,先把一个最小的放到最后,然后把有效区间的最后一个放到第一个,顶替删除的最小节点,然后再次调整为小根堆(堆的删除操作),重复前面的操作,取出一个最小的放到最后,然后再用有效区的最后一个顶替第一个,调整为小根堆,这时候再用两个取出来的两棵树合成一课树,插在有效区最后(堆的插入操作),然后将大树的左右孩子指向合成这两棵树的节点下标,一轮就完成了。
如下图所示,n=3,所有最终会有2+3=5个节点,先开辟号位置,分界线代表有效区,开始指向3。先找出1,然后放到最后,这时候2顶替1,调整,有效区左移,再来一轮2放到最后,有效区左移,2到堆顶,然后两个出来的节点1和2合成3,让节点指向合成他的节点,然后插入到堆中,完成一轮。
一轮操作代码如下
m是总长度,len是有效区间的长度,也就是1---len是建哈夫曼树的考虑区间
node[m] = node[1];//小的放最后
node[1]= node[len];//有效长度的最后一个顶上来
len--;//有效长度-1
adjust_heap(1);//调整
//再找一次
m--;
node[m] = node[1];
node[1] = node[len];
len--;//有效长度-1
adjust_heap(1);//调整
m--;
//接下来构造指针
//此时len指向有效长度位置,m指向空位置
len++;//新来了一个节点,是合成的
node[len].value = node[m + 1].value+node[m+2].value;
node[len].lchild = m + 1;
node[len].rchild = m + 2;//模拟指针
//要插入这个新结构体到小顶堆中
int j = len;
while (j) {//插入的调整,要跟父节点比较,比父节点小就上移
if (node[j].value < node[j/2].value)
swap(j, j / 2);
else break;//啥也不干就可以退出
j /= 2;
}
到最后一定是第一个是整个哈夫曼树的根节点,然后因为要计算编码总长度,其实就是计算叶子结点路径长度的问题,而路径长度也可以理解为深度,这里运用递归来求解。
也就是如果判断为叶子结点,那么答案加上此时对应的深度。
void cal(int p,int deep) {//计算长度
if (p == 0)return;//空节点
if (node[p].lchild == 0 && node[p].rchild == 0) {//叶子结点
ans += deep*node[p].value;
return;
}
cal(node[p].lchild, deep + 1);//计算左子树
cal(node[p].rchild, deep + 1);
}
最后是完整代码
#include<stdio.h>
#include<string.h>
using namespace std;
struct TreeNode {
int value;
int lchild, rchild;
};
TreeNode node[100000];
char a[10000];
int num[200];//桶数组存放每个字母出现的次数
int len;//node数组的有效长度
int ans;
void ini() {
len = 0;
for (int i = 'a'; i <= 'z'; i++) {
num[i] = 0;
}
}
void swap(int x, int y) {//交换两个对应下标的结构体
TreeNode t;
t = node[x];
node[x] = node[y];
node[y] = t;
}
void adjust_heap(int x) {
int j = x * 2;//左子树
while (j <= len) {//在有效区域内
if (j + 1 <= len)//右子树在范围内,有效
if (node[j].value > node[j + 1].value)
j++;//指向小的
if (node[j].value < node[j / 2].value) {
swap(j, j / 2);
}
j *= 2;//下沉
}
}
void create_haffman() {
//printf("len=%d\n", len);
for (int i = len / 2; i >= 1; i--)
adjust_heap(i);//从后往前调整
//接下来应该是小顶堆了
int m = 2 * len - 1;//总长度
while(len>1) {//每次找出两个最小数字放在m和m-1位置
node[m] = node[1];//小的放最后
node[1]= node[len];//有效长度的最后一个顶上来
len--;//有效长度-1
adjust_heap(1);//调整
//再找一次
m--;
node[m] = node[1];
node[1] = node[len];
len--;//有效长度-1
adjust_heap(1);//调整
m--;
//接下来构造指针
//此时len指向有效长度位置,m指向空位置
len++;//新来了一个节点,是合成的
node[len].value = node[m + 1].value+node[m+2].value;
node[len].lchild = m + 1;
node[len].rchild = m + 2;//模拟指针
//要插入这个新结构体到小顶堆中
int j = len;
while (j) {
if (node[j].value < node[j/2].value)//打得过就上
swap(j, j / 2);
else break;//啥也不干就可以退休了
j /= 2;
}
}
}
void cal(int p,int deep) {//计算长度
if (p == 0)return;//空节点
if (node[p].lchild == 0 && node[p].rchild == 0) {//叶子结点
ans += deep*node[p].value;
return;
}
cal(node[p].lchild, deep + 1);
cal(node[p].rchild, deep + 1);
}
int main()
{
int n; scanf("%d", &n);
while(n--){
scanf("%s", a+1);
int l = strlen(a+1);
for (int i = 1; i <= l; i++)
num[a[i]]++;
for (int i = 'a'; i <= 'z'; i++) {
if (num[i]) {
node[++len].value = num[i];
node[len].lchild = 0;
node[len].rchild = 0;
}
}
if (len == 1)//只有一种
printf("%d\n", node[1].value);//这种情况就不多做讨论
else {
int n = len;//备份,len接下来在变化
create_haffman();//构造哈夫曼树
//for (int i = 1; i <= 2 * n - 1; i++) {
// printf("%d %d %d\n", node[i].value,node[i].lchild,node[i].rchild);
// printf("\n");
//}
ans = 0;//答案
cal(1,0);//第一个肯定是根节点,0代表深度
printf("%d\n", ans);
}
ini();
}
return 0;
}