K元归并树——贪心算法
问题介绍
两个分别包含n个和m个记录的已分类文件可以在O(n+m)时间内归并在一起而得到一个分类文件。当要把两个以上的已分类文件归并在一起时,可以通过成对地重复归并已分类的文件来完成。例如:假定X1,X2,X3,X4是要归并的文件,则可以首先把X1和X2归并成文件Y1,然后将Y1和X3归并成Y2,最后将Y2和X4归并,从而得到想要的分类文件;也可以先把X1和X2归并成Y1,然后将X3和X4归并成Y2,最后归并Y1和Y2而得到想要的分类文件。给出n个文件,则有许多种把这些文件成对地归并成一个单一分类文件的方法。不同的配对法要求不同的计算时间。
问题是确定一个把n个已分类文件归并在一起的最优方法(即需要最少比较的方法)
像刚才所描述的归并模式称为二路归并模式(每一个归并步包含两个文件的归并)。二路归并模式可以用二元归并树来表示。叶结点被画成方块形,表示已知的文件。这些叶结点称为外部结点。剩下的结点被画成圆圈,称为内部结点。每个内部结点恰好有两个儿子,表示把它的两个儿子所表示的文件归并而得到的文件。每个结点中的数字都是那个结点所表示的文件的长度(记录数)。一个i级结点在距离根为i-1的地方,在i级结点上的文件的记录都要移动i-1次。如果di是由根到代表文件Fi的外部结点的距离,qi是Fi的长度,则这颗二元归并树的记录移动总量是 。这个和数叫做这颗树的带权外部路径长度。一个最优二路归并模式与一颗具有最小权外部路径的二元树相对应。
生成归并树的贪心方法也适用于K路归并的情况。相应的归并树是一颗K元树。所有的内部结点的度数必须为K,所以对于n的某些值,就不与K元归并树相对应。例如:当K=3时,就不存在具有n=2个外部结点的K元归并树。所以有必要引进一定量的“虚”外部结点。每一个虚外部结点被赋以0值的qi。这个虚值不会影响所产生的K元树的带权外部路径长度。
问题分析
算法思路分析
问题简述为:“确定一个把n个已分类文件归并在一起的最优方法”
即:最优K路归并模式问题,也就是如何构建一棵具有最短带权外部路径的K叉树
而为了构造具有最短带权外部路径
的树,感性的、直观的想法是将复杂的(即文件Fi的长度qi长的) 置于上层,简单的置于下层;
即,先解决K元短文件之间的归并,再考虑剩下的以及已经归并了的文件中的K元“简单文件”的归并。
这个思路,以及构造具有最短带权外部路径
的树,毫不意外想到使用哈夫曼算法来构建哈夫曼树
所以问题可以等价替换为:
用所给的n个文件(Fi)的长度值(qi)来构建K叉哈夫曼树,从而得到最优值(最少比较次数)和最优解(K路归并的分配策略)
证明:K叉哈夫曼树是最优树
设:T(n) 为带权节点
q
1
⩽
q
2
⩽
…
⩽
q
n
−
1
⩽
q
n
(
n
>
2
)
\ q_1 \leqslant q_2 \leqslant … \leqslant q_{n-1} \leqslant q_n(n > 2)
q1⩽q2⩽…⩽qn−1⩽qn(n>2)的最优树的带权外部路径长度。
证明 哈夫曼树是最优树 等价于:
证明:
{
Q
1
:
带
权
q
1
,
q
2
,
…
,
q
K
为
兄
弟
节
点
Q
2
:
最
优
树
收
缩
与
展
开
同
为
最
优
树
Q
3
:
最
优
树
的
合
并
仍
为
最
优
树
\begin{cases} \mathbf{Q_1}:带权q_1,q_2,…,q_K为兄弟节点 \\ \mathbf{Q_2}:最优树收缩与展开同为最优树 \\ \mathbf{Q_3}:最优树的合并仍为最优树 \end{cases}
⎩⎪⎨⎪⎧Q1:带权q1,q2,…,qK为兄弟节点Q2:最优树收缩与展开同为最优树Q3:最优树的合并仍为最优树
证明 Q 1 : 带 权 q 1 , q 2 , … , q K 为 兄 弟 节 点 \mathbf{Q_1}:带权q_1,q_2,…,q_K为兄弟节点 Q1:带权q1,q2,…,qK为兄弟节点
1、设:
N
o
d
e
m
a
x
Node_{max}
Nodemax为最优树中通路长度最长的内节点,则其一定有K个叶子节点。
由于K叉哈夫曼算法的性质决定,内节点一定包含K个叶子节点。
2、设:
N
o
d
e
m
a
x
Node_{max}
Nodemax为最优树中通路长度最长的内节点,则其K个叶子节点的权一定为
q
1
,
q
2
,
…
,
q
K
q_1,q_2,…,q_K
q1,q2,…,qK
若
L
x
L_x
Lx为
N
o
d
e
m
a
x
Node_{max}
Nodemax的一叶子节点,但是其权重
q
x
∉
{
q
1
,
q
2
,
…
,
q
K
}
q_x\notin\{q_1,q_2,…,q_K\}
qx∈/{q1,q2,…,qK}
L
y
L_y
Ly为上层的某叶子节点
q
x
∈
{
q
1
,
q
2
,
…
,
q
K
}
q_x\in\{q_1,q_2,…,q_K\}
qx∈{q1,q2,…,qK}, 即,
q
x
>
q
y
q_x > q_y
qx>qy
则,可以计算其
T
(
n
)
T(n)
T(n):
T
(
n
)
=
g
(
n
)
+
L
(
x
)
∗
q
x
+
L
(
y
)
∗
L
y
T(n)=g(n)+L(x)*q_x+L(y)*L_y
T(n)=g(n)+L(x)∗qx+L(y)∗Ly… … … … … …(1)
其中,
g
(
n
)
g(n)
g(n)为除去
L
x
L_x
Lx和
L
y
L_y
Ly的加权路径和。
L
(
x
)
∗
q
x
L(x)*q_x
L(x)∗qx为
L
x
L_x
Lx的加权路径,
L
y
L_y
Ly同理。
若,交换节点
L
x
L_x
Lx和
L
y
L_y
Ly
则,可以计算其
T
∗
(
n
)
:
T^*(n):
T∗(n):
T
∗
(
n
)
=
g
(
n
)
+
L
(
y
)
∗
q
x
+
L
(
x
)
∗
q
y
T^*(n)=g(n)+L(y)*q_x+L(x)*q_y
T∗(n)=g(n)+L(y)∗qx+L(x)∗qy… … … … … …(2)
(1) - (2)得:
T
(
n
)
−
T
∗
(
n
)
=
T(n)-T^*(n)=
T(n)−T∗(n)=
(
L
(
x
)
−
L
(
y
)
)
∗
q
x
(L(x)-L(y))*q_x
(L(x)−L(y))∗qx+
(
L
(
y
)
−
L
(
x
)
)
∗
q
y
(L(y)-L(x))*q_y
(L(y)−L(x))∗qy
∵
L
(
x
)
>
L
(
y
)
\because L(x)>L(y)
∵L(x)>L(y)且
q
x
⩾
q
y
q_x \geqslant q_y
qx⩾qy
∴
T
(
n
)
−
T
∗
(
n
)
⩾
0
\therefore T(n)-T^*(n) \geqslant 0
∴T(n)−T∗(n)⩾0
即,
T
(
n
)
⩾
T
∗
(
n
)
T(n) \geqslant T^*(n)
T(n)⩾T∗(n),当且仅当
q
y
=
q
k
,
q
x
=
q
k
+
1
,
q
k
=
q
k
+
1
q_y = q_k, q_x = q_{k+1}, q_k = q_{k+1}
qy=qk,qx=qk+1,qk=qk+1时,取等。
∴ \therefore ∴得证,带权 q 1 , q 2 , . . . , q k q_1,q_2,...,q_k q1,q2,...,qk为兄弟节点
证明 Q 2 : 最 优 树 收 缩 与 展 开 同 为 最 优 树 \mathbf{Q_2}:最优树收缩与展开同为最优树 Q2:最优树收缩与展开同为最优树
设:将带权
q
1
,
q
2
,
.
.
.
,
q
k
q_1,q_2,...,q_k
q1,q2,...,qk的K个叶子节点收缩至
N
o
d
e
m
a
x
Node_{max}
Nodemax(其权重为:
∑
i
=
1
k
q
i
\sum_{i=1}^{k}q_i
∑i=1kqi)
得到新树
T
∗
(
n
−
k
)
:
T^*(n-k):
T∗(n−k):
T
(
n
)
=
T
∗
(
n
−
k
)
+
∑
i
=
1
k
q
i
T(n)=T^*(n-k)+\displaystyle \sum_{i=1}^k q_i
T(n)=T∗(n−k)+i=1∑kqi … … … … … … (3)
必有与之对应的最优树
T
(
n
−
k
)
T(n-k)
T(n−k),将其中
N
o
d
e
m
a
x
Node_{max}
Nodemax展开,
得,
T
(
n
−
k
)
=
T
∗
(
n
)
−
∑
i
=
1
k
q
i
T(n-k)=T^*(n)-\displaystyle \sum_{i=1}^k q_i
T(n−k)=T∗(n)−i=1∑kqi … … … … … … (4)
两式作和:
T
(
n
)
+
T
(
n
−
k
)
=
T
∗
(
n
−
k
)
+
T
∗
(
n
)
T(n)+T(n-k)=T^*(n-k)+T^*(n)
T(n)+T(n−k)=T∗(n−k)+T∗(n)
当且仅当,
{
T
(
n
)
=
T
∗
(
n
)
T
(
n
−
k
)
=
T
∗
(
n
−
k
)
\begin{cases} T(n) = T^*(n) \\ T(n-k) = T^*(n-k) \\ \end{cases}
{T(n)=T∗(n)T(n−k)=T∗(n−k) 时,成立
∴ \therefore ∴得证,最优树收缩与展开同为最优树
证明 Q 3 : 最 优 树 的 合 并 仍 为 最 优 树 \mathbf{Q_3}:最优树的合并仍为最优树 Q3:最优树的合并仍为最优树
∵
Q
2
:
\because Q_2:
∵Q2:最优树收缩与展开同为最优树,
∴
\therefore
∴当子树
T
(
x
)
,
T
(
y
)
,
.
.
.
,
T
(
k
)
T(x),T(y),...,T(k)
T(x),T(y),...,T(k)K个子树分别收缩于
T
(
n
)
T(n)
T(n)的子节点,它们再次收缩,直至只剩下
T
(
n
)
T(n)
T(n)节点,然后反向展开,得到的树仍为最优树
∴ \therefore ∴得证,最优树的合并仍为最优树
综上所述,哈夫曼树(K叉)是最优树,是一种带权路径长度最短的树
代码实现(内含详尽注释)
/**
* @author LWJ
* @date 2021/11/12
* @brief K Way Merge Problem
* Copyright (C), 2021 Algorithms Analysis and Design
*/
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <stack>
#include <utility>
#include <assert.h>
using namespace std;
基本数据结构定义
/**
* @brief 树节点类
* 使用vector存储指针域,使得支持K路归并
*/
class __TreeNode
{
public:
/**
* @brief 虚节点构造器
*/
__TreeNode()
{
m_file_name = "";
m_file_size = 0;
}
/**
* @brief 叶子节点构造器
* @param file_name 文件名
* @param file_size 文件大小
*/
__TreeNode(string file_name, unsigned file_size)
{
m_file_name = file_name;
m_file_size = file_size;
}
/**
* @brief 内节点构造器
* @param file_name 文件名
* @param file_size 文件大小
* @param ptr_vec K元指针域
*/
__TreeNode(string file_name, unsigned file_size,
vector<__TreeNode *>ptr_vec)
{
m_file_name = file_name;
m_file_size = file_size;
m_ptr_vec = ptr_vec;
}
/**
* @brief 析构器
* 当删除根节点时,释放其自身的指针域指向的指针
* 而且指针域指针即为指向子节点的指针,
* 从而删除根节点,即可删除整棵树
*/
~__TreeNode()
{
for (auto ptr : m_ptr_vec)
{
if (ptr)
{
delete ptr;
}
}
}
/**
* @brief 增加内节点表示的文件长度(用于内节点构造阶段)
* @param increase 增量
*/
inline void inc_size(unsigned increase) { m_file_size += increase; }
/**
* @brief 添加子节点(用户内节点构造阶段)
* @param child 待添加的子节点
*/
inline void push_child(__TreeNode *child) { m_ptr_vec.push_back(child); }
/**
* @brief 获取文件名
* @return 文件名
*/
inline const string get_name() const { return m_file_name; }
/**
* @brief 获取文件大小
* @return 文件大小
*/
inline const unsigned get_size() const { return m_file_size; }
/**
* @brief 获取子节点序列
* @return 子节点序列
*/
inline const auto &get_children() const { return m_ptr_vec; }
private:
string m_file_name; // 文件名
unsigned m_file_size; // 文件大小
vector<__TreeNode*> m_ptr_vec; // 指针域
};
K路归并问题所需基本数据结构定义
// 文件基本信息(文件名,文件长度)
using File_info = pair<string, unsigned>;
// 文件序列
using File_list = vector<File_info>;
// 最优值
using OPT_value = size_t;
算法所需比较规则定义
// 树节点指针的自定义升序比较(函数对象)
class tree_node_com_greater
{
public:
inline bool operator()(__TreeNode *a, __TreeNode *b)
{
return a->get_size() > b->get_size();
}
};
K叉树(最优解)打印所需基本缩进常量(层级符号)
// 例如:某种2叉树
// root
// |___F1 (2)
// | |___F2 (22)
// | \___F3 (21)
// \___F4 (1)
// |___F5 (12)
// | |___F6 (122)
// | \___F7 (121)
// \___F8 (11)
// 易发现层级符号具有规律,
// 由此可以快速构建可视化多叉树
// 类似于(Linux中的tree命令)
//
constexpr auto node = "|___";
constexpr auto next_level = "| ";
constexpr auto last_node = "\\___";
constexpr auto last_branch = " ";
/**
* @brief K叉树可视化(最优解可视化)
* 使用 前序遍历 的方式实现可视化
* @param root K叉树根节点
*/
void __visual_tree(const __TreeNode *root)
{
// 初始化栈
stack<__TreeNode *> preorder_stack;
for (auto child_node = root->get_children().rbegin();
child_node != root->get_children().rend(); ++child_node)
{
preorder_stack.push(*child_node);
}
// 初始化层索引(用于确定层级相对位置,输出相应的层级符号)
vector<size_t> index_each_level;
index_each_level.push_back(root->get_children().size());
// 打印根节点
cout << "Final File" << endl;
// 开始打印K叉树
while (!preorder_stack.empty())
{
// 打印层级符号
for (auto itr = index_each_level.begin();
itr != index_each_level.end() - 1; ++itr)
{
switch (*itr)
{
case 1:
cout << last_branch;
break;
default:
cout << next_level;
break;
}
}
__TreeNode *top = preorder_stack.top();
preorder_stack.pop();
if (index_each_level.back() == 1)
{
cout << last_node << top->get_name();
if (top->get_children().empty())
{
while (!index_each_level.empty()
&& index_each_level.back() == 1)
{
index_each_level.pop_back();
}
if (!index_each_level.empty())
{
--index_each_level.back();
}
}
else
{
index_each_level.push_back(top->get_children().size());
}
}
else
{
cout << node << top->get_name();
if (top->get_children().empty())
{
--index_each_level.back();
}
else
{
index_each_level.push_back(top->get_children().size());
}
}
cout << endl;
// 前序遍历
for (auto child_node = top->get_children().rbegin();
child_node != top->get_children().rend(); ++child_node)
{
preorder_stack.push(*child_node);
}
}
}
/**
* @brief K路归并(算法入口)
* @param files_vec 待归并文件序列
* @param K 归并路数
* @return 最优值
*/
OPT_value k_way_merge(File_list &files_vec, const unsigned K)
{
// 校验输入合法性
assert(files_vec.size() > 2 && K >= 2);
// 构造优先队列实现贪心策略
priority_queue<__TreeNode *, vector<__TreeNode *>, tree_node_com_greater> pro_K_queue;
// 构建K叉哈夫曼树叶子节点
for (auto &file : files_vec)
{
__TreeNode *temp = new __TreeNode(file.first, file.second);
pro_K_queue.push(temp);
}
// 贪心策略构建K叉哈夫曼树,求取最优解
while (pro_K_queue.size() != 1)
{
// 局部权重(局部K份文件最小长度和)
__TreeNode *temp = new __TreeNode("Package", 0);
for (unsigned i = 0; i < K && !pro_K_queue.empty(); ++i)
{
temp->inc_size(pro_K_queue.top()->get_size());
temp->push_child(pro_K_queue.top());
pro_K_queue.pop();
}
pro_K_queue.push(temp);
}
// 获取根节点
__TreeNode *root = pro_K_queue.top();
// 清空优先队列
pro_K_queue.pop();
// 层序遍历K叉哈夫曼树,求取最优值
// 初始化层序遍历队列
queue<__TreeNode *> level_queue;
for (auto &itm : root->get_children())
{
level_queue.push(itm);
}
// 记录1层节点个数
size_t length = level_queue.size();
// 初始化最优值
OPT_value result = 0;
// 开始层序遍历计算最优值
for (size_t i = 1, level = 1; !level_queue.empty(); ++i)
{
// 遍历第level层的i个节点
// 计算叶节点的加权最短路径长度(最优值)
if (level_queue.front()->get_name() != "Package")
{
result += level_queue.front()->get_size() * level;
}
// 下一层节点入队
for (auto &itm : level_queue.front()->get_children())
{
level_queue.push(itm);
}
// 弹出当前节点
level_queue.pop();
// 当前层遍历完成
if (i == length)
{
// 获取下一层节点个数
length = level_queue.size();
// 重置i为0
i = 0;
// 层数递增
++level;
}
}
// 树型结构最优解可视化
__visual_tree(root);
// 堆内存释放
delete root;
return result;
}
感谢阅读!
有疑问或者认为有错误请留言,谢谢!