树,二叉树,二叉树遍历,哈夫曼树(详解+刷题)

news2024/9/23 19:26:51

👂 后街男孩经典之精选 - 歌单 - 网易云音乐

👂 年轮(电视剧《花千骨》男声版插曲) - 汪苏泷 - 单曲 - 网易云音乐

目录

🌼5.1 -- 树

🌼5.2 -- 二叉树

1,性质

2,存储

3,创建

🌼5.3 -- 二叉树遍历

(1)先序

(2)中序

(3)后续

(4)层序

(5)还原树

⚽刷题

🦎P1305 新二叉树

🦎Tree Recovery

🦎Tree

🌼5.4 -- 哈夫曼树

1,前置知识 

2,实现

3,分析

4,优化

⚽刷题

🦎围栏修复

🦎信息熵

🦎转换哈夫曼编码


🌼5.1 -- 树

1,树:除根节点,分为 m 个 互不相交的有限集

2,节点的度:节点拥有的子树个数

3,由于树中没有环,树中任意两个节点的路径是唯一的

4,有序树:节点的各子树,从左至右有序,不能互换位置

5,树 -- 顺序存储 -- 3种方式👇

#图解 数据结构:树的存储结构 - 知乎 (zhihu.com)

双亲表示法,无法直接得到该节点的孩子;孩子表示法,由于不知道每个节点到底有多少个孩子,只能按照树的度(节点的最大度)分配孩子空间,极大浪费空间双亲孩子表示法,浪费空间

6,树 -- 链式存储 -- 2种方式

二叉树的链式存储 - C语言学习教程_C语言程序_ c语言编程_ c语言入门 (itheima.net)

(1)孩子链表表示法:

类似邻接表,表头包含数据元素data和指向第一个孩子的指针,所有孩子都放入一个单链表中,单链表中的节点记录该节点的下标和下一个节点的地址。

(2)孩子兄弟表示法:

类似二叉链表,节点存储数据域data和2个指针域(lchild和rchild),lchild存储第1个孩子地址,rchild存储右兄弟地址。

7,树,森林和二叉树的转换

(1)森林和二叉树    (2)树和二叉树

森林、树与二叉树相互转换-阿里云开发者社区 (aliyun.com)

兄弟关系在右斜线上,长子是左孩子👆

8,总结

由于在普通的树中,每个节点子树个数不同,存储和运算都比较困难

(比如:不知道每个节点有多少个孩子,或者,知道用书中节点最大度分配空间,造成浪费)

因此实际应用中,将树 / 森林转换为二叉树(进行存储和运算)

二者存在唯一对应关系,不影响结果

🌼5.2 -- 二叉树

1,性质

(1)二叉树第 i 层至多有 2^{i+1} 个节点(某一层)

(2)深度为 k 的二叉树至多有 2^{k} - 1 个节点(总共)

(3)任何一棵二叉树,叶子数 n_{0}^{} ,度为2的节点数 n_{2}^{} ,那么 n_{2}^{}+ 1 =  n_{0}^{} 

-- 度表示节点拥有的子树个数 --

(4)具有 n 个节点的完全二叉树,深度为 \left \lfloor \log n \right \rfloor + 1

(5)完全二叉树,若从上至下,从左至右编号,编号为 i 的节点,其左孩子编号为 2*i,右孩子编号为 2*i + 1,双亲编号为 i / 2(下取整)

2,存储

完全二叉树  -- 顺序存储👇

二叉树的顺序存储结构(看了无师自通) (biancheng.net)

普通二叉树 -- 链式存储

(顺序存储需要补充为完全二叉树(补0),浪费空间)

链式存储分为二叉链表 和 三叉链表👇

二叉树的存储结构 二叉链表 三叉链表 - papering - 博客园 (cnblogs.com)

3,创建

树的应用 —— 二叉树的创建_创建二叉树_Ding Jiaxiong的博客-CSDN博客

👆就是《算法训练营入门篇》的原文,下面是代码👇

1,询问法

先序遍历顺序,每次输入节点信息后,都询问是否创建该节点的 左 / 右 子树(递归创建)

不创建则置空(NULL)

void createtree(Btree &T) //创建二叉树(询问法)
{
    char check; //是否创建左右孩子
    T = new Bnode;
    cout<<"请输入节点信息:"<<endl; //输入根节点数据
    cin>>T->data;

    cout<<"是否添加 "<<T->data<<"的左孩子? (Y/N)"<<endl; //询问创建T的左子树
    cin>>check;
    if(check == "Y")
        createtree(T->lchild);
    else
        T->lchild = NULL;

    cout<<"是否添加 "<<T->data<<"的右孩子? (Y/N)"<<endl; //询问创建T的右子树
    cin>>check;
    if(check == "Y")
        createtree(T->rchild);
    else
        T->rchild = NULL;
}

2,补空法

为空则用特殊字符补空(比如"#"),依然是先序遍历和递归创建

