1.Huffman Tree(哈弗曼树)
1.1首先存在定义:
树的带权路径长度:树中所有叶节点的权值乘上其到根的路径长度之和
构造一棵二叉树使其带权路径长度最小,称这样的二叉树为哈夫曼树。也叫最优二叉树
1.2 性质1:哈弗曼树是一颗满二叉树:
证明:如果不是满二叉树,我们就把儿子数为1的节点变为叶子节点,这样会更优;
性质2:哈夫曼树中权重较小的叶结点的深度不可能小于权重较大的叶结点的深度。
证明:如果不正确,那么交换这两个节点就会得到更优的答案;
性质3:存在这样一株哈夫曼树,其中权重最小的两个叶结点的父结点相同。
证明:很显然吧?如果不成立,那么就会不是满二叉树,与性质1矛盾;
1.3 构造方法:
假设有n 个权值w1; w2; w3.......wn,则哈夫曼树的构造规则为:
将w1; w2; w3......wn 看成有n 棵树的森林(每棵树仅有一个点)
在森林中选出两个根节点的权值最小的树合并,作为一棵新树的左右子树,定义新树根节点的权值为左右子树根节点的权值之和
从森林中删除选取的两棵树,并将新树加入森林
重复2,3 两步,直到森林中只剩一棵树,该树为所求的哈夫曼树
此时哈夫曼树根节点的权值为树的带权路径长度
构造哈夫曼树时可以维护一个堆,每次从堆中取两个权值最小的点,将
其合并,然后把合并后的点加入堆
1.4 Huffman Coding(哈弗曼编码)
考虑将一个文件二进制编码,给每个字符赋一个二进制编码
为了方便解码,我们要求任意一个字符的编码不是另一个字符编码的前缀;
因为如果一个字符编码是另一个的前缀那么在解码时会发生混淆
哈夫曼编码是一种满足如上条件的使总编码长度最短的一种编码
1.5构造方法
首先统计每个字符的出现次数
然后将出现次数作为叶子节点的权值构造哈夫曼树
将字符编码为从根到字符所在叶节点的路径(左儿子为0,右儿子为1)
那么总编码长度就是树的带权路径长度
因为哈夫曼树使带权路径长度最小,因此这种编码方式使总编码长度最小
哈弗曼编码是一种无前缀编码。解码时不会混淆。其主要应用在数据压缩,加密解密等场合。
2.prufer序列
prufer序列:
将一颗n个节点的树压缩成一个n-2长度的序列,这个序列包含了每个节点的度数的信息。
由前人们的经验可以知道,prufer序列与无根树一一对应,互相映射。
1.树转prufer
找到所有树上编号最小的**叶子**节点,将其父亲加入prufer序列,一共进行n-2次如上操作,标准复杂度$O(n^{2})$;
如果我们使用堆,可以做到$O(nlogn)$;
如果我们动点脑子,可以做到$O(n)$;
大体来说:指针指向编号最小的叶节点。每次删掉它之后,如果产生了新的叶节点(如果产生一定是原叶子节点的父亲)且编号比指针指向的更小,则直接继续删掉,否则自增找到下一个编号最小的叶节点。易证正确。
2.prufer转树:
取出prufer序列中最前面的元素x,然后寻找一个在点集(一开始是全集{1,2,...,n})中,但不在prufer序列中的编号最小的一个点y,把y从点集中删除,并连接点x和点y,重复以上行为n-1次,这棵树我们就跟他说拜拜了~,标准复杂度$O(n^2)$
如果我们使用堆,可以做到$O(nlogn)$;
如果我们动点脑子,可以做到$O(n)$;
大体来说:指针指向编号最小的不在prufer序列(这代表着这个点的度数为1,不懂的一会看下面)中的点。每次删掉它之后,如果他的父亲节点的度数也是1且编号比指针指向的更小,则直接继续删掉,否则自增找到下一个编号最小的度数为1的节点。易证正确。
由此,prufer序列的特性呼之欲出:
1.prufer序列与无根树一一对应;(由每次找最小的编号的点得到这个性质)
2.prufer序列中某个编号出现的次数就等于这个编号的节点在无根树中的度数-1 (由树转prufer的过程得知)
3.n个点的完全图所构成的生成树的计数,就是n^(n-2);
(证明:一个n个节点的树,它的prufer序列的长度是n-2,每个位置有n种选择)
#includeusing namespace std; #define p 2000000011 long long KSM(long long a,long long b){ long long res=1; while(b){ if(b&1) res=res*a%p; a=a*a%p; b/=2; } return res%p; } int main(){ int n; cin>>n; for(int i=1;i<=n;i++){ int x; scanf("%d",&x); printf("Case #%d: %lld\n",i,KSM(x,x-2)); } }
综上所述,prufer可以优美的解决大部分有关树度数计数问题。