05-树9 Huffman Codes

05-树9 Huffman Codes (30分)

题目描述

In 1953, David A. Huffman published his paper “A Method for the Construction of Minimum-Redundancy Codes”, and hence printed his name in the history of computer science. As a professor who gives the final exam problem on Huffman codes, I am encountering a big problem: the Huffman codes are NOT unique. For example, given a string “aaaxuaxz”, we can observe that the frequencies of the characters ‘a’, ‘x’, ‘u’ and ‘z’ are 4, 2, 1 and 1, respectively. We may either encode the symbols as {‘a’=0, ‘x’=10, ‘u’=110, ‘z’=111}, or in another way as {‘a’=1, ‘x’=01, ‘u’=001, ‘z’=000}, both compress the string into 14 bits. Another set of code can be given as {‘a’=0, ‘x’=11, ‘u’=100, ‘z’=101}, but {‘a’=0, ‘x’=01, ‘u’=011, ‘z’=001} is NOT correct since “aaaxuaxz” and “aazuaxax” can both be decoded from the code 00001011001001. The students are submitting all kinds of codes, and I need a computer program to help me determine which ones are correct and which ones are not.

Input Specification

Each input file contains one test case. For each case, the first line gives an integer N (2 ≤ N ≤ 63), then followed by a line that contains all the N distinct characters and their frequencies in the following format:

c[1] f[1] c[2] f[2] ... c[N] f[N]

where c[i] is a character chosen from {‘0’ - ‘9’, ‘a’ - ‘z’, ‘A’ - ‘Z’, ‘_’}, and f[i] is the frequency of c[i] and is an integer no more than 1000. The next line gives a positive integer M (≤1000), then followed by M student submissions. Each student submission consists of N lines, each in the format:

c[i] code[i]

where c[i] is the i-th character and code[i] is an non-empty string of no more than 63 '0’s and '1’s.

Output Specification

For each test case, print in each line either “Yes” if the student’s submission is correct, or “No” if not.

Note: The optimal solution is not necessarily generated by Huffman algorithm. Any prefix code with code length being optimal is considered correct.

Sample Input

7
A 1 B 1 C 1 D 3 E 3 F 6 G 6
4
A 00000
B 00001
C 0001
D 001
E 01
F 10
G 11
A 01010
B 01011
C 0100
D 011
E 10
F 11
G 00
A 000
B 001
C 010
D 011
E 100
F 101
G 110
A 00000
B 00001
C 0001
D 001
E 00
F 10
G 11

Sample Output

Yes
Yes
No
No

解题思路

本题的大意是:由于哈夫曼编码的不唯一性,从 WPL 的角度,最优编码的个数可以有多个,我们现在需要判断输入的编码是否是可能的一种最优编码类型。
例如,对于题目中所给出的测试案例,以下两种哈夫曼编码类型都是可能的:

1

两棵树的 WPL 均为53.

最小堆的实现

哈夫曼树的创建的前提是需要一个最小堆,代码如下:

// 哈夫曼树结点定义
typedef struct HuffmanTree {int frequency; // 表示每个字符出现的频率struct HuffmanTree *left;struct HuffmanTree *right;
} HuffmanTree;#define root 0 // 初始化堆的根结点为0
#define parent(idx) ( idx / 2 ) // 查找idx的父结点索引
#define lchild(idx) ( idx * 2 + 1 ) // 查找idx的左孩子结点索引
#define rchild(idx) ( idx * 2 + 2 ) // 查找idx的右孩子结点索引// 最小堆的定义
typedef struct Heap {HuffmanTree **data; // 存储值类型为HuffmanTree的数组int size; // 结点的个数int capacity; // 堆的最大容量
} MinHeap;// 创建及初始化最小堆
MinHeap *CreateHeap( int size ) {MinHeap *heap = ( MinHeap *) malloc(sizeof(MinHeap) );heap->data = ( HuffmanTree **) malloc(sizeof(HuffmanTree *) * size );heap->size = 0;heap->capacity = size;return heap;
}// 父子结点的交换,直接进行结点的交换
void swap( MinHeap *heap, int idx1, int idx2 ) {HuffmanTree *temp = heap->data[idx1];heap->data[idx1] = heap->data[idx2];heap->data[idx2] = temp;
}// 父子结点的比较,比较的依据是frequency
bool compare(MinHeap *heap, int idx1, int idx2 ) {return heap->data[idx1]->frequency < heap->data[idx2]->frequency;
}// 堆的上浮操作,通常用于向堆中插入元素,进行结点的调整
void Heap_Float(MinHeap *heap, int idx ) {int par = parent(idx);while ( par >= root ) {// 如果当前结点大于其父结点,进行交换if ( compare(heap, idx, par) ) {swap(heap, par, idx);idx = par;par = parent(idx);}else break;}
}// 判断堆是否已满
bool isFull(MinHeap *heap ) {return heap->size == heap->capacity;
}// 向堆中插入元素,与堆栈类似,不过需要进行值的上浮操作
bool Heap_Push(MinHeap *heap, HuffmanTree *val ) {if ( isFull(heap) ) {return false;}heap->data[heap->size ++] = val;Heap_Float(heap, heap->size - 1);return true;
}// 堆的下沉操作,通常用于向堆中删除元素,进行结点的调整
void Heap_Sink(MinHeap *heap, int idx ) {int child = lchild(idx);while ( child < heap->size ) {// 查找左右孩子中值最小的if ( rchild(idx) < heap->size ) {// 如果右孩子的值更小,应取右孩子的值if ( compare(heap, rchild(idx), child) ) {child = rchild(idx);}}// 如果当前孩子结点大于当前结点,进行交换if ( compare(heap, child, idx) ) {swap(heap, child, idx);idx = child;child = lchild(idx);}else {break;}}
}// 判断堆是否为空
bool isEmpty(MinHeap *heap ) {return heap->size == 0;
}// 在堆中删除元素,与堆栈类似,不过需要进行值的下沉操作
HuffmanTree *Heap_Popped(MinHeap *heap ) {if ( isEmpty(heap) ) {return NULL;}HuffmanTree *temp = heap->data[root];heap->data[root] = heap->data[-- heap->size];Heap_Sink(heap, root);return temp;
}// 释放堆
void Heap_Free(MinHeap *heap ) {free(heap->data);free(heap);
}

哈夫曼树的创建

整体思路较为简单,哈夫曼树创建完成的同时,堆就为空了,其作用就完成了,可以进行空间的释放。

HuffmanTree *CreateHuffmanTree( MinHeap *heap ) {HuffmanTree *HT = NULL;int size = heap->size;for ( int i = 1; i < size; i ++ ) {HT = ( HuffmanTree *) malloc( sizeof(HuffmanTree) );HT->left = Heap_Popped(heap);HT->right = Heap_Popped(heap);HT->frequency = HT->left->frequency + HT->right->frequency;Heap_Push(heap, HT);}return Heap_Popped(heap);
}

最优编码的判定

从哈夫曼编码的特点进行判断:

  • WPL 最小
  • 满足前缀码
  • 没有度唯一的结点
WPL 最小

在进行判断前,需要先计算最优编码的 WPL 的值,这也是前面创建哈夫曼树的目的。结合 WPL 的计算公式与递归,下面的代码应该好理解。

int getWPL( HuffmanTree *HT, int depth ) {if ( HT->left == NULL && HT->right == NULL ) {return depth * HT->frequency;}return getWPL(HT->left, depth + 1 ) + getWPL(HT->right, depth + 1);
}
满足前缀码及度不为1

我们没有办法直接通过输入直接看出其是否是前缀码以及度是否为1,最直接的办法就是,根据输入直接创建一棵树,不过与哈夫曼树的创建相比要轻松许多,因为此时创建树其实就是普通的二叉树的创建,与二叉树的层序生成过程类似。

#define NULLVALUE -1bool isPrefixCode( HuffmanTree *T, const char *code, int fre ) {HuffmanTree *temp = T;for ( int i = 0; code[i]; i ++ ) {// 当前code与上一次的code具有同一个前缀码,返回falseif ( temp->frequency != NULLVALUE ) {return false;}if ( code[i] == '0' ) {if ( temp->left == NULL ) {temp->left = ( HuffmanTree *) malloc( sizeof(HuffmanTree) );temp->left->frequency = NULLVALUE;temp->left->left = temp->left->right = NULL;}temp = temp->left;}else if ( code[i] == '1' ) {if ( temp->right == NULL ) {temp->right = ( HuffmanTree *) malloc( sizeof(HuffmanTree) );temp->right->frequency = NULLVALUE;temp->right->left = temp->right->right = NULL;}temp = temp->right;}}// 叶子结点且为空值if ( temp->left == NULL && temp->right == NULL && temp->frequency == NULLVALUE ) {temp->frequency = fre;return true;}return false;
}
主函数的实现

整个程序框架的逻辑如下:

int main(void) {int M, N;scanf("%d", &N); // 读取字符集的大小Ngetchar(); // 清除输入缓冲区的换行符MinHeap *heap = CreateHeap(N); // 创建一个最小堆,用于构建霍夫曼树char in; // 用于存储输入的字符int fre; // 用于存储字符的频率HuffmanTree *temp = NULL; // 临时变量,用于创建新的霍夫曼树节点int frequency[N]; // 数组,存储每个字符的频率// 读取每个字符及其频率,并将其压入堆中for (int i = 0; i < N; i++) {scanf("%c %d", &in, &fre);frequency[i] = fre; // 存储频率temp = (HuffmanTree *) malloc(sizeof(HuffmanTree)); // 分配新的霍夫曼树节点temp->frequency = fre; // 设置频率temp->left = temp->right = NULL; // 初始化左右子树为空Heap_Push(heap, temp); // 将新节点压入堆中getchar(); // 清除输入缓冲区的换行符}scanf("%d", &M); // 读取要检查的编码方案的数量Mgetchar(); // 清除输入缓冲区的换行符// 构建霍夫曼树,并计算最优WPLHuffmanTree *HT = CreateHuffmanTree(heap);int trueWPL = getWPL(HT, root); char code[SIZE]; // 用于存储输入的编码// 对于每个要检查的编码方案for (int i = 0; i < M; i++) {bool flag = true; // 标志,用于判断编码方案是否有效int WPL = 0; // 当前编码方案的WPLHuffmanTree *T = (HuffmanTree *) malloc(sizeof(HuffmanTree)); // 创建一个临时霍夫曼树节点T->frequency = NULLVALUE; // 设置为空值T->left = T->right = NULL; // 初始化左右子树为空// 读取每个字符的编码,并验证编码方案for (int j = 0; j < N; j++) {scanf("%c %s", &in, code);getchar(); // 清除输入缓冲区的换行符// 如果编码不是前缀码或者长度超过限制,则标记为无效if (flag && (strlen(code) > N - 1 || !isPrefixCode(T, code, frequency[j]))) {flag = false; // 标记编码方案无效}WPL += strlen(code) * frequency[j]; // 计算当前编码方案的WPL}// 如果编码方案有效且WPL与最优WPL相等,则输出"Yes",否则输出"No"if (flag && WPL == trueWPL) {printf("Yes\n");}else {printf("No\n");}HuffmanTreeFree(T); // 释放临时霍夫曼树节点}Heap_Free(heap); // 释放堆内存return 0; // 程序结束
}

全部代码

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>#define SIZE 64
#define NULLVALUE -1typedef struct HuffmanTree {int frequency;struct HuffmanTree *left;struct HuffmanTree *right;
} HuffmanTree;#define root 0
#define parent(idx) ( idx / 2 )
#define lchild(idx) ( idx * 2 + 1 )
#define rchild(idx) ( idx * 2 + 2 )typedef struct Heap {HuffmanTree **data;int size;int capacity;
} MinHeap;MinHeap *CreateHeap( int size ) {MinHeap *heap = ( MinHeap *) malloc(sizeof(MinHeap) );heap->data = ( HuffmanTree **) malloc(sizeof(HuffmanTree *) * size );heap->size = 0;heap->capacity = size;return heap;
}void swap( MinHeap *heap, int idx1, int idx2 ) {HuffmanTree *temp = heap->data[idx1];heap->data[idx1] = heap->data[idx2];heap->data[idx2] = temp;
}bool compare(MinHeap *heap, int idx1, int idx2 ) {return heap->data[idx1]->frequency < heap->data[idx2]->frequency;
}void Heap_Float(MinHeap *heap, int idx ) {int par = parent(idx);while ( par >= root ) {if ( compare(heap, idx, par) ) {swap(heap, par, idx);idx = par;par = parent(idx);}else break;}
}bool isFull(MinHeap *heap ) {return heap->size == heap->capacity;
}bool Heap_Push(MinHeap *heap, HuffmanTree *val ) {if ( isFull(heap) ) {return false;}heap->data[heap->size ++] = val;Heap_Float(heap, heap->size - 1);return true;
}void Heap_Sink(MinHeap *heap, int idx ) {int child = lchild(idx);while ( child < heap->size ) {if ( rchild(idx) < heap->size ) {if ( compare(heap, rchild(idx), child) ) {child = rchild(idx);}}if ( compare(heap, child, idx) ) {swap(heap, child, idx);idx = child;child = lchild(idx);}else {break;}}
}bool isEmpty(MinHeap *heap ) {return heap->size == 0;
}HuffmanTree *Heap_Popped(MinHeap *heap ) {if ( isEmpty(heap) ) {return NULL;}HuffmanTree *temp = heap->data[root];heap->data[root] = heap->data[-- heap->size];Heap_Sink(heap, root);return temp;
}void Heap_Free(MinHeap *heap ) {free(heap->data);free(heap);
}HuffmanTree *CreateHuffmanTree( MinHeap *heap ) {HuffmanTree *HT = NULL;int size = heap->size;for ( int i = 1; i < size; i ++ ) {HT = ( HuffmanTree *) malloc( sizeof(HuffmanTree) );HT->left = Heap_Popped(heap);HT->right = Heap_Popped(heap);HT->frequency = HT->left->frequency + HT->right->frequency;Heap_Push(heap, HT);}return Heap_Popped(heap);
}int getWPL( HuffmanTree *HT, int depth ) {if ( HT->left == NULL && HT->right == NULL ) {return depth * HT->frequency;}return getWPL(HT->left, depth + 1 ) + getWPL(HT->right, depth + 1);
}bool isPrefixCode( HuffmanTree *T, const char *code, int fre ) {HuffmanTree *temp = T;for ( int i = 0; code[i]; i ++ ) {if ( temp->frequency != NULLVALUE ) {return false;}if ( code[i] == '0' ) {if ( temp->left == NULL ) {temp->left = ( HuffmanTree *) malloc( sizeof(HuffmanTree) );temp->left->frequency = NULLVALUE;temp->left->left = temp->left->right = NULL;}temp = temp->left;}else if ( code[i] == '1' ) {if ( temp->right == NULL ) {temp->right = ( HuffmanTree *) malloc( sizeof(HuffmanTree) );temp->right->frequency = NULLVALUE;temp->right->left = temp->right->right = NULL;}temp = temp->right;}}if ( temp->left == NULL && temp->right == NULL && temp->frequency == NULLVALUE ) {temp->frequency = fre;return true;}return false;
}void HuffmanTreeFree( HuffmanTree *HT ) {if ( HT ) {HuffmanTreeFree(HT->left);HuffmanTreeFree(HT->right);free(HT);}
}int main(void)
{int M, N;scanf("%d", &N);getchar();MinHeap *heap = CreateHeap(N);char in; int fre;HuffmanTree *temp = NULL;int frequency[N];for ( int i = 0; i < N; i ++ ) {scanf("%c %d", &in, &fre);frequency[i] = fre;temp = ( HuffmanTree * ) malloc( sizeof(HuffmanTree) );temp->frequency = fre;temp->left = temp->right = NULL;Heap_Push(heap, temp);getchar();}scanf("%d", &M);getchar();HuffmanTree *HT = CreateHuffmanTree(heap);int trueWPL = getWPL(HT, root);char code[SIZE];for ( int i = 0; i < M; i ++) {bool flag = true;int WPL = 0;HuffmanTree *T = ( HuffmanTree *) malloc( sizeof(HuffmanTree) );T->frequency = NULLVALUE;T->left = T->right = NULL;for ( int j = 0; j < N; j ++ ) {scanf("%c %s", &in, code);getchar();if ( flag && ( strlen(code) > N - 1 || !isPrefixCode(T, code, frequency[j]) ) ) {flag = false;}WPL += strlen(code) * frequency[j];}if ( flag && WPL == trueWPL ) {printf("Yes\n");}else {printf("No\n");}HuffmanTreeFree(T);}Heap_Free(heap);return 0;
}