void Createtree(Btree &T) //创建二叉树(补空)
{
    char ch;
    cin>>ch; //二叉树补空后,先序遍历输入字符
    if(ch == '#')
        T = NULL; //建空树
    else {
        T = new Bnode;
        T->data = ch; //生成根节点
        Createtree(T->lchild); //递归创建左子树
        Createtree(T->rchild); //递归创建右子树
    }
}

🌼5.3 -- 二叉树遍历

(1)先序

访问根,先序遍历左子树,左子树为空(或已遍历)才可以先序遍历右子树

void preorder(Btree T) //先序遍历
{
    if(T) {
        cout<<T->data<<" ";
        preorder(T->lchild);
        preorder(t->rchild);
    }
}

(2)中序

中序遍历左子树,左子树为空才可以访问根,最后中序遍历右子树

void inorder(Btree T) //中序遍历
{
    if(T) { //该节点不为空
        inorder(T->lchild); //中序遍历左子树
        cout<<T->data<<" "; //访问根节点
        inorder(t->rchild); //中序遍历右子树
    }
}

(3)后续

后续遍历左子树,后续遍历右子树,左右子树为空才可以先访问根

void posorder(Btree T) //后序遍历
{
    if(T) { //该节点不为空
        posorder(T->lchild); //后序遍历左子树
        posorder(t->rchild); //后序遍历右子树
        cout<<T->data<<" "; //访问根节点
    }
}

(4)层序

第1层开始,从左至右顺序访问(队列实现:先被访问节点的孩子也先被访问)

bool Leveltraverse(Btree T)
{
    Btree p;
    if(!T) 
        return false;
    queue<Btree>Q; //创建一个普通队列,存放指针类型
    Q.push(T); //根指针入队
    while(!Q.empty()) { //队列不为空
        p = Q.front(); //取队头作为当前节点
        Q.pop(); //队头出队
        cout<<p->data<<" ";
        if(p->lchild)
            Q.push(p->lchild); //左孩子指针入队
        if(p->rchild)
            Q.push(p->rchild); //右孩子指针入队
    }
    return true;
}

(5)还原树

根据遍历顺序,可以还原这棵树,包括二叉树还原,树还原,森林还原(3种方式)

1,二叉树还原

(已知中序和先序)

根据中序和先序 或者 中序和后续,可以唯一地还原二叉树

步骤:a.先序序列第1个字符为根  b.中序序列,以根为中心划分左右子树  c.重复上述步骤

eg:

1,已知一棵二叉树,先序ABDECFG,中序DBEAFGC,那么根为A,左子树DBE,右子树FGC

分析1:

左子树DBE在先序中是BDE,所以左子树的根为B,且中序以B为中心划分左右孩子D,E

分析2:

右子树FGC在先序中是CFG,所以右子树的根为C,左子树包含FG,右子树空,先序中为FG,所以F为根,中序中以F为根划分左右子树,左子树为空,右子树为G

代码

BiTree pre_mid_createBiTree(char *pre, char *mid, int len) //先,中序还原二叉树
{
    if(len == 0)
        return NULL;
    char ch = pre[0]; //先序第1个节点,作为根
    int index = 0; //中序查找根节点,index记录查找长度
    while(mid[index] != ch)
        index++; //找到根节点后,左边左子树,右边右子树
    BiTree T = new BiTNode; //创建根节点
    T->data = ch;
    //递归创建左子树
    T->lchild = pre_mid_createBiTree(pre + 1, mid, index); 
    //递归创建右子树
    T->rchild = pre_mid_createBiTree(pre+index+1, mid+index+1, len-index-1); 
    return T;
}

👆代码解释👇

1,pre,mid为指针类型,分别指向先序,中序序列的首地址

len为序列长度

2,先序序列第1个字符pre[0]为根,index记录中序中查找pre[0]的长度

3,左子树:先序的首地址为pre + 1,中序首地址mid,长度index

4,右子树:先序首地址pre + index + 1,中序首地址mid + index + 1,长度len - index - 1

(右子树长度 = 总长度 - 左子树长度 - 根)

(已知中序和后序)

后序 DEBGFCA,中序 DBEAFGC

建议画图

BiTree pro_mid_createBiTree(char *last, char *mid, int len) //后中序建立二叉树
{
    if(len == 0)
        return NULL;
    char ch = last[len - 1]; //后续最后1个节点为根
    int index = 0; //中序查找根节点,index记录查找长度
    while(mid[index] != ch)
        index++;
    BiTree T = new BiTNode; //创建根节点
    T->data = ch;
    T->lchild = pro_mid_createBiTree(last, mid, index); //创建左子树
    T->rchild = pro_mid_createBiTree(last+index, mid+index+1, len-index-1); //创建右子树
    return T;
}

2,树还原

树的先序,中序遍历 == 二叉树的先序,中序遍历,所以

(根据给定的先序,中序序列)👇先还原为二叉树,再从二叉树转树

