PTA习题解析——修理牧场(2 种解法)
禁止码迷,布布扣,豌豆代理,码农教程,爱码网等第三方爬虫网站爬取!
题目详情
题干
输入样例
8
4 5 1 2 1 3 1 1
输出样例
49
哈夫曼树
情景模拟
我们如果按照分割木块的思想去分析,会显得很不直观,所以我们直接通过我们的目标来组合出最花费的情况,那么这就很显然要使用贪心算法来解决。如图所示用圆形来表示小木块。
要保证花费更小,就需要让较短的木块后生成,较长的木块先生成,因此我选择较小的两个木块,还原为它们被分割出来之前的状态。即选择两个长度为 1 的木块,组合成一个长度为 2 的木块。
接下来我要去生成下一个木块,选择两个目前最短的木块,还是两个 1 长度的木块,组合成长度为 2 的木块。
重复上述操作。
模拟完毕,观察一下我们发现,我们已经还原出了木块的每一次应该如何切割,由于木块切割费用与其长度相等,因此我们把黄色源泉的数字都加起来,发现正好是 49。此时我可以说,问题已经解决了,我们把目标木块当成一个个结点,长度当做权,这道题不就是要建一棵哈夫曼树嘛。
代码实现
将上述函数封装好,并添加读取数据的代码即可实现。
#include<iostream>
using namespace std;
typedef struct {
int weight; //权值
int parent, lchild, rchild; //指向双亲、左右孩子结点的游标
}HTNode, * HuffmanTree;
void CreatHuffmanTree(HuffmanTree& HT, int n); //建哈夫曼树
void Select(HuffmanTree HT, int k, int &idx1, int &idx2); //找孩子结点
int main()
{
int n;
HuffmanTree HT;
int sum = 0;
cin >> n;
CreatHuffmanTree(HT, n); //建哈夫曼树
for (int i = n + 1; i < 2 * n; i++) //算总花费
{
sum += HT[i].weight;
}
cout << sum;
}
void CreatHuffmanTree(HuffmanTree& HT, int n)
{
int m = 2 * n - 1;
int idx1, idx2;
int i;
if (n < 0) //树的结点数为 0,即空树
return;
//二叉树初始化
HT = new HTNode[m + 1]; //0 号元素不使用,因此需要分配 m + 1 个单元,HT[m] 为根结点
for (i = 1; i <= m; i++)
{
HT[i].parent = HT[i].lchild = HT[i].rchild = 0; //初始化结点的游标均为 0
}
for (i = 1; i <= n; i++)
cin >> HT[i].weight; //输入各个结点的权值
//建哈夫曼树
for (i = n + 1; i <= m; i++)
{
Select(HT, i , idx1, idx2);
//在 HF[k](1 <= k <= i - 1)中选择两个双亲游标为 0 且权值最小的结点,并返回他们在 HT 中的下标 idx1,idx2
HT[idx1].parent = HT[idx2].parent = i;
//得到新结点 i,idx1 和 idx2 从森林中删除,修改它们的双亲为 i
HT[i].lchild = idx1;
HT[i].rchild = idx2; //结点 i 的左右子结点为 idx1 和 idx2
HT[i].weight = HT[idx1].weight + HT[idx2].weight; //结点 i 的权值为左右子结点权值之和
}
}
void Select(HuffmanTree HT, int k, int& idx1, int& idx2)
{
int min1, min2;
min1 = min2 = 99999999999; //不能太小
for (int i = 1; i < k; i++)
{
if (HT[i].parent == 0 && min1 > HT[i].weight)
{
if (min1 < min2)
{
min2 = min1;
idx2 = idx1;
}
min1 = HT[i].weight;
idx1 = i;
}
else if (HT[i].parent == 0 && min2 > HT[i].weight)
{
min2 = HT[i].weight;
idx2 = i;
}
}
}
优先级队列(非 STL 库)
情景模拟
为了使费用最省,我们使用贪心算法的思想,每一次选择最小的两段木头拼回去,直到将所有木头拼成一段完整的木头,每次一拼接都计算一次费用。我们发现,优先级队列也是可以实现贪心算法的。
我们首先需要先把这个队列修改成小顶堆,方便我们实现优先级队列。
接下来令两个元素出队列,计算一次费用,然后将两个元素之和的数字入队列。
重复上述操作,使的队列只剩一个元素。
解法分析
在这里我们可以看出优先级队列是可以解决问题的,此时的问题是我该怎么控制堆中的数据元素个数?通过观察,每一次是出堆 2 个元素,入堆 1 个元素,也就是说每次的净出堆元素是 1 个,那么就在堆中元素为 1 时结束这个流程就行了。
接下来就是如何调整堆的问题了,比较粗暴的方式是每次更改之后都建初堆,但是这样效率和哈夫曼树差不多,优化不是很明显。根据我们刚刚的分析,既然有 2 次出堆,那么在出堆之后保证剩下的元素也是堆就可以了,那么只需要 2 次调整堆。比较方便的操作是第一次出堆时,拿堆的最后一个元素到堆顶调整堆,第二次出堆时直接把入堆元素填充到堆顶调整堆。
代码实现
#include<iostream>
using namespace std;
void heapify(int a_heap[], int idx1, int idx2);
void creatHeap(int a_heap[], int n);
int main()
{
int a_heap[10001];
int count; //堆中剩余元素个数
int heap_top; //暂时保存第一个堆顶
int money = 0; //总费用
cin >> count;
for (int i = 1; i <= count; i++)
{
cin >> a_heap[i];
}
creatHeap(a_heap, count); //建初堆
while (count != 1)
{
heap_top = a_heap[1]; //提取第一个最小花费
a_heap[1] = a_heap[count--];
heapify(a_heap, 1, count); //调整堆寻找第二个最小花费
a_heap[1] += heap_top; //提取第二个最小花费,将花费相加,入堆
money += a_heap[1]; //更新总花费
heapify(a_heap, 1, count); // 调整堆,准备下一次拼接
}
cout << money;
return 0;
}
void heapify(int a_heap[], int idx1, int idx2) //调整堆,细节见上文
{
int insert_node = a_heap[idx1];
for (int i = 2 * idx1; i <= idx2; i *= 2)
{
if (i < idx2 && a_heap[i] > a_heap[i + 1])
{
i++;
}
if (insert_node <= a_heap[i])
{
break;
}
a_heap[idx1] = a_heap[i];
idx1 = i;
}
a_heap[idx1] = insert_node;
}
void creatHeap(int a_heap[], int n) //建初堆,细节见上文
{
for (int i = n / 2; i > 0; i--)
{
heapify(a_heap, i, n);
}
}