测试结果

测试结果

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

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

相关文章

并行执行的4种类别——《OceanBase 并行执行》系列 4

OceanBase 支持多种类型语句的并行执行。在本篇博客中&#xff0c;我们将根据并行执行的不同类别&#xff0c;分别详细阐述&#xff1a;并行查询、并行数据操作语言&#xff08;DML&#xff09;、并行数据定义语言&#xff08;DDL&#xff09;以及并行 LOAD DATA 。 《并行执行…

寻找志同道合的小伙伴,让生活更加多彩

在繁忙的生活中&#xff0c;我们时常渴望找到一个可以倾诉心声、分享喜悦和烦恼的角落。有时候&#xff0c;一个简单的聊天就能让心情变得豁然开朗。而今天&#xff0c;我想向大家介绍一个可以让生活更加多彩的小天地——那是一个充满活力和温暖的QQ群。 群号&#xff1a;78004…

wandb: - 0.000 MB of 0.011 MB uploaded持续出现的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

安卓通信方式简介

目录 一、Binder二、Socket三、Binder与Socket四、Handler 一、Binder Binder作为Android系统提供的一种IPC机制&#xff0c;无论从系统开发还是应用开发&#xff0c;都是Android系统中最重要的组成。 二、Socket Socket通信方式也是C/S架构&#xff0c;比Binder简单很多。在…