先序ABEFCDGIH,中序EFBCIGHDA   (A是根)

先序BEFCDGIH,中序EFBCIGHD          (B是根)

先序EFCDGIH,中序  (左)EF      (右)CIGHD      (E,C是根)

先序DGIH,中序IGHD            (D是根)

先序GIH,中序IGH     (G是根)           

-->(至此,还原完毕)

👆二叉树转树,长子作为左孩子,兄弟关系右斜

3,森林还原

类似上面的树还原,区别在于,森林加1个根节点,就变成了普通的树

⚽刷题

🦎P1305 新二叉树

P1305 新二叉树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

静态存储的方式,l[]存储左儿子,r[]存储右儿子,比如:

l[0] == 1表示a的左儿子是b

r[0] == 2表示a的右儿子是c

#include<iostream>
using namespace std;

int l[30], r[30], root, n;
string s;

void preorder(int t) //先序遍历
{
    if(t != '*' - 'a') { //非空节点
        cout<<char(t + 'a'); //整型转字符
        preorder(l[t]); 
        preorder(r[t]);
    }
}

int main()
{
    cin>>n;
    for(int i = 0; i < n; ++i) {
        cin>>s;
        if(!i) //根节点
            root = s[0] - 'a'; //字符转整型
        l[s[0] - 'a'] = s[1] - 'a'; //存储左儿子
        r[s[0] - 'a'] = s[2] - 'a'; //存储右儿子
    }

    preorder(root); //先序遍历并输出

    return 0;
}

🦎Tree Recovery

Tree Recovery - UVA 536 - Virtual Judge (vjudge.net)

为了更好理解postorder()函数的过程,想想递归树,也可以对照样例来模拟

比如👇

DBACEGF ABCDEFG
ACBFGED

解释

int len = inorder.find(preorder[l1]) - l2; //左子树长度

👆inorder.find(preorder[l1]) 表示中序中根节点下标,l2表示中序中子树的起始位置

根节点 - 起始 = 左子树长度

AC  代码

#include<iostream>
using namespace std;

string preorder, inorder;

//l1先序起始位置, l2中序起始, n节点数
void postorder(int l1, int l2, int n)
{
    if(n <= 0) return; //递归出口
    int len = inorder.find(preorder[l1]) - l2; //左子树长度
    //先序DBACEGF 中序ABCDEFG  对照样例写递归
    postorder(l1 + 1, l2, len); //递归左子树
    postorder(l1 + len + 1, l2 + len + 1, n - len - 1); //递归右子树
    cout<<preorder[l1]; //输出根节点位置
}

int main()
{
    while(cin>>preorder>>inorder) {
        int len = preorder.size();
        postorder(0, 0, len);
        cout<<endl;
    }

    return 0;
}

🦎Tree

Tree - UVA 548 - Virtual Judge (vjudge.net)

思路

输入二叉树中序,后续序列,求从根到叶子编号之和最小的叶子节点,sum一样就取编号最小的叶子节点。

先根据后序确定根,然后在中序中划分左右子树,还原二叉树,最后先序遍历,并查找根到叶子编号之和最小的叶子节点的编号(权值)。

解释

(1) 

stringstream s(line);

将字符串传递给 stringstream 构造函数,后续的int x; while(s>>x);     因为x是int,所以会读取字符串中的int,而不读取空格

(2)

if(!getline(cin, line)) {}

getline 函数会从输入流cin中读取一行数据,并将其存储在字符串变量line中,包含空格和其他特殊字符

(3)

lch[root]=createtree(l1,l2,len);
rch[root]=createtree(l1+len+1,l2+len,m-len-1);

与

findmin(rch[v],sum);

这里解释下lch[] 和 rch[],两个数组保存的是左,右子树的信息,下标索引为节点编号,也即节点的值,比如 lch[13] = 5; 表示节点13的左儿子为5

(4)

sum_min = 0x7fffffff;

0x 十六进制,0 八进制,0b 二进制

0x7fffffff 就表示 0111 1111 1111 1111 1111 1111 1111 1111,即10进制中 2^31 - 1

十六进制的 7 表示成二进制 0111,十六进制的 f,即 15,表示成二进制 1111

AC  代码                

#include<iostream>
#include<sstream> // stringstream
using namespace std;

const int maxn = 10000+5;
int lch[maxn], rch[maxn], in[maxn], post[maxn], n, ans; // in中序, post后序
int sum_min; // 最大int

// 读入带空格字符串
bool read(int *a)
{
    string line;
    if(!getline(cin, line))
        return false; // 读入失败
    stringstream s(line);
    n = 0;
    int num;
    while (s >> num) // num为int,所以只会读取整数
        a[n++] = num; // 得到初始节点总数 n
    return n > 0;
}

// 递归建树
int create_tree(int L1, int L2, int len) // L1中序起始, L2后序起始
{ // len当前子树长度
    if (len <= 0)
        return 0; //空树
    int root = post[L2 + len - 1]; // 当前子树后序最后一个为根, 所以要加上L2
    // 中序中找根节点
    int p = 0;
    while (in[L1 + p] != root)
        p++; // 根节点在中序位置
    lch[root] = create_tree(L1, L2, p); // 递归左子树
    rch[root] = create_tree(L1 + p + 1, L2 + p, len - p - 1); // 递归右子树
    return root;
}

void findmin(int v, int sum) // v当前节点编号值value
{
    sum += v;
    if (!lch[v] && !rch[v]) // 没有左右子树, 即叶子节点
        if (sum < sum_min || (sum == sum_min && v < ans)) {
            ans = v;
            sum_min = sum;
        }
    if (lch[v]) // v 有左子树
        findmin(lch[v], sum); // 递归左子树
    if (rch[v]) // v 有右子树
        findmin(rch[v], sum); // 递归右子树
}

int main()
{
    while(read(in)) {
        read(post);
        create_tree(0, 0, n);

        sum_min = 0x7fffffff; // 记得在此处更新

        findmin(post[n - 1], 0);
        cout<<ans<<endl;
    }
    return 0;
}

🌼5.4 -- 哈夫曼树

1,前置知识 

(1)根据维基百科

霍夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。

(2)

哈夫曼编码(Huffman Coding)多图详细解析_von Libniz的博客-CSDN博客

👍 ---- 知识点介绍,引用别人的就好,省的敲

(3) 

为了追求最优编码方案,如果每个字符使用频率一样,我们可以采用固定长度编码

但使用频率不一样的话,需要采用不等长编码 ,哈夫曼编码就是其中的一个代表

哈夫曼编码广泛用于 数据压缩,尤其是远距离通信和大容量数据存储,常用的JPEG图片,就是采用哈夫曼编码压缩的。

基本思想 

以字符的使用频率作为权值构建一颗哈夫曼树。

要编码的字符作为叶子节点,出现的频率作为权值。

以自底向上的方式,进行 n - 1 次“合并”。

权值越大的叶子节点,离根越近。

(4)

贪心策略

每次从森林中,找到2棵权值最小孤儿树,作为左右子树,构造一棵新树,新树权值为2颗孤儿树的权值和,将新树放回森林。

2,实现

(1)数据结构

节点结构体 HnodeType  --  二叉树

typedef struct {
    double weight; // 权值
    int parent, lchild, rchild; // 双亲,左孩,右孩
    char value; // 字符
} HNodeType;

编码结构体 HcodeType   --  与字符一一对应的二进制

bit[] 存放节点的编码,start 记录开始的下标

逆向编码存储时(从叶子到根),start 从 n - 1 开始依次递减,从后向前存储 

读取时,从 start + 1开始到 n - 1,前向后输出,即该字符的编码

为什么存储 -- 后向前;而读取 -- 前向后呢?

这符合哈夫曼树的 构造和编码 流程,每个字符对应的二进制,都是从根节点开始读的(前向后),但是记录 bit[] 时,是从每一个叶节点出发的(后向前)(详情看前置只是的博客)

typedef struct {
    int bit[MAXBIT]; // 存储编码的数组
    int start; // 编码开始的下标
} HCodeType;

(2)初始化

节点权值为 0

parent, lchild, rchild 为 -1

value 对应字符

然后读入叶子节点权值

(3) 循环构造哈夫曼树

集合T中取出双亲为-1且权值最小的两棵树ti, tj,合并为一颗新树zk

int x1, x2; //  最小权值节点的序号
double m1, m2; // 最小权值节点的权值
for (int i = 0; i < n - 1; i++) {
    m1 = m2 = MAXVALUE; // 初始化为最大值, m编号, x序号
    x1 = x2 = -1; // 初始化为 -1
    // 找出所有节点中权值最小, 无双亲节点的两个节点
    for (int j = 0; j < n + i; j++) {
        if (HuffNode[j].weight < m1 && HuffNode[j].parent == -1) {
            // 交换当前最小权值的两棵树
            m2 = m1, x2 = x1; // m1最小, m2次小

            m1 = HuffNode[j].weight; // 最小节点权值
            x1 = j; // 记录序号
        }
        else if (HuffNode[j].weight < m2 && HuffNode[j].parent == -1) {
            m2 = HuffNode[j].weight; // 次小节点权值
            x2 = j; // 记录序号
        }
    }
    /* 更新新树信息 */
    // 设置 parent
    HuffNode[x1].parent = n + i;
    HuffNode[x2].parent = n + i;
    // 设置 child 和 weight
    HuffNode[n + i].weight = m1 + m2;
    HuffNode[n + i].lchild = x1; 
    HuffNode[n + i].rchild = x2;
}

解释

(1)外层 for 循环 n - 1 次

构建哈夫曼树时,假设 n 个叶节点,那么总节点数 2n - 1 个,此时内部节点数 = 总数 - 叶子数 = n - 1