如何将图片表格转成excel?分享3种好用的软件!

在信息爆炸的时代&#xff0c;我们每天都会接触到大量的图片表格。这些表格中可能包含着我们需要的各种数据和信息&#xff0c;但是如何将它们快速、准确地转化为Excel格式&#xff0c;以便我们进行编辑、分析呢&#xff1f;今天&#xff0c;就让我们一起来探讨一下如何将图片表…

发票审核如何自查?报销没有发票,如何处理?

在财务管理中&#xff0c;发票是非常重要的一项凭证&#xff0c;是费用核算和税务申报的重要依据&#xff0c;但光靠发票入账可能会被定义为虚开。 一、费用报销审核必看的6个要点 1、票据与实际业务吻合 这是费用报销中最基本的常识&#xff0c;比如&#xff1a;采购一批物料&…

(十)JSP教程——config对象

config对象是脚本程序配置对象&#xff0c;表示当前JSP页面的配置信息。由于JSP页面通常无需配置&#xff0c;因此该对象在JSP页面中比较少见。 config对象可以读取一些初始化参数的值&#xff0c;而这些参数一般在web.xml配置文件中可以看到&#xff0c;并通过config对象的相应…

4D 成像毫米波雷达:新型传感器助力自动驾驶

1 感知是自动驾驶的首要环节&#xff0c;高性能传感器必不可少 感知环节负责对侦测、识别、跟踪目标&#xff0c;是自动驾驶实现的第一步。自动驾驶的实现&#xff0c;首先要能够准确理解驾驶环境信息&#xff0c;需要对交通主体、交通信号、环境物体等信息进行有效捕捉&#x…