所以循环次数为 n - 1(内部节点数)

(2)初始化权值MAX 和序号 -1

为了保证后续选择序号和权值时,有合适的初始值

(3)内层 for 循环

在当前节点集合中找出权值最小的两个节点x1和x2,并且这两个节点没有双亲节点

(parent == -1)

通过遍历节点集合,比较节点的权值找出最小和次小的节点并记录它们的序号

(4)更新信息

为什么双亲的序号是 n + i 呢

n 表示原始叶子节点的个数,i 表示当前正在处理的第 i 个非叶子节点

保证新节点的编号是唯一的,不会与其他已存在的节点重复

(4)输出 哈夫曼编码 

回顾

节点结构体 HNodeType

typedef struct {
    double weight; // 权值
    int parent, lchild, rchild; // 双亲,左孩,右孩
    char value; // 字符
} HNodeType;

编码结构体 HCodeType

typedef struct {
    int bit[MAXBIT]; // 存储编码的数组
    int start; // 编码开始的下标
} HCodeType;

代码

解释看注释(详细过程的图解 + 图表 + 代码,请自行看书)

// (用于生成叶子节点编码) 参数1编码结果的HCodeType数组, 参数2叶子个数
void HuffmanCode(HCodeType HuffCode[MAXLEAF], int n) 
{
    HCodeType cd; // 编码信息
    int c, p; // 叶节点和父节点
    for (int i = 0; i < n; i++) { // 遍历所有叶节点
        // 索引0开始, 所以最后一个位置是 n - 1
        cd.start = n - 1; // 临时编码起始位置 n - 1, 后往前
        c = i; // i 为叶子节点编号
        p = HuffNode[c].parent; // 叶节点父亲的编号
        // p != -1保证向上遍历, 直到根节点, 因为根节点没有父亲, p == -1
        while (p != -1) { // 父节点不为 -1
            if (HuffNode[p].lchild == c) 
                cd.bit[cd.start] = 0; // 左 0
            else 
                cd.bit[cd.start] = 1; // 右 1
            cd.start--; // 向前记录编码的 0 / 1
            c = p; // 子变父
            p = HuffNode[c].parent; // 此时的c就是原来的p, 父变爷爷
        } 
        // while循环结束后, 当前叶节点 i 的二进制记录完毕
        // 每个while循环, 都是一条完整的从叶节点到根节点的路径

        /* 叶节点的编码从临时编码cd中复制出来, 放入编码结构体数组 */
        // bit是结构体成员, cd是临时编码结构体, 第 i 个叶节点, 第 j 个编码位置
        // 编码起始位置开始, 依次复制
        // cd.start + 1, 开始, 因为最后多前移了一格, 而这一格没有父节点, bit[]没有记录
        for (int j = cd.start + 1; j < n; j++) // 前往后
            HuffCode[i].bit[j] = cd.bit[j]; 
        HuffCode[i].start = cd.start; // 更新HuffCode中起始位置, 打印时和cd位置一样
    }
}

3,分析

在 构造循环哈夫曼树 JuffmanTree() 函数 中,

if (HuffNode[j].weight < m1 && HuffNode[j].parent == -1) 为基本语句

外层 i 与 内层 j 双层循环

i = 0, 该语句执行 n 次

i = 1,n + 1次

...

i = n - 2,执行 n + (n - 2) 次

加起来 (3n - 2) * (n - 1) / 2

而在函数 HuffmanCode() 中,编码和输出的时间复杂度都接近 n^2

所以时间复杂度 O(n^2)

空间复杂度:所需存储空间为 节点结构体数组 和 编码结构体数组

哈夫曼树数组 HuffNode[] 中节点为 n 个,每个节点包含 bit[MAXBIT] 和 start 两个域,

则算法空间复杂度为 O(n * MAXBIT)

4,优化

(1)时间

HuffmanTree() 构造哈夫曼树中,找两个权值最小节点时,使用优先队列,时间复杂度 O(logn),执行 n - 1 次,总时间复杂度 O(nlogn)

(2)空间