Paddle 实现DCGAN

传统GAN 传统的GAN可以看我的这篇文章&#xff1a;Paddle 基于ANN&#xff08;全连接神经网络&#xff09;的GAN&#xff08;生成对抗网络&#xff09;实现-CSDN博客 DCGAN DCGAN是适用于图像生成的GAN&#xff0c;它的特点是&#xff1a; 只采用卷积层和转置卷积层&#x…

Oracle体系结构初探:闪回技术

在Oracle体系结构初探这个专栏中&#xff0c;已经写过了REDO、UNDO等内容。觉得可以开始写下有关备份恢复的内容。闪回技术 — Oracle数据库备份恢复机制的一种。它可以在一定条件下&#xff0c;高效快速的恢复因为逻辑错误&#xff08;误删误更新等&#xff09;导致的数据丢失…

数据库表自增主键超过代码Integer长度问题

数据库自增主键是 int(10) unsigned类型的字段&#xff0c;int(M) 中 M指示最大显示宽度&#xff0c;不代表存储长度&#xff0c;实际int(1)也是可以存储21.47亿长度的数字&#xff0c;如果是无符号类型的&#xff0c;那么可以从0~42.94亿。 我们的表主键自增到21.47亿后&#…

应用层协议之 DNS 协议

DNS 就是一个域名解析系统。域名就是网址&#xff0c;类似于 www.baidu.com。网络上的服务器想要访问它&#xff0c;就得需要它对应的 IP 地址&#xff0c;同时&#xff0c;每个域名对对应着一个 / N个 IP 地址&#xff08;即对应多台服务器&#xff09;。 因此&#xff0c;为了…