HuffmanCode() 输出哈夫曼编码中,哈夫曼编码数组 HuffCode[] 中,定义一个动态分配空间的线性表来存储编码(每个线性表的长度都为实际编码长度,大大节省空间

⚽刷题

🦎围栏修复

Fence Repair - POJ 3253 - Virtual Judge (vjudge.net)

3253 -- Fence Repair (poj.org)

样例解释

思路

21 + 13 = 34

容易发现,要取最小值,正向思考,每次去掉当前最大值,逆向则符合哈夫曼树的构造

(每次取两个最小的合并,直到合并为一棵树,每次合并的结果就是切割的费用)


(优先队列默认最大堆,队头(堆顶)元素始终是最大值,这里我们需要设置最小堆)

使用优先队列(最小值优先),每次都弹出两个最小值 t1, t2, t = t1 + t2, sum += t,将 t 入队,继续,直到队空。sum为所需花费


假设20000个木板,每块50000米,哈夫曼构造,结果 sum = 10^4 * (5*2 + 5*3 + ... + 5*20000) = 5 * 10^4 * (2 + 3 + ... + 20000) ≈ 5 * 10^4 * 2 * 10^8 = 10^13(用long long)

priority_queue三个参数👇

std::priority_queue - cppreference.com

关于第 2 参数

用于存储元素的底层容器类型。容器必须满足 SequenceContainer 的要求,其迭代器必须满足 LegacyRandomAccessIterator 的要求

priority_queue常用函数

AC  代码

注意测试样例

#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;

int main()
{
    long long sum = 0;
    int n, t, t1, t2;
    priority_queue<int, vector<int>, greater<int> > q;

    scanf("%d", &n);
    for (int i = 0; i < n; ++i) {
        scanf("%d", &t);
        q.push(t);
    }

    if (q.size() == 1) {
        sum += q.top();
        q.pop();
    }

    while (q.size() > 1) {
        t1 = q.top(), q.pop();
        t2 = q.top(), q.pop();
        t = t1 + t2, q.push(t);
        sum += t;
        // cout << sum << endl;
    }

    printf("%lld", sum);
    return 0;
}

🦎信息熵

Entropy - POJ 1521 - Virtual Judge (vjudge.net)

Huffman模板题 

The text strings will consist only of uppercase alphanumeric characters and underscores (which are used in place of spaces)

文本字符串只包含大写字母数字字符和下划线(用来代替空格)

样例解释

对于输入中的每个文本字符串,输出 8 位 ASCII 编码的比特长度、最佳无前缀变长编码的比特长度,以及精确到小数点后一位的压缩比

思路

最佳无前缀可变长度编码(optimal prefix-free variable-length encoding)

就是哈夫曼编码

首先根据字符串统计每个字符出现频率,然后按频率构造哈夫曼树,计算总的编码长度

(多次读入)

理解

这里可以按2种方式理解

AC  代码

源码是漏了考虑数字,但是,有点还是不错,它有对一个字符的特判,初始化 ans = s.size(),这是我不足的地方

有个坑,double 的 printf 输出用 .f ,而 scanf 输入用 .lf

第二个坑,需要特判一个字符的情况,比如 aaaa,输出应该是 32 4 8.0

所以 if (q.size() == 1) ans = s.size()

#include<iostream>
#include<cstring> //memset()
#include<cstdio> // printf()
#include<queue>
using namespace std;

int a[200]; // 字符出现次数

int main()
{
    string s;
    int ans, t1, t2, t, n;
    while (1) {
        cin >> s;
        if (s == "END") break;

        memset(a, 0, sizeof(a)); // 初始化
        ans = 0, n = s.size();

        for (int i = 0; i < n; ++i) {
            if (s[i] == '_')
                a[99]++; // _代替空格
            else
                a[s[i] - '0']++; // 字符出现次数
        }

        // 最小值在堆顶的优先队列
        priority_queue<int , vector<int>, greater<int> > q;

        for (int i = 0; i <= 99; ++i)
            if (a[i]) q.push(a[i]);

        // 特判: 只出现一种字符
        if (q.size() == 1) ans = n; // 注意这里是n, 每个字符用一个0/1表示, n个0/1

        while (q.size() > 1) {
            t1 = q.top(), q.pop();
            t2 = q.top(), q.pop();
            t = t1 + t2;
            ans += t, q.push(t);
        }

        printf("%d %d %.1f\n", 8*n, ans, (double)8*n / ans);
    }

    return 0;
}

也可以不用 a[] 来统计 数字 + 大写字母 + 下划线,出现次数

直接对字符串 s 排序,然后顺序判断每个字符出现次数

🦎转换哈夫曼编码

Inverting Huffman - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

Inverting Huffman - UVA 12676 - Virtual Judge (vjudge.net)

思路

本题不是常规(简单)的哈夫曼编码问题,不是给出字符出现次数,来求最短编码长度;

而是给定编码长度,即离根节点的距离

编码长度 = 离根节点距离(与出现次数成反比

给定编码长度,已知长度越大,离根节点越远,权值越小(出现次数越小)

那么我们假定编码长度最大的为1,然后秉持着 “每一层当前节点权值,至少是下一节点权值的最大值”,即可找到最短编码长度

如果当前节点权值比下一层最大值小,那么应该出现在下一层,而不会出现在本层 

举个例子

4 // 4个不同字符
3 1 2 3 // 每个字符编码长度

3 1 2 3 即 3 3 2 1,那么可以假定离根节点距离最大,为 3 的两个字符,权值都为 1👇

那么最短编码长度为 5

算法设计

根节点权值最大可能:1 2 3 5 8 13 21 34 55 89 144 ...   到第50个数,第49层, 用 long long

  1. 读取输入的字符个数 n

  2. 清空之前的数据,对深度数组 deep[] 进行清空操作,确保每次循环都是从空数组开始

  3. 初始化变量 maxd 为 0(最大深度)

  4. 读取每个字符对应的深度 x

    a. 更新 maxd,如果当前深度 x 大于 maxd,则更新 maxd 的值为 x

    b.   deep[x] ,第 x 层中,初始化每个节点权值为 0

  5. 向上遍历处理树的每一层,从最大深度开始,直到第 0 层

    a. 当前层

    i. 节点的值为 0,找到一个字符,根据贪心,赋值为上一层最大元素

    ii. 对当前层的节点进行排序

    b. 合并节点,并放入上一层

    i. t = t1 + t2,t 加入上一层

    c. 更新 temp 当前层最大值

  6. 输出第 0 层第 1 个元素的值,即 *(deep[0].begin())

AC  代码

#include<iostream>
#include<algorithm> // sort()
#include<cmath> // max()
#include<vector>
using namespace std;

const int maxn = 55; // 最大深度
vector<long long> deep[maxn]; // 每个元素都是vector数组

int main()
{
    int n, x; // 第 x 层, n 个字符

    while (cin >> n) {

        // 清空
        for (int i = 0; i < n; ++i)
            deep[i].clear(); // 清空vector数组

        // 初始化
        int maxd = 0; // 最大深度
        for (int i = 0; i < n; ++i) {
            cin >> x; // 第 x 层, 即到根节点距离 x
            maxd = max(maxd, x);
            deep[x].push_back(0); // 第 x 层每个节点初始化为0
        }

        // 向上遍历
        long long temp = 1; // 该层排序后最大元素 (最后一个)
        for (int i = maxd; i > 0; --i) { // 最大深度开始

            for (int j = 0; j < deep[i].size(); ++j)
                if (!deep[i][j])
                    deep[i][j] = temp; // 第 i 层没有权值的节点, 被上一层最大值赋值
            sort(deep[i].begin(), deep[i].end()); // 第 i 层排序

            // 合并后放入上一层
            for (int j = 0; j < deep[i].size(); j += 2)
                deep[i - 1].push_back(deep[i][j] + deep[i][j + 1]);

            // 更新 temp 为第 i 层最大值
            temp = *(deep[i].end() - 1); // 最大值即最后一个元素
        }
        cout << *(deep[0].begin()) << endl; // 输出第 0 层第 1 个元素
    }

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1228523.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RTMP协议和源码解析

一、背景 实时消息传输协议&#xff08;Real-Time Messaging Protocol&#xff09;是目前直播的主要协议&#xff0c;是Adobe公司为Flash播放器和服务器之间提供音视频数据传输服务而设计的应用层私有协议。RTMP协议是目前各大云厂商直线直播业务所公用的基本直播推拉流协议&a…

02 elementplus前端增删改查【小白入门SpringBoot+Vue3】

视频教程来源于 B站青戈 https://www.bilibili.com/video/BV1H14y1S7YV 只用elementplus&#xff0c;学点增删改查&#xff0c;还没有于后端连接起来&#xff0c;具体在下一篇 搭建一个小页面&#xff0c;显示数据 补充&#xff1a;webstorm格式化代码&#xff0c;修改了快捷…

二叉树oj题集(LeetCode)

100. 相同的树 关于树的递归问题&#xff0c;永远考虑两方面&#xff1a;返回条件和子问题 先考虑返回条件&#xff0c;如果当前的根节点不相同&#xff0c;那就返回false&#xff08;注意&#xff0c;不要判断相等时返回什么&#xff0c;因为当前相等并不能说明后面节点相等…

记录:RK3568显示异常。

最近调一个RK3568的新板子&#xff0c;板子其它接口功能都调试ok。可唯独在适配显示时发现&#xff0c;HDMI和MIPI显示均出现异常。当系统启动要进入桌面时候内核就开始报错。 因为这套源码之前在其它的板子上适配过&#xff0c;所以第一反应就是硬件问题或者是那个电压没配置…

5 redis的GEO操作

一、GEO Redis 3.2版本提供了GEO(地理信息定位)功能&#xff0c;支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。 有效纬度从-85.05112878度到85.05112878度 注意&#xff1a;当坐标位置超出上述指定范围时&#xff0c;将会返回一个错误。 …

音视频技术在手机上的应用与挑战

// 编者按&#xff1a;随着手机相机功能日益强大&#xff0c;4k&#xff0c;8k&#xff0c;各类特色短视频的拍摄&#xff0c;编辑、播放需求日益增长&#xff0c;短视频应用的火爆也对当前的手机音视频技术提出了更高的要求&#xff0c;如何更好地提高用户体验成为了行业共同…

Linux——编译器gcc/g++、调试器gdb以及自动化构建工具makefilemake详解

编译器—gcc/g、调试器—gdb以及自动化构建工具—makefile&&make 文章目录 编译器—gcc/g、调试器—gdb以及自动化构建工具—makefile&&make1. 编译器——gcc/g1.1 生成可执行文件与修改默认可执行文件1.2 程序的翻译过程以及对应的gcc选项1.2.1 预处理 gcc -E…

VMware Workstation 与 Device/Credential Guard 不兼容 解决办法

问题描述 问题描述&#xff1a; VMware 启动虚拟机会报错。无法运行。 错误信息&#xff1a;VMware Workstation 与 Device/Credential Guard 不兼容。在禁用 Device/Credential Guard 原因分析&#xff1a; 通常原因是 Window 系统开启了 内置的Hyper-V 虚拟机。 解决方案&…

C++模拟实现——红黑树封装set和map

一、红黑树迭代器的实现 基本的框架和实现链表的迭代器思路是一样的&#xff0c;都是对指针进行封装处理&#xff0c;然后实现一些基本的运算符重载&#xff0c;最重要的是operator&#xff0c;需要不递归的实现走中序的规则&#xff0c;这里只实现那最核心的几个基本功能&…

Typecho框架漏洞

这里说的框架漏洞只适用于1.2.0版本及以下的版本 这里说的漏洞是xss漏洞&#xff0c;学过渗透的应该都学过&#xff0c;我在这里就不过多阐述了&#xff0c;下面我们直接进入正题 直接在这个地方插入网址&#xff0c;后面再接上html代码即可&#xff0c;代码如下&#xff1a; …

软件测试: 测试用例

一. 软件测试四要素 测试环境,操作步骤,测试数据,预期结果 二. 基于需求进行测试用例的设计 基于需求设计测试用例是测试设计和开发测试用例的基础,第一步就要分析测试需求,验证需求是否正确,完整,无二义性,并且逻辑自洽.在需求正确的基础上细化测试需求,从测试需求提炼出一…

高防CDN为什么可以防DDOS攻击

CDN的全称是ContentDeliveryNetwork&#xff0c;即内容分发网络&#xff0c;顾名思义&#xff0c;它是一个分布式节点网络(也称为边缘服务器)&#xff0c;CDN节点具有缓存内容的功能&#xff0c;使用户可以在不获取源服务器数据的情况下就近获取所需内容&#xff0c;提高客户访…

你知道什么是SaaS吗?

你知道什么是SaaS吗&#xff1f; 云服务架构的三个概念 PaaS 英文就是 Platform-as-a-Service&#xff08;平台即服务&#xff09;PaaS&#xff0c;某些时候也叫做中间件。就是把客户采用提供的开发语言和工具&#xff08;例如Java&#xff0c;python, .Net等&#xff09;开…

【Django使用】django经验md文档10大模块。第4期:Django数据库增删改查

Django的主要目的是简便、快速的开发数据库驱动的网站。它强调代码复用&#xff0c;多个组件可以很方便的以"插件"形式服务于整个框架&#xff0c;Django有许多功能强大的第三方插件&#xff0c;你甚至可以很方便的开发出自己的工具包。这使得Django具有很强的可扩展…

【Highway-env】IntersectionEnv代码阅读

文章目录 主要完成任务代码结构1.action space2.default_config3.reward_agent_rewards_agent_reward_reward_rewards小结 4.terminated & truncated5.reset_make_road_make_vehicles_spawn_vehicle 6.step 主要完成任务 IntersectionEnv继承自AbstractEnv,主要完成以下4个…

基于单片机16路抢答器仿真系统

**单片机设计介绍&#xff0c; 基于单片机16路抢答器仿真系统 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的16路抢答器仿真系统是一种用于模拟和实现抢答竞赛的系统。该系统由硬件和软件两部分组成。 硬件方面&am…

单张图像3D重建:原理与PyTorch实现

近年来&#xff0c;深度学习&#xff08;DL&#xff09;在解决图像分类、目标检测、语义分割等 2D 图像任务方面表现出了出色的能力。DL 也不例外&#xff0c;在将其应用于 3D 图形问题方面也取得了巨大进展。 在这篇文章中&#xff0c;我们将探讨最近将深度学习扩展到单图像 3…

CICD 持续集成与持续交付——git

git使用 [rootcicd1 ~]# yum install -y git[rootcicd1 ~]# mkdir demo[rootcicd1 ~]# cd demo/ 初始化版本库 [rootcicd1 demo]# git init 查看状态 [rootcicd1 demo]# git status[rootcicd1 demo]# git status -s #简化输出 [rootcicd1 demo]# echo test > README.md[roo…

计算机毕业设计选题推荐-内蒙古旅游微信小程序/安卓APP-项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

控制您的音乐、视频等媒体内容

跨多个 Chrome 标签页播放音乐或声音 在计算机上打开 Chrome 。在标签页中播放音乐、视频或其他任何有声内容。您可以停留在该标签页上&#xff0c;也可以转到别处。要控制声音&#xff0c;请在右上角点击“媒体控件”图标 。您可暂停播放、转到下一首歌曲/下一个视频&#xf…