HarmonyOS开发案例:【生活健康app之实现打卡功能】(2)

实现打卡功能 首页会展示当前用户已经开启的任务列表&#xff0c;每条任务会显示对应的任务名称以及任务目标、当前任务完成情况。用户只可对当天任务进行打卡操作&#xff0c;用户可以根据需要对任务列表中相应的任务进行点击打卡。如果任务列表中的每个任务都在当天完成则为…

安装vmware station记录

想学一下linux,花了3个多小时&#xff0c;才配置好了&#xff0c;记录一下 安装vm12,已配置linux系统 报错&#xff0c;VMware Workstation 与 Device/Credential Guard 不兼容解决方案&#xff0c;网上说有不成功的&#xff0c;电脑蓝屏&#xff0c;选择装vm16试试 vm16 在…

【JVM】JVM规范作用及其核心

目录 认识JVM规范的作用 JVM规范定义的主要内容 认识JVM规范的作用 Java 虚拟机规范为不同的硬件平台提供了一种编译Java技术代码的规范。 Java虚拟机认得不是源文件&#xff0c;认得是编译过后的class文件&#xff0c;它是对这个class文件做要求、起作用的&#xff0c;而并…

算法设计与分析 动态规划/回溯

1.最大子段和 int a[N]; int maxn(int n) {int tempa[0];int ans0;ansmax(temp,ans);for(int i1;i<n;i){if(temp>0){tempa[i];}else tempa[i];ansmax(temp,ans);}return ans; } int main() {int n,ans0;cin>>n;for(int i0;i<n;i) cin>>a[i];ansmaxn(n);co…

LeetCode例题讲解:876.链表的中间结点

给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。 如果有两个中间结点&#xff0c;则返回第二个中间结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[3,4,5] 解释&#xff1a;链表只有一个中间结点&#xff0c;值为 3 。…

kubernetes删除命名空间下所有资源

kubernetes强制删除命名空间下所有资源 在 Kubernetes 中&#xff0c;当一个命名空间处于 Terminating 状态但不会完成删除过程时&#xff0c;通常是因为内部资源没有被正确清理。要强制删除这个命名空间及其所有资源&#xff0c;你可以采取以下步骤&#xff1a; 1. 确认命名空…

渲染农场评测:6大热门云渲染平台全面比较

在3D行业中&#xff0c;选择一个合适的云渲染平台可能会令许多专业人士感到难以抉择。为此&#xff0c;我们精心准备了6家流行云渲染平台的详尽评测&#xff0c;旨在为您的决策过程提供实用的参考和支持。 目前&#xff0c;市面上主要的3D网络渲染平台包括六大服务商&#xff0…

张驰咨询六西格玛黑带培训,上海开班,质量精英的摇篮!

一、课程背景与意义 在当今竞争激烈的市场环境中&#xff0c;企业要想立于不败之地&#xff0c;就必须不断提升自身的核心竞争力。而六西格玛作为一种先进的质量管理工具和方法&#xff0c;已经被越来越多的企业所采纳。通过六西格玛黑带培训&#xff0c;学员们可以系统地掌握…